Ночной охотник за аватарками - продвинутый стратегический скрипт
Что делает:
1. Находит ник последнего комментатора и пытается подгрузить его аватар. 2. Использует кеш (localStorage), карту userMap и парсинг профилей. 3. Перебирает несколько кандидатных URL и резервных селекторов. 4. Фильтрует дефолтные/системные картинки, ставит только корректные авы.
Поведение:
1. Работает автономно, устойчив к неудачам и CORS; логирует процесс в консоль. 2. Логика подробно описана в теле скрипта - читайте, чтобы понять, как он достигает цели.
Лозунг: Работает по ночам, находит всегда.
Код
<script> /* Скрипт ночной охотник за авами - продвинутый режим профи - пробует найти аватар в .last_post (строго) - если не найдёт, пробует запасные селекторы - использует originalUsername для построения путей, нормализованный ключ для кэша - расширенный лог (console) для диагностики - устойчив к CORS/ошибкам fetch (в таком случае ставит дефолт) */
function saveCache(){ localStorage.setItem(CACHE_KEY, JSON.stringify(cache)); }
function normalizeKey(name){ return (name || '').trim().toLowerCase(); }
function findAvatarSrcInDoc(doc){ const selectors = [ // специфичные селекторы для uCoz и популярных движков '.user_avatar img', 'img[src*="/avatar/"]', 'img[src*="/ava/"]', '.ipsUserPhoto img', '.ipsUserPhotoLink img', 'img.avatar', 'img.profile-avatar', '.profile-avatar img' ]; for(const s of selectors){ const el = doc.querySelector(s); if(el && el.src){ const src = el.src; if(!/noava|default|guest|anon|empty/i.test(src)){ return new URL(src, location.href).href; } } } return null; }
async function fetchAndFindAvatar(url){ try{ const res = await fetch(url, { cache: 'no-cache' }); if(!res.ok){ console.debug('autoAvatar: fetch returned not ok', url, res.status); return null; } const txt = await res.text(); const parser = new DOMParser(); const doc = parser.parseFromString(txt, 'text/html'); return findAvatarSrcInDoc(doc); }catch(e){ // Может быть CORS или сетевой сбой console.warn('autoAvatar: fetch failed (CORS/network?)', url, e); return null; } }
function buildProfileCandidates(href, originalUsername){ const candidates = []; if(href){ try{ candidates.push(new URL(href, location.href).href); }catch(e){} } if(originalUsername){ // оставляем вариант с кодировкой и без — иногда работает один из них const safe = encodeURIComponent(originalUsername.replace(/\s+/g, '-')); candidates.push(location.origin + '/index/8-0-' + safe); candidates.push(location.origin + '/index/8-0-' + originalUsername); // иногда используется /user/ID или /users/USERNAME candidates.push(location.origin + '/user/' + originalUsername); candidates.push(location.origin + '/users/' + originalUsername); } return Array.from(new Set(candidates)).filter(Boolean); }
// возвращает блок .last_post (если есть) и информацию о нике и href function getLastCommentInfo(topicEl){ // сначала целевой блок last_post const lastBlock = topicEl.querySelector('.last_post, .lastpost, .last-post'); let anchor = null; if(lastBlock){ anchor = lastBlock.querySelector('.uLPost, a, .lastpost_author a, .username, .user a'); } // запасной вариант — искать по общим селекторам внутри темы if(!anchor){ anchor = topicEl.querySelector('.last_post .uLPost, .last_post a, .uLPost, .last_post span a, a[href*="/index/8-"], a[href*="/user/"], .last_user a, .lastpost_author a, .username a'); } if(!anchor) return null; const username = (anchor.textContent || '').trim(); const href = anchor.getAttribute('href') || null; return { username: username || null, href: href }; }
// по теме пытаемся найти элемент img для замены. // сначала строго внутри .last_post, иначе пробуем ряд запасных контейнеров, // но помним — не хотим случайно поставить аву автора темы вместо последнего коммента. function findAvatarImgElement(topicEl){ // 1) строго в .last_post let lastBlock = topicEl.querySelector('.last_post, .lastpost, .last-post'); if(lastBlock){ const img = lastBlock.querySelector('img[class*="avatar"], img.ipsUserPhoto, .user_avatar img, img.profile-avatar'); if(img) return img; }
// 2) запасной — элемент .uLPost рядом с ником const uL = topicEl.querySelector('.uLPost, .lastpost_author, .last_user'); if(uL){ const maybe = uL.querySelector('img[class*="avatar"], img.ipsUserPhoto, img.profile-avatar'); if(maybe) return maybe; }
// 3) более общий запасной — ищем img рядом с селектором последнего автора const infoAnchor = topicEl.querySelector('.last_post a, .last_post .uLPost, .lastpost_author a, .last_user a, a[href*="/index/8-"], a[href*="/user/"]'); if(infoAnchor){ // ищем ближайший <img> в родителях/соседях let p = infoAnchor.parentElement; for(let i=0;i<4 && p;i++, p = p.parentElement){ const img = p.querySelector('img[class*="avatar"], img.ipsUserPhoto, img.profile-avatar, .user_avatar img'); if(img) return img; } }
// 4) если совсем ничего — возвращаем null return null; }
async function handleTopic(topicEl){ try{ const avatarImg = findAvatarImgElement(topicEl); if(!avatarImg){ console.debug('autoAvatar: avatar element not found for topic', topicEl); return; } if(avatarImg.dataset.autoAvatarDone) return; avatarImg.dataset.autoAvatarDone = '1';
const info = getLastCommentInfo(topicEl); if(!info || !info.username){ console.debug('autoAvatar: no last author info for topic'); return; }
// 0) если уже стоит нормальная картинка (и не дефолт) — ничего не делать try{ const cur = avatarImg.getAttribute('src') || avatarImg.src; if(cur && !/noava|default|guest|anon|empty/i.test(cur)){ console.debug('autoAvatar: current img looks valid, skip', cur); // но всё равно кешируем под ключ если нет в кэше if(!cache[cacheKey]){ cache[cacheKey] = cur; saveCache(); } return; } }catch(e){ /* ignore read errors */ }
// 1) кэш if(cache[cacheKey]){ avatarImg.src = cache[cacheKey]; console.info('autoAvatar: set from cache', originalUsername, cache[cacheKey]); return; }
async function processAll(root = document){ // расширенный набор селекторов тем (попытаемся покрыть разные шаблоны) const topics = Array.from(root.querySelectorAll( '.__topic, tr.__topic, .bb_box .ipb_table .__topic, .topic, .forum-row, .threads-list-item' )); for(const t of topics){ await handleTopic(t); await new Promise(r=>setTimeout(r, CHECK_DELAY)); } }
// запускаем как можно раньше, но если DOMContentLoaded уже был- process сразу if(document.readyState === 'loading'){ document.addEventListener('DOMContentLoaded', ()=>{ processAll().catch(console.error); }); } else { // даём маленькую паузу, чтобы динамический контент успел вставиться setTimeout(()=> processAll().catch(console.error), 20); }
const obs = new MutationObserver(muts=>{ for(const m of muts){ for(const n of m.addedNodes || []){ if(n.nodeType === 1){ processAll(n).catch(console.error); } } } }); obs.observe(document.body, { childList: true, subtree: true });