Skip to content

Instantly share code, notes, and snippets.

@manciuszz
Last active Apr 24, 2019
Embed
What would you like to do?
Unloop The Tube. Tired of YouTube autoplay suggested videos being stuck on loop? Well look no further, because this neat little script will make sure that your next autoplay video will never be the same! (...literally)
// ==UserScript==
// @name Unloop The Tube.
// @author Manciuszz
// @version 1.01
// @match https://www.youtube.com/*
// @grant unsafeWindow
// @updateURL https://gist.githubusercontent.com/manciuszz/eef680b89bfe04e6c293b88cb3b10d08/raw
// ==/UserScript==
(function(window) {
'use strict';
let getCurrentVideoId = function() {
return location.href.replace(/.*v=(.*)&?.*/g, "$1");
}
let fetchYTData = function(callbackFn) {
if (typeof callbackFn === "function") {
fetch(`https://www.youtube.com/watch?v=${getCurrentVideoId()}&pbj=1`, {
"method": "GET",
"headers": {
"x-youtube-client-name": window.yt.config_.INNERTUBE_CONTEXT_CLIENT_NAME,
"x-youtube-client-version": window.yt.config_.INNERTUBE_CONTEXT_CLIENT_VERSION,
"x-youtube-identity-token": window.yt.config_.ID_TOKEN
},
}).then(res => res.json()).then(myJson => callbackFn(myJson));
}
};
class VideoState {
constructor(ytData) {
if (typeof ytData === "undefined")
return console.log("Failed to get 'ytData'!");
this.ytData = ytData;
}
unloopTheTube() {
this.markCurrentlyWatchingAsSeen();
if (this.nextVideoDetails.wasAlreadySeen) {
this.selectNotSeenVideo();
}
this.storage.saveVideos();
}
markCurrentlyWatchingAsSeen() {
this.storage.seenVideos[this.currentVideoDetails.videoId] = this.currentVideoDetails.title;
}
selectNotSeenVideo(idx = 0) {
let videos = this.suggestedUnseenVideos || [];
if (videos.length > 0) {
this.nextVideoDetails.metadataMethods.updateWith(videos[idx]);
}
}
get suggestedVideos() {
return this.ytData[3].response.contents.twoColumnWatchNextResults.secondaryResults.secondaryResults.results;
}
get suggestedUnseenVideos() {
let videos = this.suggestedVideos;
let filterUnseen = function() {
let unseenVideos = [];
videos.forEach((v, i) => {
let videoObject = Object.values(v)[0];
if ("contents" in videoObject)
videoObject = Object.values(videoObject.contents[0])[0];
if (!("playlistId" in videoObject) && ("lengthText" in videoObject)) {
if (!videoObject.navigationEndpoint.watchEndpoint.startTimeSeconds) {
if (videoObject.thumbnailOverlays.length < 3)
unseenVideos.push(videoObject);
}
}
});
return unseenVideos;
};
return filterUnseen();
}
get currentVideoDetails() {
return {
title: document.querySelector("#container > .title").textContent,
videoId: getCurrentVideoId()
};
}
get nextVideoDetails() {
let autoplayVideo = this.suggestedVideos[0].compactAutoplayRenderer.contents[0].compactVideoRenderer;
return {
wasAlreadySeen: !!this.storage.seenVideos[autoplayVideo.videoId] || autoplayVideo.navigationEndpoint.watchEndpoint.startTimeSeconds > 0 || autoplayVideo.thumbnailOverlays.length > 2,
get metadataMethods() {
let nextButton = document.querySelector(".ytp-next-button");
return {
set videoEndpoint(href) {
let newHref = () => { location.href = href; };
nextButton.href = href;
nextButton.onclick = newHref;
document.querySelector(".video-stream.html5-main-video").onended = newHref;
},
change(videoElement, newMetadata) {
let img = videoElement.querySelector("img");
let header = videoElement.querySelector("h3");
let overlays = videoElement.querySelector("#overlays");
let links = videoElement.querySelectorAll("a")
if (nextButton) {
nextButton.dataset.preview = newMetadata.preview;
nextButton.dataset.duration = newMetadata.duration;
nextButton.dataset.tooltipText = newMetadata.tooltipText;
}
if (img) img.src = newMetadata.preview;
if (header) header.textContent = newMetadata.tooltipText;
if (overlays) {
new MutationObserver(function(mutations) { // TODO: Might want to change this into somethign better?
(function(resumeBar, thumbnail) {
if (resumeBar) resumeBar.remove();
if (thumbnail) thumbnail.textContent = newMetadata.duration;
})(overlays.querySelector("ytd-thumbnail-overlay-resume-playback-renderer"), overlays.querySelector("ytd-thumbnail-overlay-time-status-renderer"));
this.disconnect();
}).observe(overlays, { childList: true });
}
if (links.length) {
links.forEach( (a) => {
a.href = newMetadata.newURL;
});
}
},
updateWith(selectedNewVideo) {
let newURL = `https://www.youtube.com/watch?v=${selectedNewVideo.videoId}`;
this.videoEndpoint = newURL;
let autoplayRenderer = document.querySelector("ytd-compact-autoplay-renderer ytd-compact-video-renderer");
if (autoplayRenderer) {
this.change(autoplayRenderer, {
preview: selectedNewVideo.thumbnail.thumbnails[0].url,
duration: selectedNewVideo.lengthText.simpleText,
tooltipText: selectedNewVideo.title.simpleText,
newURL: newURL
});
}
}
};
}
};
};
get storage() {
let seenVideos_key = "unloop_yt::seenVideos";
if (!this._seenVideos)
this._seenVideos = JSON.parse((function(storageItem) { return ({[storageItem]: storageItem, "undefined": undefined})[storageItem]; })(localStorage.getItem(seenVideos_key)) || "{}") || {};
return {
seenVideos: this._seenVideos,
saveVideos() {
localStorage.setItem(seenVideos_key, JSON.stringify(this.seenVideos));
},
clearVideos() {
localStorage.removeItem(seenVideos_key);
},
};
}
};
window.onload = function() {
let unloopTheTube = function() {
if (location.pathname != "/watch")
return;
fetchYTData(ytData => new VideoState(ytData).unloopTheTube());
};
window.addEventListener("yt-navigate-finish", unloopTheTube);
window.onYouTubeIframeAPIReady = unloopTheTube();
};
})(unsafeWindow);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment