// ==UserScript==
// @name Drive2 Old Auto UnSubscriber
// @namespace drive2.com
// @version 0.86
// @description работает на стренице подписок на сашины. проврка на бывшие авто и взаимные подписки, автоматически отписывается от бывших авто и от машин чьи владельцы не во взаимной подписке.
// @author drive2 lover
// @match https://www.drive2.com/*
// @match https://www.drive2.ru/*
// @license MIT
// @grant none
// ==/UserScript==
let tail = '';
let fctx = '';
const MY_CARS_KEY = 'my_cars__';
let myCars = localStorage.getItem(MY_CARS_KEY) ? JSON.parse(localStorage.getItem(MY_CARS_KEY)) : false;
const localStorageKey = 'saveLastPage';
let stats = {
pages: 0,
totalBlocks: 0,
processedBlocks: 0,
removedBlocks: 0,
unsubscribedBlocks: 0,
oldSubscribedBlocks: 0
};
// Создаем блок статистики
const statsDiv = document.createElement('div');
statsDiv.style.position = 'fixed';
statsDiv.style.top = '0';
statsDiv.style.left = '0';
statsDiv.style.backgroundColor = '#333';
statsDiv.style.color = '#fff';
statsDiv.style.padding = '15px';
statsDiv.style.margin = '10px';
statsDiv.style.zIndex = '1000';
// Блок "Отписываться от авто"
const unsubscribeCheckboxDiv = document.createElement('div');
const unsubscribeCheckbox = document.createElement('input');
unsubscribeCheckbox.type = 'checkbox';
unsubscribeCheckbox.checked = true;
unsubscribeCheckbox.id = 'unsubscribe-checkbox';
const unsubscribeLabel = document.createElement('label');
unsubscribeLabel.textContent = ' Отписываться от авто';
unsubscribeLabel.style.marginRight = '10px';
unsubscribeLabel.setAttribute('for', 'unsubscribe-checkbox');
unsubscribeCheckboxDiv.appendChild(unsubscribeCheckbox);
unsubscribeCheckboxDiv.appendChild(unsubscribeLabel);
// statsDiv.appendChild(unsubscribeCheckboxDiv);
// Блок "Скрывать старые авто"
const hideOldCarsCheckboxDiv = document.createElement('div');
const hideOldCarsCheckbox = document.createElement('input');
hideOldCarsCheckbox.type = 'checkbox';
hideOldCarsCheckbox.checked = true;
hideOldCarsCheckbox.id = 'hide-old-cars-checkbox';
const hideOldCarsLabel = document.createElement('label');
hideOldCarsLabel.textContent = ' Скрывать старые авто';
hideOldCarsLabel.style.marginRight = '10px';
hideOldCarsLabel.setAttribute('for', 'hide-old-cars-checkbox');
hideOldCarsCheckboxDiv.appendChild(hideOldCarsCheckbox);
hideOldCarsCheckboxDiv.appendChild(hideOldCarsLabel);
// statsDiv.appendChild(hideOldCarsCheckboxDiv);
// Кнопка "Проверить"
const checkButton = document.createElement('button');
checkButton.innerText = 'Проверить';
checkButton.style.marginTop = '10px';
checkButton.style.padding = '5px 10px';
checkButton.style.backgroundColor = '#007bff';
checkButton.style.color = '#fff';
checkButton.style.border = 'none';
checkButton.style.borderRadius = '5px';
checkButton.style.cursor = 'pointer';
// statsDiv.appendChild(checkButton);
// Блок для ввода страниц и кнопки "Пропустить страниц"
const skipPagesDiv = document.createElement('div');
skipPagesDiv.style.marginTop = '10px';
// Поле ввода количества страниц
const pagesInput = document.createElement('input');
pagesInput.type = 'number';
pagesInput.min = '1';
pagesInput.value = localStorage.getItem(localStorageKey) ?? 1;
pagesInput.style.width = '50px';
pagesInput.style.marginRight = '10px';
// Кнопка "Пропустить страниц"
const skipButton = document.createElement('button');
skipButton.innerText = 'Пропустить страниц';
skipButton.style.padding = '5px 10px';
skipButton.style.backgroundColor = '#dc3545';
skipButton.style.color = '#fff';
skipButton.style.border = 'none';
skipButton.style.borderRadius = '5px';
skipButton.style.cursor = 'pointer';
// Добавляем инпут и кнопку в блок
skipPagesDiv.appendChild(pagesInput);
skipPagesDiv.appendChild(skipButton);
//statsDiv.appendChild(skipPagesDiv);
const carBlock = document.querySelector('.l-container .u-link-area');
const carBlockClass = carBlock?.parentElement?.classList?.[0] ?? null;
const carsBlockClass = carBlock?.parentElement?.parentElement?.className ?? null;
const carsBlockClassFormatted = carsBlockClass?.split(' ').map(className => '.' + className).join('') ?? '';
console.log('carBlockClass ' + carBlockClass);
console.log('carsBlockClassFormatted ' + carsBlockClassFormatted);
async function init_me() {
if (window.d2Env) {
tail = window.d2Env.userId;
fctx = window.d2Env.formContext['.FCTX'];
} else {
alert('обновите версию скрипта!');
return;
}
}
// Функция для нажатия кнопки загрузки страниц и очистки элементов
async function skipPages() {
let pagesToSkip = parseInt(pagesInput.value, 10);
if (isNaN(pagesToSkip) || pagesToSkip <= 0) {
alert('Введите корректное число страниц');
return;
}
for (let i = 0; i < pagesToSkip; i++) {
const loadMoreButton = document.querySelector('button.x-box-more');
if (loadMoreButton) {
await clearGrid();
loadMoreButton.click();
console.log(`Нажатие ${i + 1} на кнопку "Показать ещё"`);
await new Promise(resolve => setTimeout(resolve, 3000)); // Ждём 3 секунды
stats.pages++;
updateStats();
await clearEmptyBlocks();
} else {
alert('Кнопка "Показать ещё" не найдена, остановка.');
break;
}
}
}
// Функция очистки блока .o-grid.o-grid--2.o-grid--equal
async function clearGrid() {
const grids = document.querySelectorAll(carsBlockClassFormatted);
if (grids.length > 0) {
let blocks;
for (const grid of grids) {
blocks = grid.querySelectorAll('.' + carBlockClass);
if (blocks.length) {
stats.processedBlocks += blocks.length;
blocks.forEach(car => car.remove());
}
}
console.log('Удалены авто из пропущенных страниц.');
} else {
console.log('Не найдено блоков для очистки.');
}
}
skipButton.addEventListener('click', skipPages);
const updateStats = () => {
statsDiv.innerHTML = `<div>
Страниц пройдено: ${stats.pages}<br>
Иконок авто в очереди: ${stats.totalBlocks}<br>
Обработано: ${stats.processedBlocks}<br>
Отписались автоматом: ${stats.unsubscribedBlocks}<br>
Подписан на старые авто: ${stats.oldSubscribedBlocks}
</div>`;
statsDiv.appendChild(unsubscribeCheckboxDiv);
statsDiv.appendChild(hideOldCarsCheckboxDiv);
statsDiv.appendChild(checkButton);
statsDiv.appendChild(skipPagesDiv);
localStorage.setItem(localStorageKey, stats.pages);
};
// Функция для поиска и клика по кнопке "Загрузить еще"
const clickMoreButton = async () => {
const button = document.querySelector('button.x-box-more');
if (button) {
console.error('Нашли кнопку дальнейшей загрузки');
stats.pages++;
console.log('Загружаем страницу ' + stats.pages);
button.click();
updateStats();
await clearEmptyBlocks();
await new Promise(resolve => setTimeout(resolve, 3000));
console.log('Загрузили блоки с авто, приступаем к их обработке');
processBlocks();
} else {
alert('Кнопка не найдена, остановка процесса');
}
};
async function clearEmptyBlocks()
{
document.querySelectorAll(carsBlockClassFormatted).forEach(grid => {if (!grid.querySelector('div')) { grid.remove(); }});
}
async function loadMyCars() {
if (!localStorage.getItem(MY_CARS_KEY)) {
const response = await fetch('/my/r/');
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const cars = [];
doc.querySelectorAll('.c-car-draglist__item .c-car-card__caption a').forEach(car => {
if (!car.classList.contains('x-secondary-color')) {
const id = car.href.match(/\/(\d+)\//)[1];
cars.push({
id,
name: car.textContent.trim()
});
}
});
localStorage.setItem(MY_CARS_KEY, JSON.stringify(cars));
console.log('обновил кеш моих авто', cars);
} else {
console.log('кеш моих авто уже загружен', myCars);
}
}
function addCloseButton(element) {
if (!element) return;
const closeButton = document.createElement('button');
closeButton.innerHTML = '×'; // Символ "×" (крестик)
closeButton.style.position = 'absolute';
closeButton.style.top = '5px';
closeButton.style.right = '5px';
closeButton.style.background = 'red';
closeButton.style.color = 'white';
closeButton.style.border = 'none';
closeButton.style.padding = '5px 10px';
closeButton.style.cursor = 'pointer';
closeButton.style.fontSize = '16px';
closeButton.style.borderRadius = '50%';
closeButton.addEventListener('click', function () {
this.parentNode.remove(); // Удаляет родительский элемент кнопки
});
if (window.getComputedStyle(element).position === 'static') {
element.style.position = 'relative';
}
element.appendChild(closeButton);
element.classList.remove(carBlockClass);
element.querySelector('a.u-link-area').remove();
element.style.position = 'sticky';
}
const unsubscribeCar = async (id) => {
const url = '/ajax/subscription';
const data = {
_: 'unsubscribe',
type: 'car',
id: id,
'.FCTX': fctx
};
try {
const response = await fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: new URLSearchParams(data).toString()
});
if (response.ok) {
const result = await response.json();
console.log('Ответ unsubscribeCar:', result);
return result;
} else {
console.error('Ошибка запроса:', response.status);
}
} catch (error) {
console.error('Ошибка выполнения POST-запроса:', error);
}
};
const followUser = async (id) => {
const url = '/ajax/subscription';
const data = {
_: 'subscribe',
type: 'user',
id: id,
'.FCTX': fctx
};
try {
const response = await fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: new URLSearchParams(data).toString()
});
if (response.ok) {
const result = await response.json();
console.log('Ответ subscribeCar:', result);
return result;
} else {
console.error('Ошибка запроса:', response.status);
}
} catch (error) {
console.error('Ошибка выполнения POST-запроса:', error);
}
};
const unfollowUser = async (id) => {
const url = '/ajax/subscription';
const data = {
_: 'unsubscribe',
type: 'user',
id: id,
'.FCTX': fctx
};
try {
const response = await fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: new URLSearchParams(data).toString()
});
if (response.ok) {
const result = await response.json();
console.log('Ответ subscribeCar:', result);
return result;
} else {
console.error('Ошибка запроса:', response.status);
}
} catch (error) {
console.error('Ошибка выполнения POST-запроса:', error);
}
};
const subscribeCar = async (id) => {
const url = '/ajax/subscription';
const data = {
_: 'subscribe',
type: 'car',
id: id,
'.FCTX': fctx
};
try {
const response = await fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: new URLSearchParams(data).toString()
});
if (response.ok) {
const result = await response.json();
console.log('Ответ subscribeCar:', result);
return result;
} else {
console.error('Ошибка запроса:', response.status);
}
} catch (error) {
console.error('Ошибка выполнения POST-запроса:', error);
}
};
async function loadUser(userId) {
const response = await fetch(`/_api/hovercards/${userId}?tail=${tail}`);
return await response.json();
}
const processBlocks = async () => {
const blocks = document.querySelectorAll('.' + carBlockClass);
console.error('На странице найдено ' + blocks.length + ' авто');
updateStats();
let myCarNames = '';
for (const block of blocks) {
const titleElement = block.querySelector('.c-car-title');
stats.totalBlocks = document.querySelectorAll('.' + carBlockClass).length;
// Если чекбокс скрытия старых авто включен и блок старый – пропускаем или удаляем
if (titleElement.classList.contains('x-secondary-color')) {
stats.oldSubscribedBlocks++;
stats.processedBlocks++;
if (hideOldCarsCheckbox.checked) {
block.remove();
stats.removedBlocks++;
console.error('Старое авто, удаляем');
} else {
console.error('Старое авто, оставляем на странице');
addCloseButton(block);
}
updateStats();
continue;
}
const subscribeButton = block.querySelector('subscribe-button');
const userId = block.querySelector('a.c-username')?.getAttribute('data-ihc-token');
if (!userId) {
console.error('Не найден userId, пропускаем');
continue
};
// Делаем GET-запрос для проверки подписки
const data = await loadUser(userId);
const followedCarIds = data?.subscriptions?.followedCars?.map(car => {
const match = car.url.match(/\/(\d+)\//);
return match ? match[1] : null;
}).filter(Boolean);
const myCarIds = myCars ? myCars.map(car => car.id) : [];
if (myCarIds.length == 0) {
console.error('У текущего юзера не найдены авто');
return;
}
const hasMyCar = followedCarIds ? followedCarIds.some(carId => myCarIds.includes(carId)) : false;
if (hasMyCar) {
myCarNames = myCars
.filter(car => followedCarIds.includes(car.id))
.map(car => car.name)
.join(', ');
console.log(`Юзер номер ${userId} подписан на (${myCarNames}), пропускаем.`);
} else {
let uid = subscribeButton.getAttribute('uid');
console.log('Юрер номер ' + userId + ' не подписан на мои авто.');
if (unsubscribeCheckbox.checked) {
unsubscribeCar(uid);
stats.unsubscribedBlocks++;
console.error('Отписываемся от авто с номером ' + uid);
}
}
// Удаляем блок, если он не содержит мою машину
block.remove();
stats.removedBlocks++;
stats.processedBlocks++;
updateStats();
// Ждём 1 секунду перед обработкой следующего блока
await new Promise(resolve => setTimeout(resolve, 2000));
}
console.error('Обработали все авто');
clickMoreButton();
};
function isCarsFollowingPage()
{
return (/^\/users\/(.*)\/carsfollowing/).test(window.location.pathname);
}
function isSomeCarFollowingPage()
{
return (/^\/r\/(.*)\/followers/).test(window.location.pathname);
}
function addSubscribeButton() {
const counterElement = document.querySelector('.x-title-header .x-title .c-counter');
if (counterElement) {
const button = document.createElement('button');
button.textContent = 'Подписаться на эти авто';
button.classList.add('c-button');
button.classList.add('c-button--primary');
button.style.marginLeft = '10px';
button.addEventListener('click', function () {
scrapeUsers();
});
counterElement.after(button);
}
}
async function scrapeUsers() {
let users = new Set();
let usersDone = 0;
let pagesProcessed = 0;
let subscribeToCar = 0;
let lostUsers = 0;
let lostSubscribe = 0;
let infoBox = document.createElement('div');
infoBox.style.position = 'fixed';
infoBox.style.top = '10px';
infoBox.style.right = '10px';
infoBox.style.background = 'rgb(51, 51, 51)';
infoBox.style.color = 'white';
infoBox.style.padding = '10px';
infoBox.style.borderRadius = '5px';
infoBox.style.zIndex = '1000';
document.body.appendChild(infoBox);
function updateInfoBox() {
let progress = users.size > 0 ? Math.round((usersDone / users.size) * 100) : 0;
infoBox.innerHTML = `Страниц обработано: ${pagesProcessed}
<br> Пользователей собрано: ${users.size}
<br> Пользователей обработано: ${usersDone}
<br> Пользователей пропущено: ${lostUsers}<br> (посещали сайт > чем месяц назад или нет актуальных авто)
<br> Подписано на авто: ${subscribeToCar}<br> (даже если уже был на авто подписан)
<br> Не удалось подписаться: ${lostSubscribe}
<br>
<div style='width: 100%; background: #555; height: 10px; border-radius: 5px; margin-top: 5px;'>
<div style='width: ${progress}%; background: #4caf50; height: 10px; border-radius: 5px;'></div>
</div>`;
}
async function processUsers(userList) {
for (let userId of userList) {
try {
usersDone++;
await new Promise(resolve => setTimeout(resolve, 1000));
const data = await loadUser(userId);
if (data?.lastVisit && shouldSkipUser(data.lastVisit)) {
console.log(`Пропускаем пользователя ${data.nickname} (${userId}) из-за даты последнего визита: ${data.lastVisit}`);
lostUsers++;
continue;
}
if (data?.cars) {
for (let car of data.cars) {
if (car.belongState === "My") {
let carId = parseCarId(car.url);
if (carId) {
await new Promise(resolve => setTimeout(resolve, 1000));
let result = await subscribeCar(carId);
if (result?.types.length) {
subscribeToCar++;
} else {
lostSubscribe++;
}
updateInfoBox();
console.log(`Подписка на авто: ${car.fullCaption}`);
}
}
}
} else {
lostUsers++;
console.log('Не найдено авто у ' + userId);
}
if (data?.isFollowable) {
if (data?.isFollowed === false && data?.subscriptions?.followsMe === true) {
await new Promise(resolve => setTimeout(resolve, 1000));
followUser(userId);
}
if (data?.isFollowed === true && data?.subscriptions?.followsMe === false) {
await new Promise(resolve => setTimeout(resolve, 1000));
unfollowUser(userId);
}
}
} catch (error) {
console.error(`Ошибка загрузки данных пользователя ${userId}:`, error);
}
}
console.log('Обработка всех пользователей завершена.');
}
function shouldSkipUser(lastVisit) {
if (!lastVisit) return true;
if (lastVisit.includes("Сейчас онлайн")) return false;
if (lastVisit.includes("Был больше года назад")) return true;
let monthMatch = lastVisit.match(/Был (\d+) (month|месяц)/);
if (monthMatch) {
let months = parseInt(monthMatch[1], 10);
return months > 1;
}
return false;
}
function parseCarId(carUrl) {
let match = carUrl.match(/\/(\d+)\/?$/);
return match ? match[1] : null;
}
async function processPage() {
let mainContainer = document.querySelector('.l-page-main.g-column-mid');
if (!mainContainer) {
console.log('Основной контейнер не найден');
return;
}
let boxes = mainContainer.querySelectorAll('.x-box.o-f');
boxes.forEach(box => {
let userDivs = box.querySelectorAll('div > div');
userDivs.forEach(div => {
let userLink = div.querySelector('a.c-username');
if (userLink) {
let token = userLink.getAttribute('data-ihc-token');
if (token) {
users.add(token);
}
}
div.remove();
});
if (box.children.length === 0) {
console.log('Удаляем пустой контейнер');
box.remove();
}
});
updateInfoBox();
let loadMoreButton = document.querySelector('.x-box-more');
if (loadMoreButton) {
loadMoreButton.click();
pagesProcessed++;
updateInfoBox();
await new Promise(resolve => setTimeout(resolve, 2000));
processPage();
} else {
console.log(`Сбор завершен. Всего пользователей собрано: ${users.size}`);
processUsers(Array.from(users));
}
}
processPage();
}
checkButton.addEventListener('click', processBlocks);
loadMyCars();
init_me();
if (isCarsFollowingPage()) {
document.body.appendChild(statsDiv);
updateStats();
}
if (isSomeCarFollowingPage()) {
addSubscribeButton();
}
document.querySelector('.l-dv__i')?.remove();
document.querySelector('.c-dv-side.o-row.o-sticky.o-f')?.remove();