Skip to content

Commit

Permalink
use SocialMediaPosting for video urls
Browse files Browse the repository at this point in the history
RIP graphql
  • Loading branch information
flurrux committed Jun 17, 2023
1 parent ba44967 commit 319ca56
Show file tree
Hide file tree
Showing 15 changed files with 311 additions and 50 deletions.
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@ this extension breaks occasionally (maybe once a month) due to Instagram updatin

so before you use this extension, you have to be willing to wait for a fix when something breaks and then [manually re-install the extension](#install).

## current breakage
## previous breakage

June 13th 2023

Instagram introduced breaking changes yet again and it's currently not possible to download videos from posts (of course).
Instagram introduced breaking changes yet again and that broke, of course, video downloads.

previously i've intercepted calls to `graphql` to obtain the video urls, but these have disappeared now.
but not all is lost. if i'm seeing this correctly, Instagram might have made it easier than ever to get the video url, by inserting all of its metadata into the DOM directly.
i am trying to fix this as soon as possible!
i did spot a script element in the DOM that has all of the video urls and that's what i'm using now instead.
two potential problems with video quality and carousel index:
- i don't know if the video qualitites are the best possible
- downloading videos from carousels may give you the wrong file! always make sure you have gotten the right one.

download the latest release (1.3.18) for this fix.

## current limitations are:

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { observeCarouselIndex } from "../../../carousel-index-observer";
import { queryMediaElement } from "../media-and-src/query-media-element";
import { waitForElementExistence } from "../../../../lib/await-element";
import { Lazy } from "fp-ts/es6/function";

// the object `SocialMediaPosting` contains videos and images of a carousel,
// but they are in seperate arrays.
// how do we get the correct video at a given carousel index?
// it appears that the order of the videos is the same as their order in the carousel.

// example:
// carousel: [ image1, image2, video1, image3, video2, video3, image4, video4 ]
// SocialMediaPosting.video: [ video1, video2 , video3, video4 ]
// SocialMediaPosting.image: [ image1, image2 , image3, image4 ]

// so if we are at index 4 in the carousel which is item `video2`,
// how do we know that it is the second video?
// the only idea i have so far is to keep track of how many videos we have
// scrolled past in the carousel.
// this module provides an observer for the video index.
// be cautious! it may not be very robust!
// always doublecheck your downloads!


export function makeVideoIndexObserver(postElement: HTMLElement): Lazy<number> {

let videoIndex = 0;

(async function(){
const carouselElement = await waitForElementExistence(100, 5, postElement, "ul");

let videoIndexInitialized = false;
let isCurrentlyVideo = false;
let previousIndex = 0;

observeCarouselIndex(
carouselElement,
({ child, index }) => {
const mediaElement = queryMediaElement(child);
if (!mediaElement) return;

const isVideoElement = mediaElement.matches("video");

if (!videoIndexInitialized) {
isCurrentlyVideo = isVideoElement;
videoIndex = isCurrentlyVideo ? 0 : -1;
videoIndexInitialized = true;
}
else {
if (index > previousIndex) {
const isVideoNext = isVideoElement;
videoIndex += isVideoNext ? 1 : 0;
}
else if (index < previousIndex) {
const wasVideoPrevious = isCurrentlyVideo;
videoIndex += wasVideoPrevious ? -1 : 0;
}
isCurrentlyVideo = isVideoElement;
}
previousIndex = index;
}
);
})();

return () => videoIndex;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Option, none, some } from "fp-ts/es6/Option";
import { SocialMediaPosting } from "./types";

// my previous methods of obtaining video urls is now broken,
// but Instagram has kindly provided us with the exact
// data we need in the DOM.
// i believe this works only on post pages and not on the mainfeed,
// but i haven't checked yet.

export function findSocialMediaPostingInDom(): Option<SocialMediaPosting> {
const script = document.querySelector('script[type="application/ld+json"]');
if (!script) return none;

try {
const scriptParsed = JSON.parse(script.innerHTML);
if (typeof(scriptParsed) !== "object") return none;

// i've seen instances where the parsed result is an array
// instead of a single object.
// let's pack the object in an array so that we won't have
// fragmented logic
const resultArray = (
Array.isArray(scriptParsed) ? scriptParsed : [scriptParsed]
);

const postingItem = resultArray.find(
(item) => {
if (typeof(item) !== "object") return false;
return item["@type"] === "SocialMediaPosting";
}
);
if (!postingItem) return none;

// TODO: validation of type
return some(postingItem as SocialMediaPosting);
}
catch(e){
return none;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Option, fromNullable, isNone, none } from "fp-ts/es6/Option";
import { PostType } from "../../from-fetch-response/types";
import { SingleMediaInfo } from "../../media-types";
import { findTypeOfPost } from "../post-type";
import { tryGetImageSrc } from "../../hybrid/try-get-image-src";
import { findUsernameInPost } from "../post-username";
import { getCurrentPageType, isSinglePostType } from "../../../insta-navigation-observer";
import { makeVideoIndexObserver } from "./carousel-video-index";
import { SocialMediaPosting } from "./types";
import { findSocialMediaPostingInDom } from "./find-in-dom";

// make a function that lazily extracts media from this post.
// if it's an image, it will query the image source.
// if it's a video, it will use `SocialMediaPosting` taken
// from a certain script element in the DOM.
// for carousels, it is also necessary to keep track of the
// current video index, because videos and images are stored
// in separate arrays.

// granted, it is not very pretty!
// i have basically copy pasted this code from another place
// when i was still doing fetches.
// for fetches, it was necessary to cache as many values as possible.
// i should definitely rewrite this function and split it
// into multiple cases (image, single video, carousel, ...).

export function makeSocialMediaPostingExtractor(postElement: HTMLElement){

// videoIndex is not needed if this is an image,
// but since everything is done lazily, we need
// to keep track of the video index if it's a carousel,
// to have it ready when the download button is pressed.
const getVideoIndex = makeVideoIndexObserver(postElement);

// cached values
let socialMediaPosting: Option<SocialMediaPosting> = none;
let currentPostType: Option<PostType> = none;

return async (): Promise<SingleMediaInfo | undefined> => {

// <post type> ------------------------

if (isNone(currentPostType)) {
currentPostType = fromNullable(findTypeOfPost(postElement));
}

// check again if postType is some
if (isNone(currentPostType)) {
console.error("could not find type of post", postElement);
return;
}

const postType = currentPostType.value;

// </post type> ------------------------



// if this current post or carousel item is an image,
// then we can quickly find its source
const imageSrcData = tryGetImageSrc(postType, postElement);
if (imageSrcData) {
const username = findUsernameInPost(postElement);
return {
username: username as string,
...imageSrcData
}
}

// case: single video or carousel video on mainfeed
if (!isSinglePostType(getCurrentPageType())) {
console.warn("please open the page of this post in a new tab. downloading videos directly from the mainfeed is currently not supported.");
return;
}


// case: single- or carousel-video on post-page

// <social media posting> -----------------

if (isNone(socialMediaPosting)) {
socialMediaPosting = findSocialMediaPostingInDom();
}

if (isNone(socialMediaPosting)) {
console.error("could not find social media posting in DOM");
return;
}

const mediaPost = socialMediaPosting.value;
const videoItems = mediaPost.video;
const username = mediaPost.author.identifier.value;

// </social media posting> -----------------


const videoIndex = getVideoIndex();
if (videoIndex < 0 || videoIndex >= videoItems.length){
console.warn("video index is out of bounds. somethings wrong!");
return;
}

return {
type: "video",
username,
src: videoItems[videoIndex].contentUrl
};
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export type SocialMediaPosting = {
"@type": "SocialMediaPosting",
articleBody: string,
author: Person,
identifier: {
propertyID: "Post Shortcode",
value: string
},
image: ImageObject[],
video: VideoObject[]
}

type Person = {
"@type": "Person",

name: string, // more like display name

alternateName: string, // this seems to be the real username, but also could be an alias

identifier: {
propertyID: "Username",
value: string // i suppose use this for foldernames
},

image: string, // profile pic maybe

url: string
}

export type ImageObject = {
width: string,
height: string,
representativeOfPage: boolean,
url: string
}

export type VideoObject = {
width: string,
height: string,
thumbnailUrl: string,
contentUrl: string
}
5 changes: 3 additions & 2 deletions app/src/download-button-injection/post-button-injection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Predicate } from "fp-ts/es6/Predicate";
import { Option, elem, isNone, none, some } from "fp-ts/es6/Option";
import { findInAncestors } from "../../lib/find-dom-ancestor";
import { Either, isLeft, left, right } from "fp-ts/es6/Either";
import { makeSocialMediaPostingExtractor } from "../data-extraction/directly-in-browser/social-media-posting/media-provider";


function findSavePostElement(postElement: HTMLElement) {
Expand Down Expand Up @@ -49,7 +50,7 @@ const applyPostDownloadElementStyle = (postElement: HTMLElement, element: HTMLEl

function makeAndPrepareDownloadButton(postElement: HTMLElement){
const downloadButton = createDiskDownloadButton(
makeLazyMediaExtractor(postElement)
makeSocialMediaPostingExtractor(postElement)
);
applyPostDownloadElementStyle(postElement, downloadButton);

Expand Down Expand Up @@ -131,7 +132,7 @@ async function autoShowLinkForMainFeedVideos(

observeCarouselIndex(
carouselElement,
({ child }) => {
({ child, index }) => {
const mediaElement = queryMediaElement(child);
if (!mediaElement) return;
setDownloadButtonVisible(
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
(async () => {
await import(
/* @vite-ignore */
chrome.runtime.getURL("assets/index.ts.3e77b15c.js")
chrome.runtime.getURL("assets/index.ts.a2827a5a.js")
);
})().catch(console.error);

Expand Down
35 changes: 0 additions & 35 deletions dist/assets/index.ts.3e77b15c.js

This file was deleted.

Loading

0 comments on commit 319ca56

Please sign in to comment.