Injects new Reddit profile pictures into Old Reddit and Reddit-Stream.com next to the username. Caches in localstorage. This version requires an API key. Enter your API Key under CLIENT_ID and CLIENT_SECRET or it will not work.
- // ==UserScript==// @name Old Reddit with New Reddit Profile Pictures - API Key Version - Reddit-Stream Version// @namespace typpi.online// @version 7.0.7// @description Injects new Reddit profile pictures into Old Reddit and Reddit-Stream.com next to the username. Caches in localstorage. This version requires an API key. Enter your API Key under CLIENT_ID and CLIENT_SECRET or it will not work.// @author Nick2bad4u// @match *://reddit-stream.com/*// @connect reddit.com// @connect reddit-stream.com// @grant GM_xmlhttpRequest// @homepageURL https://github.com/Nick2bad4u/UserStyles// @license Unlicense// @resource https://www.google.com/s2/favicons?sz=64&domain=reddit-stream.com// @icon https://www.google.com/s2/favicons?sz=64&domain=reddit-stream.com// @icon64 https://www.google.com/s2/favicons?sz=64&domain=reddit-stream.com// @run-at document-start// @tag reddit// ==/UserScript==(function () {'use strict';console.log('Reddit Profile Picture Injector Script loaded');// Reddit API credentialsconst CLIENT_ID = 'EnterClientIDHere';const CLIENT_SECRET = 'EnterClientSecretHere';const USER_AGENT = 'ProfilePictureInjector/7.0.6 by Nick2bad4u';let accessToken = localStorage.getItem('accessToken');// Retrieve cached profile pictures and timestamps from localStoragelet profilePictureCache = JSON.parse(localStorage.getItem('profilePictureCache') || '{}',);let cacheTimestamps = JSON.parse(localStorage.getItem('cacheTimestamps') || '{}',);const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days in millisecondsconst MAX_CACHE_SIZE = 100000; // Maximum number of cache entriesconst cacheEntries = Object.keys(profilePictureCache);// Rate limit variableslet rateLimitRemaining = 1000;let rateLimitResetTime = 0;// eslint-disable-next-line @typescript-eslint/no-unused-varsconst resetDate = new Date(rateLimitResetTime);// eslint-disable-next-line @typescript-eslint/no-unused-varsconst now = Date.now();// Save the cache to localStoragefunction saveCache() {localStorage.setItem('profilePictureCache',JSON.stringify(profilePictureCache),);localStorage.setItem('cacheTimestamps', JSON.stringify(cacheTimestamps));}// Remove old cache entriesfunction flushOldCache() {console.log('Flushing old Reddit profile picture URL cache');const now = Date.now();for (const username in cacheTimestamps) {if (now - cacheTimestamps[username] > CACHE_DURATION) {console.log(`Deleting cache for Reddit user - ${username}`);delete profilePictureCache[username];delete cacheTimestamps[username];}}saveCache();console.log('Old cache entries flushed');}// Limit the size of the cache to the maximum allowed entriesfunction limitCacheSize() {const cacheEntries = Object.keys(profilePictureCache);if (cacheEntries.length > MAX_CACHE_SIZE) {console.log(`Current cache size: ${cacheEntries.length}`);console.log('Cache size exceeded, removing oldest entries');const sortedEntries = cacheEntries.sort((a, b) => cacheTimestamps[a] - cacheTimestamps[b],);const entriesToRemove = sortedEntries.slice(0,cacheEntries.length - MAX_CACHE_SIZE,);entriesToRemove.forEach((username) => {delete profilePictureCache[username];delete cacheTimestamps[username];});saveCache();console.log(`Cache size limited to ${MAX_CACHE_SIZE.toLocaleString()} URLs`,);}}function getCacheSizeInBytes() {const cacheEntries = Object.keys(profilePictureCache);let totalSize = 0;// Calculate size of profilePictureCachecacheEntries.forEach((username) => {const pictureData = profilePictureCache[username];const timestampData = cacheTimestamps[username];// Estimate size of data by serializing to JSON and getting the lengthtotalSize += new TextEncoder().encode(JSON.stringify(pictureData)).length;totalSize += new TextEncoder().encode(JSON.stringify(timestampData),).length;});return totalSize; // in bytes}function getCacheSizeInMB() {return getCacheSizeInBytes() / (#### * ####); // Convert bytes to MB}function getCacheSizeInKB() {return getCacheSizeInBytes() / ####; // Convert bytes to KB}// Obtain an access token from Reddit APIasync function getAccessToken() {console.log('Obtaining access token');const credentials = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`);try {const response = await fetch('https://www.reddit.com/api/v1/access_token',{method: 'POST',headers: {Authorization: `Basic ${credentials}`,'Content-Type': 'application/x-www-form-urlencoded',},body: 'grant_type=client_credentials',},);if (!response.ok) {console.error('Failed to obtain access token:', response.statusText);return null;}const data = await response.json();accessToken = data.access_token;const expiration = Date.now() + data.expires_in * 1000;localStorage.setItem('accessToken', accessToken);localStorage.setItem('tokenExpiration', expiration.toString());console.log('Access token obtained and saved');return accessToken;} catch (error) {console.error('Error obtaining access token:', error);return null;}}// Fetch profile pictures for a list of usernamesasync function fetchProfilePictures(usernames) {console.log('Fetching profile pictures');const now = Date.now();const tokenExpiration = parseInt(localStorage.getItem('tokenExpiration'),10,);// Check rate limitif (rateLimitRemaining <= 0 && now < rateLimitResetTime) {console.warn('Rate limit reached. Waiting until reset...');const timeRemaining = rateLimitResetTime - now;const minutesRemaining = Math.floor(timeRemaining / 60000);const secondsRemaining = Math.floor((timeRemaining % 60000) / 1000);console.log(`Rate limit will reset in ${minutesRemaining} minutes and ${secondsRemaining} seconds.`,);await new Promise((resolve) =>setTimeout(resolve, rateLimitResetTime - now),);}// Refresh access token if expiredif (!accessToken || now > tokenExpiration) {accessToken = await getAccessToken();if (!accessToken) return null;}// Filter out cached usernamesconst uncachedUsernames = usernames.filter((username) =>!profilePictureCache[username] &&username !== '[deleted]' &&username !== '[removed]',);if (uncachedUsernames.length === 0) {console.log('All usernames are cached');return usernames.map((username) => profilePictureCache[username]);}// Fetch profile pictures for uncached usernamesconst fetchPromises = uncachedUsernames.map(async (username) => {try {const response = await fetch(`https://oauth.reddit.com/user/${username}/about`,{headers: {Authorization: `Bearer ${accessToken}`,'User-Agent': USER_AGENT,},},);// Update rate limitrateLimitRemaining =parseInt(response.headers.get('x-ratelimit-remaining')) ||rateLimitRemaining;rateLimitResetTime =now + parseInt(response.headers.get('x-ratelimit-reset')) * 1000 ||rateLimitResetTime;// Log rate limit informationconst timeRemaining = rateLimitResetTime - now;const minutesRemaining = Math.floor(timeRemaining / 60000);const secondsRemaining = Math.floor((timeRemaining % 60000) / 1000);console.log(`Rate Limit Requests Remaining: ${rateLimitRemaining} requests, reset in ${minutesRemaining} minutes and ${secondsRemaining} seconds`,);if (!response.ok) {console.error(`Error fetching profile picture for ${username}: ${response.statusText}`,);return null;}const data = await response.json();if (data.data && data.data.icon_img) {const profilePictureUrl = data.data.icon_img.split('?')[0];profilePictureCache[username] = profilePictureUrl;cacheTimestamps[username] = Date.now();saveCache();console.log(`Fetched profile picture: ${username}`);return profilePictureUrl;} else {console.warn(`No profile picture found for: ${username}`);return null;}} catch (error) {console.error(`Error fetching profile picture for ${username}:`, error);return null;}});// eslint-disable-next-line @typescript-eslint/no-unused-varsconst r###lts = await Promise.all(fetchPromises);limitCacheSize();return usernames.map((username) => profilePictureCache[username]);}// Inject profile pictures into commentsasync function injectProfilePictures(comments) {console.log(`Comments found: ${comments.length}`);const usernames = Array.from(comments).map((comment) => comment.textContent.trim()).filter((username) => username !== '[deleted]' && username !== '[removed]',);const profilePictureUrls = await fetchProfilePictures(usernames);let injectedCount = 0; // Counter for injected profile picturescomments.forEach((comment, index) => {const username = usernames[index];const profilePictureUrl = profilePictureUrls[index];if (profilePictureUrl &&!comment.previousElementSibling?.classList.contains('profile-picture')) {console.log(`Injecting profile picture: ${username}`);const img = document.createElement('img');img.src = profilePictureUrl;img.classList.add('profile-picture');img.onerror = () => {img.style.display = 'none';};img.addEventListener('click', () => {window.open(profilePictureUrl, '_blank');});comment.insertAdjacentElement('beforebegin', img);const enlargedImg = document.createElement('img');enlargedImg.src = profilePictureUrl;enlargedImg.classList.add('enlarged-profile-picture');document.body.appendChild(enlargedImg);img.addEventListener('mouseover', () => {enlargedImg.style.display = 'block';const rect = img.getBoundingClientRect();enlargedImg.style.top = `${rect.top + window.scrollY + 20}px`;enlargedImg.style.left = `${rect.left + window.scrollX + 20}px`;});img.addEventListener('mouseout', () => {enlargedImg.style.display = 'none';});injectedCount++; // Increment count after successful injection}});console.log(`Profile pictures injected this run: ${injectedCount}`);console.log(`Current cache size: ${cacheEntries.length}`);console.log(`Cache size limited to ${MAX_CACHE_SIZE}`);const currentCacheSizeMB = getCacheSizeInMB();const currentCacheSizeKB = getCacheSizeInKB();console.log(`Current cache size: ${currentCacheSizeMB.toFixed(2)} MB or ${currentCacheSizeKB.toFixed(2)} KB`,);const timeRemaining = rateLimitResetTime - Date.now();const minutesRemaining = Math.floor(timeRemaining / 60000);const secondsRemaining = Math.floor((timeRemaining % 60000) / 1000);console.log(`Rate Limit Requests Remaining: ${rateLimitRemaining}, refresh in ${minutesRemaining} minutes and ${secondsRemaining} seconds`,);}// Set up a MutationObserver to detect new commentsfunction setupObserver() {console.log('Setting up observer to detect reddit comments');const processedComments = new Set(); // Track already processed commentslet newCommentsBatch = []; // Store new comments temporarilylet batchTimeout; // Timeout variable for batchinglet isFirstRun = true; // Flag to check if it's the first runconst observer = new MutationObserver((mutations) => {mutations.forEach((mutation) => {mutation.addedNodes.forEach((node) => {if (node.nodeType === Node.ELEMENT_NODE) {const newComments = Array.from(node.querySelectorAll('.author, .c-username'),).filter((comment) => !processedComments.has(comment));if (newComments.length > 0) {newComments.forEach((comment) => {processedComments.add(comment);newCommentsBatch.push(comment); // Add to batch});// Clear previous timeout and set a new one for batchingclearTimeout(batchTimeout);// Set a delay for the first run, then use regular debounce for othersbatchTimeout = setTimeout(() => {injectProfilePictures(newCommentsBatch);newCommentsBatch = []; // Reset the batchisFirstRun = false; // Disable first run flag after initial run},isFirstRun ? 150 : 100,); // First run delay: 1000ms, regular: 300ms}}});});});observer.observe(document.body, {childList: true,subtree: true,});console.log('Observer initialized');}// Run the scriptfunction runScript() {flushOldCache();console.log('Cache loaded:', profilePictureCache);setupObserver();}window.addEventListener('load', () => {console.log('Page loaded');runScript();});// Add CSS styles for profile picturesconst style = document.createElement('style');style.textContent = `.profile-picture {width: 20px;height: 20px;border-radius: 50%;margin-right: 5px;transition: transform 0.2s ease-in-out;position: relative;z-index: 1;cursor: pointer;}.enlarged-profile-picture {width: 250px;height: 250px;border-radius: 50%;position: absolute;display: none;z-index: 1000;pointer-events: none;outline: 3px solid #000;box-shadow: 0 4px 8px rgba(0, 0, 0, 1);background-color: rgba(0, 0, 0, 1);}`;document.head.appendChild(style);})();