3
\$\begingroup\$

I've been using the InstagramPhotoLink extension for Opera for some time but instagram broke it recently. I thought I could try to fix it... and I did.

The original extension is rather limited and it supports only a link to the first image and no videos so while fixing it I completely rewrote it in order to extend it.

It works by looking for an image or video element with a specific class and extracts the src attribute from it and puts it in a new a-element inside the media-container.

Switching images/videos is supported by the MutationObserver that updates the links when either a div or an attribute changes.

I came across a problem where the MutatorObserver reacts too quickly and before the src attribute of the new image is set. I solved it by adding a small delay by adding a call to setTimeout to wait until everything is updated.

function createMediaLink() {

    let mo = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            if (mutation.type == "attributes") {
                //console.log(mutation.attributeName);
                addOrUpdateVideoLink();
            }
            if (mutation.type == "childList") {
                //console.log(mutation.childList);
                addOrUpdateImageLink();
            }
        });
    });

    return (
        addOrUpdateImageLink(mo) ||
        addOrUpdateVideoLink(mo)
    );
}

function addOrUpdateImageLink(mo) {

    let image = document.getElementsByClassName('FFVAD');

    if (image.length == 0) {
        return false;
    }

    image = image[0];

    if (mo) {
        // the nearest div to the image that stays there when switching images
        var div = document.getElementsByClassName("rQDP3");
        mo.observe(div[0], {
            childList: true
        });
    }

    setTimeout(() => {
        addOrUpdateMediaLink(image.src);
    }, 50);

    return true;
}

function addOrUpdateVideoLink(mo) {

    let video = document.getElementsByClassName('tWeCl');

    if (video.length == 0) {
        return false;
    }

    video = video[0];

    if (mo) {
        mo.observe(video, {
            attributes: true
        });
    }

    setTimeout(() => {
        addOrUpdateMediaLink(video.src);
    }, 50);

    return true;
}

function addOrUpdateMediaLink(src) {
    console.log(`src: ${src}`);
    let a = document.getElementsByClassName('_open_');
    if (a.length == 0) {
        a = document.createElement("a");
        a.className = "_open_";
        a.innerHTML = 'Open in new tab';
        a.target = "_blank";
        // media container
        document.getElementsByClassName("_97aPb")[0].appendChild(a);
    } else {
        a = a[0];
    }
    a.href = src;
}

createMediaLink();

I don't write javascript too often so this is the best I can do but I'm pretty sure this can still be improved. What do you think?

\$\endgroup\$
4
  • \$\begingroup\$ Do you use this when viewing a list of images/videos and/or when viewing a single item (and navigating through a collection - e.g. using left/right clicks/arrow key presses)? \$\endgroup\$ Commented Jun 11, 2018 at 4:20
  • \$\begingroup\$ @SamOnela this works only in single item view, not lists. \$\endgroup\$
    – t3chb0t
    Commented Jun 11, 2018 at 4:23
  • \$\begingroup\$ Okay - I am trying to see the line var div = document.getElementsByClassName("rQDP3"); working... if I go to the album view cats_of_instagram I don't see any results there, then if I click on an item, it opens the modal but I still don't see any results there... if I refresh the item is displayed but not in a modal. When do you see results for that? \$\endgroup\$ Commented Jun 11, 2018 at 16:01
  • \$\begingroup\$ @SamOnela try this class name _97aPb - this seems to be a more common one that works on any page. I see that the one I used in my question applies only to certain pages :-| \$\endgroup\$
    – t3chb0t
    Commented Jun 11, 2018 at 16:12

2 Answers 2

4
\$\begingroup\$
  • addOrUpdateImageLink() and addOrUpdateVideoLink() are essentially the same, therefore it would be better to create just one function that would accept different parameters dependent on whether our media is an image or video;
  • In createMediaLink() you invoke addOrUpdate………Link() functions before you declare them. It is not wrong, but it worsens readability and code flow;
  • Setting 50 ms delay is not the best way to achieve what you are trying to do. Since I don't know how the DOM workings look like, I left it unchanged though;
  • Use strict equality operator (===) wherever possible — it performs no type conversion;
  • Once you pick which quotes you use ('' or ""), stick to it. Generally, single quotes option is more popular and standard.

Rewrite

const addOrUpdateMediaLink = src => {
  if (!src) { return; }
  console.log(`src: ${src}`);

  let a = document.querySelector('._open_');

  if (!a) {
    a = document.createElement('a');
    [a.className, a.textContent, a.target] = ['_open_', 'Open in new tab', '_blank'];

    // media container
    document.querySelector('._97aPb').appendChild(a);
  }

  a.href = src;
};

const addOrUpdate = (selector, observeSelector, attr, mo) => {
  let media = document.querySelector(selector);

  if (!media) { return false; }

  if (mo) {
    const obj = {};
    obj[attr] = true;
    mo.observe(document.querySelector(observeSelector), obj);
  }

  setTimeout(() => addOrUpdateMediaLink(media.src), 50);

  return true;
};

const createMediaLink = () => {
  const selectors = {
    attributes: ['.tWeCl', '.tWeCl'],
    childList:  ['.FFVAD', '.rQDP3']
  };

  const mo = new MutationObserver(mutations => mutations.forEach(mutation => addOrUpdate(selectors[mutation.type][0])));

  return addOrUpdate(...selectors.childList, 'childList', mo) || addOrUpdate(...selectors.attributes, 'attributes', mo);
};

createMediaLink();
\$\endgroup\$
4
  • 1
    \$\begingroup\$ wow, this looks awsome! I hadn't thought there would be possbile so many tricks :-) \$\endgroup\$
    – t3chb0t
    Commented Jun 9, 2018 at 17:40
  • 1
    \$\begingroup\$ The 50ms is just a small randomly picked number because without it the link was often empty when you switched to the next image because instagram is rebuilding an element tree that wasn't always complete. \$\endgroup\$
    – t3chb0t
    Commented Jun 9, 2018 at 17:42
  • 1
    \$\begingroup\$ I had to make one fix. This didn't work return addOrUpdate(mo, ...params); so I changed it to return addOrUpdate(mo, ...selectors['img'], 'childList') || addOrUpdate(mo, ...selectors['video'], 'attributes'); - but maybe it's just me who isn't entirely understanding the new syntax yet because in order to learn it didn't just copy paste it but wrote it on my own so I might have made a mistake somewhere ;-) \$\endgroup\$
    – t3chb0t
    Commented Jun 9, 2018 at 18:47
  • \$\begingroup\$ Yeah, I was in a hurry when I wrote this. Now, after giving it one more second, I better understood what's going on. I've updated the code in my answer. \$\endgroup\$
    – Przemek
    Commented Jun 10, 2018 at 13:21
1
\$\begingroup\$

As is evidenced by the original version of the extension being broken by the HTML update, one drawback of this type of code is that it is brittle. Instagram could update the HTML of their site at anytime, leaving the extension broken. A different approach might be wise. For example, the code could look for video elements - while it is possible to have more than one on a page, the Instagram pages that I looked at appear to either only have one or no video elements. If there are none, then look at the images. Obviously there will likely be more than one image on the page (since the logo and other icons will be present). I'm not sure what the best approach to find the image in focus (maybe see if it has a parent element that is the <article> element, though that also would be coupled to the structure) but I guess one could look for the image with the largest dimensions.

But if you want to stick with the current approach, the rewritten code in Przemek's answer looks more succint and uniform with . There are a few other suggestions below that might improve things.


You could use an IIFE to wrap all the code up. While the extension cannot use variables or functions defined by web pages or by other content scripts1, it would be a good practice not to add functions that could have the same name as one in the original source code. While it would be unlikely that the Instagram scripts would have a function called createMediaLink, it is possible. And it could also be used to inject certain global variables- e.g. document, window.

;(doc => {
    //addOrUpdateMediaLink, etc.

    //contents from crateMediaLink, 

})(document);

cutOutTheMiddleman

The function passed to setTimeout could be a partial-function, which would eliminate the extra function call. For example, the following block from addOrUpdateImageLink:

setTimeout(() => {
    addOrUpdateMediaLink(image.src);
}, 50);

Could be simplified like this:

setTimeout(addOrUpdateMediaLink.bind(null, image.src), 50); 

Cool magic, right? (∩`-´)⊃━☆゚.*・。゚


Inside addOrUpdateImageLink there is a new variable created:

var div = document.getElementsByClassName("rQDP3");

Some JS purists might argue that const should be used instead of var, since that is never re-assigned. And also, the naming might be a little misleading, since that function returns an HTMLCollection, so divs would be more appropraite. The same is true for let image = document.getElementsByClassName('FFVAD'); in addOrUpdateImageLink() - since multiple elements are returned images would be more appropriate.


1https://dev.opera.com/extensions/content-scripts/

\$\endgroup\$
2
  • \$\begingroup\$ Even more magic! But I love it and the links will make the study easier :-) \$\endgroup\$
    – t3chb0t
    Commented Jun 11, 2018 at 18:20
  • \$\begingroup\$ The idea with injecting global variable is nice but unfortuantelly extensions are running in a sendbox and they don't have access to any custom javascript variables e.g. on the window object so even when injected like in your example it's not possible to pass them to the script :-( there is one very useful window._sharedData that contains all the blob: viedeo links but the extension does not see it. \$\endgroup\$
    – t3chb0t
    Commented Jun 13, 2018 at 16:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.