Old Reddit with New Reddit Profile Pictures - API Key Version

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.

目前为 2024-11-10 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Old Reddit with New Reddit Profile Pictures - API Key Version
  3. // @namespace https://github.com/Nick2bad4u/UserStyles
  4. // @version 6.6
  5. // @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.
  6. // @author Nick2bad4u
  7. // @match *://*.reddit.com/*
  8. // @match *://reddit-stream.com/*
  9. // @connect reddit.com
  10. // @connect reddit-stream.com
  11. // @grant GM_xmlhttpRequest
  12. // @homepageURL https://github.com/Nick2bad4u/UserStyles
  13. // @license Unlicense
  14. // @resource https://www.google.com/s2/favicons?sz=64&domain=reddit.com
  15. // @icon https://www.google.com/s2/favicons?sz=64&domain=reddit.com
  16. // @icon64 https://www.google.com/s2/favicons?sz=64&domain=reddit.com
  17. // @run-at document-start
  18. // @tag reddit
  19. // ==/UserScript==
  20.  
  21. (function () {
  22. 'use strict';
  23. console.log('Reddit Profile Picture Injector Script loaded');
  24.  
  25. // Reddit API credentials
  26. const CLIENT_ID = 'EnterClientIDHere';
  27. const CLIENT_SECRET = 'EnterClientSecretHere';
  28. const USER_AGENT = 'ProfilePictureInjector/6.6 by Nick2bad4u';
  29. let accessToken = localStorage.getItem('accessToken');
  30.  
  31. // Retrieve cached profile pictures and timestamps from localStorage
  32. let profilePictureCache = JSON.parse(localStorage.getItem('profilePictureCache') || '{}');
  33. let cacheTimestamps = JSON.parse(localStorage.getItem('cacheTimestamps') || '{}');
  34. const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
  35. const MAX_CACHE_SIZE = 100000; // Maximum number of cache entries
  36. const cacheEntries = Object.keys(profilePictureCache);
  37.  
  38. // Rate limit variables
  39. let rateLimitRemaining = 1000;
  40. let rateLimitResetTime = 0;
  41. const resetDate = new Date(rateLimitResetTime);
  42. const now = Date.now();
  43.  
  44. // Save the cache to localStorage
  45. function saveCache() {
  46. localStorage.setItem('profilePictureCache', JSON.stringify(profilePictureCache));
  47. localStorage.setItem('cacheTimestamps', JSON.stringify(cacheTimestamps));
  48. }
  49.  
  50. // Remove old cache entries
  51. function flushOldCache() {
  52. console.log('Flushing old Reddit profile picture URL cache');
  53. const now = Date.now();
  54. for (const username in cacheTimestamps) {
  55. if (now - cacheTimestamps[username] > CACHE_DURATION) {
  56. console.log(`Deleting cache for Reddit user - ${username}`);
  57. delete profilePictureCache[username];
  58. delete cacheTimestamps[username];
  59. }
  60. }
  61. saveCache();
  62. console.log('Old cache entries flushed');
  63. }
  64.  
  65. // Limit the size of the cache to the maximum allowed entries
  66. function limitCacheSize() {
  67. const cacheEntries = Object.keys(profilePictureCache);
  68. if (cacheEntries.length > MAX_CACHE_SIZE) {
  69. console.log(`Current cache size: ${cacheEntries.length} URLs`);
  70. console.log('Cache size exceeded, removing oldest entries');
  71. const sortedEntries = cacheEntries.sort((a, b) => cacheTimestamps[a] - cacheTimestamps[b]);
  72. const entriesToRemove = sortedEntries.slice(0, cacheEntries.length - MAX_CACHE_SIZE);
  73. entriesToRemove.forEach((username) => {
  74. delete profilePictureCache[username];
  75. delete cacheTimestamps[username];
  76. });
  77. saveCache();
  78. console.log(`Cache size limited to ${MAX_CACHE_SIZE.toLocaleString()} URLs`);
  79. }
  80. }
  81.  
  82. function getCacheSizeInBytes() {
  83. const cacheEntries = Object.keys(profilePictureCache);
  84. let totalSize = 0;
  85.  
  86. // Calculate size of profilePictureCache
  87. cacheEntries.forEach((username) => {
  88. const pictureData = profilePictureCache[username];
  89. const timestampData = cacheTimestamps[username];
  90.  
  91. // Estimate size of data by serializing to JSON and getting the length
  92. totalSize += new TextEncoder().encode(JSON.stringify(pictureData)).length;
  93. totalSize += new TextEncoder().encode(JSON.stringify(timestampData)).length;
  94. });
  95.  
  96. return totalSize; // in bytes
  97. }
  98.  
  99. function getCacheSizeInMB() {
  100. return getCacheSizeInBytes() / (1024 * 1024); // Convert bytes to MB
  101. }
  102.  
  103. function getCacheSizeInKB() {
  104. return getCacheSizeInBytes() / 1024; // Convert bytes to KB
  105. }
  106.  
  107. // Obtain an access token from Reddit API
  108. async function getAccessToken() {
  109. console.log('Obtaining access token');
  110. const credentials = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`);
  111. try {
  112. const response = await fetch('https://www.reddit.com/api/v1/access_token', {
  113. method: 'POST',
  114. headers: {
  115. Authorization: `Basic ${credentials}`,
  116. 'Content-Type': 'application/x-www-form-urlencoded',
  117. },
  118. body: 'grant_type=client_credentials',
  119. });
  120. if (!response.ok) {
  121. console.error('Failed to obtain access token:', response.statusText);
  122. return null;
  123. }
  124. const data = await response.json();
  125. accessToken = data.access_token;
  126. const expiration = Date.now() + data.expires_in * 1000;
  127. localStorage.setItem('accessToken', accessToken);
  128. localStorage.setItem('tokenExpiration', expiration.toString());
  129. console.log('Access token obtained and saved');
  130. return accessToken;
  131. } catch (error) {
  132. console.error('Error obtaining access token:', error);
  133. return null;
  134. }
  135. }
  136.  
  137. // Fetch profile pictures for a list of usernames
  138. async function fetchProfilePictures(usernames) {
  139. console.log('Fetching profile pictures');
  140. const now = Date.now();
  141. const tokenExpiration = parseInt(localStorage.getItem('tokenExpiration'), 10);
  142.  
  143. // Check rate limit
  144. if (rateLimitRemaining <= 0 && now < rateLimitResetTime) {
  145. console.warn('Rate limit reached. Waiting until reset...');
  146.  
  147. const timeRemaining = rateLimitResetTime - now;
  148. const minutesRemaining = Math.floor(timeRemaining / 60000);
  149. const secondsRemaining = Math.floor((timeRemaining % 60000) / 1000);
  150.  
  151. console.log(
  152. `Rate limit will reset in ${minutesRemaining} minutes and ${secondsRemaining} seconds.`
  153. );
  154. await new Promise((resolve) => setTimeout(resolve, rateLimitResetTime - now));
  155. }
  156.  
  157. // Refresh access token if expired
  158. if (!accessToken || now > tokenExpiration) {
  159. accessToken = await getAccessToken();
  160. if (!accessToken) return null;
  161. }
  162.  
  163. // Filter out cached usernames
  164. const uncachedUsernames = usernames.filter(
  165. (username) =>
  166. !profilePictureCache[username] && username !== '[deleted]' && username !== '[removed]'
  167. );
  168. if (uncachedUsernames.length === 0) {
  169. console.log('All usernames are cached');
  170. return usernames.map((username) => profilePictureCache[username]);
  171. }
  172.  
  173. // Fetch profile pictures for uncached usernames
  174. const fetchPromises = uncachedUsernames.map(async (username) => {
  175. try {
  176. const response = await fetch(`https://oauth.reddit.com/user/${username}/about`, {
  177. headers: {
  178. Authorization: `Bearer ${accessToken}`,
  179. 'User-Agent': USER_AGENT,
  180. },
  181. });
  182.  
  183. // Update rate limit
  184. rateLimitRemaining =
  185. parseInt(response.headers.get('x-ratelimit-remaining')) || rateLimitRemaining;
  186. rateLimitResetTime =
  187. now + parseInt(response.headers.get('x-ratelimit-reset')) * 1000 || rateLimitResetTime;
  188.  
  189. // Log rate limit information
  190. const timeRemaining = rateLimitResetTime - now;
  191. const minutesRemaining = Math.floor(timeRemaining / 60000);
  192. const secondsRemaining = Math.floor((timeRemaining % 60000) / 1000);
  193.  
  194. console.log(
  195. `Rate Limit Requests Remaining: ${rateLimitRemaining}, 1000 more requests will be added in ${minutesRemaining} minutes and ${secondsRemaining} seconds`
  196. );
  197.  
  198. if (!response.ok) {
  199. console.error(`Error fetching profile picture for ${username}: ${response.statusText}`);
  200. return null;
  201. }
  202. const data = await response.json();
  203. if (data.data && data.data.icon_img) {
  204. const profilePictureUrl = data.data.icon_img.split('?')[0];
  205. profilePictureCache[username] = profilePictureUrl;
  206. cacheTimestamps[username] = Date.now();
  207. saveCache();
  208. console.log(`Fetched profile picture: ${username}`);
  209. return profilePictureUrl;
  210. } else {
  211. console.warn(`No profile picture found for: ${username}`);
  212. return null;
  213. }
  214. } catch (error) {
  215. console.error(`Error fetching profile picture for ${username}:`, error);
  216. return null;
  217. }
  218. });
  219.  
  220. const results = await Promise.all(fetchPromises);
  221. limitCacheSize();
  222. return usernames.map((username) => profilePictureCache[username]);
  223. }
  224.  
  225. // Inject profile pictures into comments
  226. async function injectProfilePictures(comments) {
  227. console.log(`Comments found: ${comments.length}`);
  228. const usernames = Array.from(comments)
  229. .map((comment) => comment.textContent.trim())
  230. .filter((username) => username !== '[deleted]' && username !== '[removed]');
  231. const profilePictureUrls = await fetchProfilePictures(usernames);
  232.  
  233. let injectedCount = 0; // Counter for injected profile pictures
  234.  
  235. comments.forEach((comment, index) => {
  236. const username = usernames[index];
  237. const profilePictureUrl = profilePictureUrls[index];
  238. if (
  239. profilePictureUrl &&
  240. !comment.previousElementSibling?.classList.contains('profile-picture')
  241. ) {
  242. console.log(`Injecting profile picture: ${username}`);
  243. const img = document.createElement('img');
  244. img.src = profilePictureUrl;
  245. img.classList.add('profile-picture');
  246. img.onerror = () => {
  247. img.style.display = 'none';
  248. };
  249. img.addEventListener('click', () => {
  250. window.open(profilePictureUrl, '_blank');
  251. });
  252. comment.insertAdjacentElement('beforebegin', img);
  253.  
  254. const enlargedImg = document.createElement('img');
  255. enlargedImg.src = profilePictureUrl;
  256. enlargedImg.classList.add('enlarged-profile-picture');
  257. document.body.appendChild(enlargedImg);
  258. img.addEventListener('mouseover', () => {
  259. enlargedImg.style.display = 'block';
  260. const rect = img.getBoundingClientRect();
  261. enlargedImg.style.top = `${rect.top + window.scrollY + 20}px`;
  262. enlargedImg.style.left = `${rect.left + window.scrollX + 20}px`;
  263. });
  264. img.addEventListener('mouseout', () => {
  265. enlargedImg.style.display = 'none';
  266. });
  267.  
  268. injectedCount++; // Increment count after successful injection
  269. }
  270. });
  271.  
  272. console.log(`Profile pictures injected this run: ${injectedCount}`);
  273. console.log(`Current cache size: ${cacheEntries.length}`);
  274. console.log(`Cache size limited to ${MAX_CACHE_SIZE.toLocaleString()} URLs`);
  275. const currentCacheSizeMB = getCacheSizeInMB();
  276. const currentCacheSizeKB = getCacheSizeInKB();
  277. console.log(
  278. `Current cache size: ${currentCacheSizeMB.toFixed(2)} MB or ${currentCacheSizeKB.toFixed(2)} KB`
  279. );
  280.  
  281. const timeRemaining = rateLimitResetTime - Date.now();
  282. const minutesRemaining = Math.floor(timeRemaining / 60000);
  283. const secondsRemaining = Math.floor((timeRemaining % 60000) / 1000);
  284. console.log(
  285. `Rate Limit Requests Remaining: ${rateLimitRemaining} requests, refresh in ${minutesRemaining} minutes and ${secondsRemaining} seconds`
  286. );
  287. }
  288.  
  289. function setupObserver() {
  290. console.log('Setting up observer');
  291. const observer = new MutationObserver((mutations) => {
  292. const comments = document.querySelectorAll('.author, .c-username');
  293. if (comments.length > 0) {
  294. console.log('New comments detected');
  295. observer.disconnect();
  296. injectProfilePictures(comments);
  297. }
  298. });
  299. observer.observe(document.body, {
  300. childList: true,
  301. subtree: true,
  302. });
  303. console.log('Observer initialized');
  304. }
  305.  
  306. // Run the script
  307. function runScript() {
  308. flushOldCache();
  309. console.log('Cache loaded:', profilePictureCache);
  310. setupObserver();
  311. }
  312.  
  313. window.addEventListener('load', () => {
  314. console.log('Page loaded');
  315. runScript();
  316. });
  317.  
  318. // Add CSS styles for profile pictures
  319. const style = document.createElement('style');
  320. style.textContent = `
  321. .profile-picture {
  322. width: 20px;
  323. height: 20px;
  324. border-radius: 50%;
  325. margin-right: 5px;
  326. transition: transform 0.2s ease-in-out;
  327. position: relative;
  328. z-index: 1;
  329. cursor: pointer;
  330. }
  331. .enlarged-profile-picture {
  332. width: 250px;
  333. height: 250px;
  334. border-radius: 50%;
  335. position: absolute;
  336. display: none;
  337. z-index: 1000;
  338. pointer-events: none;
  339. outline: 3px solid #000;
  340. box-shadow: 0 4px 8px rgba(0, 0, 0, 1);
  341. background-color: rgba(0, 0, 0, 1);
  342. }
  343. `;
  344. document.head.appendChild(style);
  345. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址