您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.4 // @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 credentials const CLIENT_ID = 'EnterClientIDHere'; const CLIENT_SECRET = 'EnterClientSecretHere'; const USER_AGENT = 'ProfilePictureInjector/7.0.2 by Nick2bad4u'; let accessToken = localStorage.getItem( 'accessToken', ); // Retrieve cached profile pictures and timestamps from localStorage let profilePictureCache = JSON.parse( localStorage.getItem('profilePictureCache') || '{}', ); let cacheTimestamps = JSON.parse( localStorage.getItem('cacheTimestamps') || '{}', ); const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds const MAX_CACHE_SIZE = 100000; // Maximum number of cache entries const cacheEntries = Object.keys( profilePictureCache, ); // Rate limit variables let rateLimitRemaining = 1000; let rateLimitResetTime = 0; const resetDate = new Date(rateLimitResetTime); const now = Date.now(); // Save the cache to localStorage function saveCache() { localStorage.setItem( 'profilePictureCache', JSON.stringify(profilePictureCache), ); localStorage.setItem( 'cacheTimestamps', JSON.stringify(cacheTimestamps), ); } // Remove old cache entries function 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 entries function 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 profilePictureCache cacheEntries.forEach((username) => { const pictureData = profilePictureCache[username]; const timestampData = cacheTimestamps[username]; // Estimate size of data by serializing to JSON and getting the length totalSize += new TextEncoder().encode( JSON.stringify(pictureData), ).length; totalSize += new TextEncoder().encode( JSON.stringify(timestampData), ).length; }); return totalSize; // in bytes } function getCacheSizeInMB() { return getCacheSizeInBytes() / (1024 * 1024); // Convert bytes to MB } function getCacheSizeInKB() { return getCacheSizeInBytes() / 1024; // Convert bytes to KB } // Obtain an access token from Reddit API async 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 usernames async function fetchProfilePictures(usernames) { console.log('Fetching profile pictures'); const now = Date.now(); const tokenExpiration = parseInt( localStorage.getItem('tokenExpiration'), 10, ); // Check rate limit if ( 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 expired if (!accessToken || now > tokenExpiration) { accessToken = await getAccessToken(); if (!accessToken) return null; } // Filter out cached usernames const 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 usernames const 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 limit rateLimitRemaining = parseInt( response.headers.get( 'x-ratelimit-remaining', ), ) || rateLimitRemaining; rateLimitResetTime = now + parseInt( response.headers.get( 'x-ratelimit-reset', ), ) * 1000 || rateLimitResetTime; // Log rate limit information const 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; } }, ); const results = await Promise.all( fetchPromises, ); limitCacheSize(); return usernames.map( (username) => profilePictureCache[username], ); } // Inject profile pictures into comments async 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 pictures comments.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 comments function setupObserver() { console.log( 'Setting up observer to detect reddit comments', ); const processedComments = new Set(); // Track already processed comments let newCommentsBatch = []; // Store new comments temporarily let batchTimeout; // Timeout variable for batching let isFirstRun = true; // Flag to check if it's the first run const 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 batching clearTimeout(batchTimeout); // Set a delay for the first run, then use regular debounce for others batchTimeout = setTimeout( () => { injectProfilePictures( newCommentsBatch, ); newCommentsBatch = []; // Reset the batch isFirstRun = 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 script function runScript() { flushOldCache(); console.log( 'Cache loaded:', profilePictureCache, ); setupObserver(); } window.addEventListener('load', () => { console.log('Page loaded'); runScript(); }); // Add CSS styles for profile pictures const 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); })();