Skip to content

Commit

Permalink
Added a shuffle button to shorts pages (#243)
Browse files Browse the repository at this point in the history
  • Loading branch information
NikkelM authored Nov 30, 2023
1 parent 2d98413 commit eebdeba
Show file tree
Hide file tree
Showing 10 changed files with 321 additions and 132 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## v2.4.0 (Unreleased)

<!--Releasenotes start-->
- Shorts pages are now supported! Shuffle buttons can now be found on all shorts pages.
- Added a new option to shuffle only from shorts.
- When watching shorts, the extension will now update the last visited channel in the popup.
- Reduced the time it takes to shuffle, as the extension now uses a more sophisticated way to decide whether or not to check if a video has been deleted or not.
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Download the extension for: [Chrome/Chromium](https://chromewebstore.google.com/

Do you have a favourite YouTube channel, but don't know what to watch? This extension is for you!

The Random YouTube Video extension adds a 'Shuffle' button to YouTube channel and video pages, which will play a random video from the current channel. You can use the extension's popup to customize your experience further.
The Random YouTube Video extension adds a 'Shuffle' button to YouTube channel, video and shorts pages, which will play a truly random video from the current channel. You can use the extension's popup to customize your experience further.

Highlighted Features:<br>
- Choose from a wide range of options to individualize your experience, such as ignoring shorts, only shuffling from the most recent videos, shuffling multiple videos into a playlist, and much more!
Expand All @@ -57,9 +57,9 @@ Highlighted Features:<br>

Do you have ideas for new features or have encountered a bug? Please [open an issue](https://github.com/NikkelM/Random-YouTube-Video/issues/new/choose).

The `main` branch of this repository *should* always be stable. If you want to test the newest unreleased features, follow the steps below to create a version of the extension you can add to your browser.
The `main` branch of this repository *should* always be stable. If you want to test the newest unreleased features, follow the steps below to create a local version of the extension that you can install in the browser of your choice.
<br>
You can find out what new changes are coming with the newest unreleased version in the [changelog](https://github.com/NikkelM/Random-YouTube-Video/blob/main/CHANGELOG.md).
You can find out what new changes will be coming in the next version in the [changelog](https://github.com/NikkelM/Random-YouTube-Video/blob/main/CHANGELOG.md).
Did you find any bugs with the version you tested? Please let me know by [opening an issue](https://github.com/NikkelM/Random-YouTube-Video/issues/new/choose)!

### Installation
Expand All @@ -69,7 +69,7 @@ Did you find any bugs with the version you tested? Please let me know by [openin
- `npm run build:chromium` builds a distribution for Chrome/Chromium only.
- `npm run build:firefox` builds a distribution for Firefox only.
- Replace `build` with `dev` in the above commands to build a development version of the extension that updates automatically when you make changes to the code.
- The bundled extension files will be placed in the `dist/<browser>` directories.
- The bundled extension files will be placed in the `dist/<environment>` directories.
- You can load the extension in your browser by following the instructions below.

#### Chromium
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "random-youtube-video",
"version": "2.3.0",
"description": "Play a random video uploaded on the current YouTube channel.",
"description": "Customize, shuffle and play random videos from any YouTube channel.",
"scripts": {
"dev": "concurrently \"npm run dev:chromium\" \"npm run dev:firefox\"",
"dev:chromium": "webpack --env browser=chromium --watch --config webpack.dev.cjs",
Expand Down
6 changes: 3 additions & 3 deletions src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ async function readDataOnce(key) {
// ---------- Helpers ----------
async function getAPIKey(forceDefault, useAPIKeyAtIndex = null) {
// List of API keys that are stored in the database/locally
let availableAPIKeys = null;
let availableAPIKeys;

// If the user has opted to use a custom API key, use that instead of the default one
if (!forceDefault && configSync.useCustomApiKeyOption && configSync.customYoutubeApiKey) {
Expand Down Expand Up @@ -280,8 +280,8 @@ async function getAPIKey(forceDefault, useAPIKeyAtIndex = null) {
return { APIKey: availableAPIKeys.map(key => rot13(key, false)), isCustomKey: false, keyIndex: null };
}

let usedIndex = null;
let chosenAPIKey = null;
let usedIndex;
let chosenAPIKey;
if (useAPIKeyAtIndex === null) {
// Choose a random one of the available API keys to evenly distribute the quotas
usedIndex = Math.floor(Math.random() * availableAPIKeys.length);
Expand Down
4 changes: 2 additions & 2 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const configSyncDefaults = {

export const shufflingHints = [
// General extension hints
"The extension adds a 'Shuffle' button to all channel and video pages on YouTube, this button has the same behaviour as shuffling from the popup!",
"The extension adds a 'Shuffle' button to all channel, video and shorts pages on YouTube, this button has the same behaviour as shuffling from the popup!",
"If you are the first person to shuffle from a channel, the video ID's of that channel are saved both locally and in a remote database for other users to use!",
"Try using the extension on April 1st - maybe something unexpected will happen!",
"The extension does not collect any personal information, it only stores video ID's of channels you shuffle from, without linking them back to you!",
Expand Down Expand Up @@ -77,7 +77,7 @@ export const shufflingHints = [

// Changelog
"The popup will highlight the 'Changelog' button whenever a new version of the extension has been installed!",
"Are you wondering how the extension has changed over time? Check out the changelog using the button on the popup!",
"Want to stay up-to-date on the newest features and improvements? Check out the changelog using the button in the popup!",

// Meta/GitHub
"Do you have an idea on how this extension could be improved? Open an issue on GitHub and let me know!",
Expand Down
120 changes: 95 additions & 25 deletions src/content.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Content script that is injected into YouTube pages
import { setDOMTextWithDelay, getPageTypeFromURL, RandomYoutubeVideoError } from "./utils.js";
import { setDOMTextWithDelay, updateSmallButtonStyleForText, getPageTypeFromURL, RandomYoutubeVideoError, delay } from "./utils.js";
import { configSync, setSyncStorageValue } from "./chromeStorage.js";
import { buildShuffleButton, shuffleButton, shuffleButtonTextElement, tryRenameUntitledList } from "./domManipulation.js";
import { chooseRandomVideo } from "./shuffleVideo.js";
Expand All @@ -15,24 +15,24 @@ document.head.appendChild(iconFont);
// The only way for a button to already exist when the content script is loaded is if the extension was reloaded in the background
// That will cause the content script to be re-injected into the page, but the DOM will not be reloaded
// That in turn will disconnect the event listener (on Firefox), which will make the button not work
const videoShuffleButton = document.getElementById("youtube-random-video-shuffle-button-video");
const channelShuffleButton = document.getElementById("youtube-random-video-shuffle-button-channel");
if (videoShuffleButton || channelShuffleButton) {
const videoShuffleButton = document.getElementById("youtube-random-video-large-shuffle-button-video");
const channelShuffleButton = document.getElementById("youtube-random-video-large-shuffle-button-channel");
const shortShuffleButton = document.getElementById("youtube-random-video-small-shuffle-button-short");
if (videoShuffleButton || channelShuffleButton || shortShuffleButton) {
window.location.reload(true);
}

// After every navigation event, we need to check if this page needs a 'Shuffle' button
document.addEventListener("yt-navigate-finish", startDOMObserver);

async function startDOMObserver(event) {
// If the button previously displayed an error message, reset the text
resetShuffleButtonText();

let pageType = getPageTypeFromURL(window.location.href);

// Get the channel id from the event data
let channelId = null;
let channelName = null;
let channelId;
let channelName;

if (pageType == "video" || pageType == "short") {
channelId = event?.detail?.response?.playerResponse?.videoDetails?.channelId;
Expand Down Expand Up @@ -121,13 +121,23 @@ async function channelDetectedAction(pageType, channelId, channelName) {
function resetShuffleButtonText() {
// The element will not exist if the button has not been built yet
if (shuffleButtonTextElement) {
shuffleButtonTextElement.innerText = "\xa0Shuffle";
if (shuffleButtonTextElement.id.includes("large-shuffle-button")) {
shuffleButtonTextElement.innerText = "\xa0Shuffle";
} else if (shuffleButtonTextElement.innerText !== "autorenew") {
updateSmallButtonStyleForText(shuffleButtonTextElement, false);
shuffleButtonTextElement.innerText = "shuffle";
}
}
}

// ---------- Shuffle ----------
// Called when the 'Shuffle' button is clicked
async function shuffleVideos() {
resetShuffleButtonText();

// Shorts pages make a copy of the shuffleButtonTextElement to be able to spin it even if the user scrolls to another short, to keep the animation going
var shuffleButtonTextElementCopy = shuffleButtonTextElement;

let channelId;
try {
// Get the saved channelId from the button
Expand All @@ -147,30 +157,84 @@ async function shuffleVideos() {

// We need this variable to make sure the button text is only changed if the shuffle hasn't finished within the time limit
var hasBeenShuffled = false;
setDOMTextWithDelay(shuffleButtonTextElement, "\xa0Shuffling...", 1000, () => { return ((shuffleButtonTextElement.innerText === "\xa0Shuffle" || shuffleButtonTextElement.innerText === "\xa0Fetching: 100%") && !hasBeenShuffled); });
setDOMTextWithDelay(shuffleButtonTextElement, "\xa0Still on it...", 5000, () => { return ((shuffleButtonTextElement.innerText === "\xa0Shuffling..." || shuffleButtonTextElement.innerText === "\xa0Fetching: 100%") && !hasBeenShuffled); });
if (configSync.shuffleIgnoreShortsOption != "1") {
setDOMTextWithDelay(shuffleButtonTextElement, "\xa0Sorting shorts...", 8000, () => { return ((shuffleButtonTextElement.innerText === "\xa0Still on it..." || shuffleButtonTextElement.innerText === "\xa0Fetching: 100%") && !hasBeenShuffled); });

// Only use this text if the button is the large shuffle button, the small one only has space for an icon
if (shuffleButtonTextElement.id.includes("large-shuffle-button")) {
setDOMTextWithDelay(shuffleButtonTextElement, "\xa0Shuffling...", 1000, () => { return ((shuffleButtonTextElement.innerText === "\xa0Shuffle" || shuffleButtonTextElement.innerText === "\xa0Fetching: 100%") && !hasBeenShuffled); });
setDOMTextWithDelay(shuffleButtonTextElement, "\xa0Still on it...", 5000, () => { return ((shuffleButtonTextElement.innerText === "\xa0Shuffling..." || shuffleButtonTextElement.innerText === "\xa0Fetching: 100%") && !hasBeenShuffled); });
if (configSync.shuffleIgnoreShortsOption != "1") {
setDOMTextWithDelay(shuffleButtonTextElement, "\xa0Sorting shorts...", 8000, () => { return ((shuffleButtonTextElement.innerText === "\xa0Still on it..." || shuffleButtonTextElement.innerText === "\xa0Fetching: 100%") && !hasBeenShuffled); });
}
} else {
let iterationsWaited = 0;

let checkInterval = setInterval(async () => {
if (!hasBeenShuffled && (shuffleButtonTextElementCopy.innerText == "100%" || (shuffleButtonTextElementCopy.innerText == "shuffle" && iterationsWaited++ >= 15))) {
clearInterval(checkInterval);
await delay(400);

// If we have finished the shuffle between the check and the delay, we don't want to change the text
if (hasBeenShuffled) {
return;
}

updateSmallButtonStyleForText(shuffleButtonTextElementCopy, false);
shuffleButtonTextElementCopy.innerText = "autorenew";

let rotation = 0;
let rotateInterval = setInterval(() => {
if (hasBeenShuffled) {
clearInterval(rotateInterval);
return;
}
shuffleButtonTextElementCopy.style.transform = `rotate(${rotation}deg)`;
rotation = (rotation + 5) % 360;
}, 25);
} else if (hasBeenShuffled) {
clearInterval(checkInterval);
}
}, 150);
}

await chooseRandomVideo(channelId, false, shuffleButtonTextElement);
hasBeenShuffled = true;

// Reset the button text in case we opened the video in a new tab
shuffleButtonTextElement.innerText = "\xa0Shuffle";
if (shuffleButtonTextElement.id.includes("large-shuffle-button")) {
shuffleButtonTextElement.innerText = "\xa0Shuffle";
} else {
updateSmallButtonStyleForText(shuffleButtonTextElementCopy, false);
shuffleButtonTextElementCopy.innerText = "shuffle";
}
} catch (error) {
console.error(error);

hasBeenShuffled = true;
if (shuffleButton.id.includes("small-shuffle-button")) {
updateSmallButtonStyleForText(shuffleButtonTextElementCopy, true);
}

let displayText = "";
switch (error.name) {
case "RandomYoutubeVideoError":
displayText = `Error ${error.code}`;
break;
case "YoutubeAPIError":
displayText = `API Error ${error.code}`;
break;
default:
displayText = `Unknown Error`;
if (shuffleButton?.id?.includes("large-shuffle-button")) {
switch (error.name) {
case "RandomYoutubeVideoError":
displayText = `Error ${error.code}`;
break;
case "YoutubeAPIError":
displayText = `API Error ${error.code}`;
break;
default:
displayText = "Unknown Error";
}
} else {
switch (error.name) {
case "RandomYoutubeVideoError":
case "YoutubeAPIError":
displayText = error.code;
break;
default:
displayText = "Unknown Error";
}
}

// Special case: If the extension's background worker was reloaded, we need to reload the page to get the correct reference to the shuffle function again
Expand All @@ -190,12 +254,18 @@ The page will reload and you can try again.`)
return;
}

// Immediately display the error
if (shuffleButton?.id?.includes("large-shuffle-button")) {
shuffleButtonTextElement.innerText = `\xa0${displayText}`;
} else {
shuffleButtonTextElementCopy.innerText = displayText == "Unknown Error" ? "Error" : displayText;
}
// Small delay to allow for the DOM change to be rendered
await delay(10);

// Alert the user about the error
window.alert(`Random YouTube Video:\n\nChannel ${channelId}\n${displayText}${error.message ? "\n" + error.message : ""}${error.reason ? "\n" + error.reason : ""}${error.solveHint ? "\n" + error.solveHint : ""}${error.showTrace !== false ? "\n\n" + error.stack : ""}`);

// Immediately display the error
shuffleButtonTextElement.innerText = `\xa0${displayText}`;

return;
}
}
Loading

0 comments on commit eebdeba

Please sign in to comment.