Перейти к содержимому
Zone of Games Forum
0wn3df1x

Скрипты Оунедфикса

Рекомендованные сообщения

Ultimate Steam Enhancer || Tamper Monkey

Описание и ссылки перенесены в блог, посвящённый скрипту.

 

 

Изменено пользователем 0wn3df1x
  • Лайк (+1) 1
  • Спасибо (+1) 2
  • +1 1

Поделиться сообщением


Ссылка на сообщение

SteamDB - Sales; Ultimate Enhancer || Tamper Monkey

Описание и ссылки перенесены в блог, посвящённый скрипту.

 

Изменено пользователем 0wn3df1x
  • Лайк (+1) 1

Поделиться сообщением


Ссылка на сообщение

Важная информация для пользователей Google Chrome (версии 138 и новее)

Если вы столкнулись с тем, что скрипты перестали работать (особенно после переустановки/обновления браузера или расширения Tampermonkey), пожалуйста, ознакомьтесь с информацией ниже.

Подробное разъяснение проблемы

Начиная с версии 138, Google внедрила новый, более строгий механизм безопасности для браузера Chrome. Ранее для работы пользовательских скриптов достаточно было включить общий "Режим разработчика" в настройках расширений.

Теперь эта логика изменена. Google считает глобальный переключатель небезопасным и заменила его на индивидуальное разрешение для каждого расширения. Для всех новых установок расширений, способных выполнять скрипты (как Tampermonkey), это разрешение по умолчанию выключено.

Именно поэтому после чистой установки или переустановки Tampermonkey скрипты могут не работать — браузер просто не дает расширению права на выполнение кода.

Пошаговая инструкция по исправлению

Как включить разрешение:

  1. Откройте страницу расширений (три вертикальные точки в правом верхнем углу экрана) > Расширения > Управления расширениями. Самый простой способ — вставить в адресную строку chrome://extensions и нажать Enter.
  2. Найдите в списке установленных расширений Tampermonkey и нажмите на кнопку "Сведения".
  3. На открывшейся странице вы увидите список всех настроек и разрешений для Tampermonkey. Прокрутите вниз и найдите переключатель с названием "Разрешить пользовательские скрипты" (Allow User Scripts).
  4. Активируйте этот переключатель.

Поделиться сообщением


Ссылка на сообщение

SteamDB - Sales; Улучшатор || Tamper Monkey — функционал перемещён в SteamDB - Sales; Ultimate Enhancer.

Скрытый текст

 

Обновление: Объединение и улучшение скриптов для раздела Sales на SteamDB. || TamperMonkey

Скрипты “SteamDB - Sales; Фильтр Недоступных” и “Steam DB — Sales; конвертация тенге в рубли” под TamperMonkey были объединены и улучшены:

  1. В рамках “фильтра недоступных” было добавлено поле с календарём для выборы даты в формате “день.месяц.год” и кнопка “отсеять по дате”. В чём польза этой функции? Периодически в Steam появляются новые скидки, но функционал SteamDB не позволяет выставить дату появления скидок. А значит, если вы отфильтруете таблицу по размеру скидок, то увидите не только новые, но и кучу старых. Данная фунция это исправляет и позволяет оставить только скидки, появившиеся после заданной даты.
  2. В рамках “конвертации” произошло расширение списка валют. Теперь конвертировать можно не только тенге. но и любую представленную на SteamDB валюту. Требуется только вбить курс валюты по отношению к вашей и нажать “конвертировать”. 
  3. Теперь оба скрипта объединены в “SteamDB - Sales; Улучшатор”.

Предназначение скрипта: На SteamDB есть раздел Sales, который позволяет посмотреть и отфильтровать актуальные скидки. Если кто-то смотрит цены для Казахстана или США, то ему необходимо каждый раз в голове конвертировать их в свою валюту. Данный скрипт позволяет задать курс и конвертировать любую валюту в другую валюту. Также данный скрипт позволяет увидеть, какие игры на распродаже недоступны в каком-то регионе. Ещё он позволяет увидеть скидки, появившиеся после заданной даты.

gkujek0.png
 

Скрытый текст


// ==UserScript==
// @name         SteamDB - Sales; Улучшатор
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Добавляет списки для фильтрации, фильтр по дате начала скидки и конвертацию валют
// @author       0wn3df1x
// @match        https://steamdb.info/sales/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Создание кнопки
    function createButton(text, onClick) {
        const button = document.createElement('button');
        button.textContent = text;
        button.style.margin = '5px';
        button.addEventListener('click', onClick);
        button.classList.add('steam-button'); // Добавляем класс для стилизации
        return button;
    }

    // Сбор всех ID
    function getAppIds() {
        const appIds = [];
        const rows = document.querySelectorAll('tr.app');
        rows.forEach(row => {
            const appId = row.getAttribute('data-appid');
            if (appId) {
                appIds.push(appId);
            }
        });
        return appIds;
    }

    // Функция для скрытия строк, которые есть в обоих списках
    function filterTable() {
        const list1 = JSON.parse(localStorage.getItem('list1') || '[]');
        const list2 = JSON.parse(localStorage.getItem('list2') || '[]');
        const commonAppIds = list1.filter(id => list2.includes(id));

        const rows = document.querySelectorAll('tr.app');
        rows.forEach(row => {
            const appId = row.getAttribute('data-appid');
            if (commonAppIds.includes(appId)) {
                row.style.display = 'none';
            } else {
                row.style.display = '';
            }
        });
    }

    // Функция для фильтрации по дате начала скидки
    function filterByDate() {
        const selectedDate = document.getElementById('datePicker').value;
        if (!selectedDate) {
            alert('Пожалуйста, выберите дату.');
            return;
        }

        const selectedTimestamp = new Date(selectedDate).getTime() / 1000;

        const rows = document.querySelectorAll('tr.app');
        rows.forEach(row => {
            const timeagoElements = row.querySelectorAll('td.timeago.dt-type-numeric');
            let startTimestamp;
            if (timeagoElements.length > 1) {
                startTimestamp = parseInt(timeagoElements[1].getAttribute('data-sort'), 10);
            } else if (timeagoElements.length === 1) {
                startTimestamp = parseInt(timeagoElements[0].getAttribute('data-sort'), 10);
            } else {
                startTimestamp = 0;
            }

            if (startTimestamp < selectedTimestamp) {
                row.style.display = 'none';
            } else {
                row.style.display = '';
            }
        });
    }

    // Стандартный курс валюты к выбранной валюте
    const defaultExchangeRate = 1;

    // Функция для конвертации цены из любой валюты в выбранную валюту
    function convertPrice(priceInSourceCurrency, exchangeRate) {
        const priceInTargetCurrency = priceInSourceCurrency * exchangeRate;
        return priceInTargetCurrency.toFixed(2); // Округляем до двух знаков после запятой
    }

    // Функция для добавления кнопки и текстового поля
    function addConvertButtonAndInput() {
        const headerSubtitle = document.querySelector('h2.header-subtitle');
        if (headerSubtitle) {
            const container = document.createElement('div');
            container.classList.add('convert-container');

            const input = document.createElement('input');
            input.type = 'text';
            input.value = defaultExchangeRate;
            input.classList.add('exchange-rate-input');

            const button = document.createElement('button');
            button.textContent = 'Конвертировать';
            button.classList.add('convert-button');
            button.addEventListener('click', () => {
                const exchangeRate = parseFloat(input.value);
                if (!isNaN(exchangeRate)) {
                    convertPrices(exchangeRate);
                }
            });

            container.appendChild(input);
            container.appendChild(button);
            headerSubtitle.parentNode.insertBefore(container, headerSubtitle.nextSibling);
        }
    }

    // Функция для конвертации цен
    function convertPrices(exchangeRate) {
        const appRows = document.querySelectorAll('tr.app');

        appRows.forEach(row => {
            const priceElements = row.querySelectorAll('td.dt-type-numeric');
            if (priceElements.length >= 3) {
                const priceElement = priceElements[2]; // Третий элемент (индексация с 0)
                const priceText = priceElement.textContent.trim();

                // Проверка на Peruvian sol
                if (priceText.includes('S/.')) {
                    const priceMatch = priceText.match(/S\/\.([0-9,.]+)/);
                    if (priceMatch) {
                        const priceInSourceCurrency = parseFloat(priceMatch[1].replace(',', '.'));
                        if (!isNaN(priceInSourceCurrency)) {
                            const priceInTargetCurrency = convertPrice(priceInSourceCurrency, exchangeRate);
                            priceElement.textContent = priceInTargetCurrency;
                        }
                    }
                } else {
                    const priceMatch = priceText.match(/([0-9,.]+)/); // Упрощенное регулярное выражение
                    if (priceMatch) {
                        const priceInSourceCurrency = parseFloat(priceMatch[1].replace(',', '.'));
                        if (!isNaN(priceInSourceCurrency)) {
                            const priceInTargetCurrency = convertPrice(priceInSourceCurrency, exchangeRate);
                            priceElement.textContent = priceInTargetCurrency;
                        }
                    }
                }
            }
        });
    }

    // Добавление кнопок и элементов управления
    const headerSubtitle = document.querySelector('h2.header-subtitle');
    if (headerSubtitle) {
        const list1Button = createButton('Список 1', () => {
            const appIds = getAppIds();
            localStorage.setItem('list1', JSON.stringify(appIds));
            alert('Список 1 сохранен');
        });
        headerSubtitle.appendChild(list1Button);

        const list2Button = createButton('Список 2', () => {
            const appIds = getAppIds();
            localStorage.setItem('list2', JSON.stringify(appIds));
            alert('Список 2 сохранен');
        });
        headerSubtitle.appendChild(list2Button);

        const filterButton = createButton('Фильтр', () => {
            filterTable();
            alert('Таблица отфильтрована');
        });
        headerSubtitle.appendChild(filterButton);

        const datePicker = document.createElement('input');
        datePicker.type = 'date';
        datePicker.id = 'datePicker';
        datePicker.style.margin = '5px';
        headerSubtitle.appendChild(datePicker);

        const filterByDateButton = createButton('Отсеять по дате', () => {
            filterByDate();
            alert('Таблица отфильтрована по дате');
        });
        headerSubtitle.appendChild(filterByDateButton);

        addConvertButtonAndInput();
    }

    // Добавление стилей CSS
    const style = document.createElement('style');
    style.textContent = `
        .steam-button {
            background-color: #171a21;
            color: #c7d5e0;
            border: 1px solid #616c77;
            padding: 5px 10px;
            border-radius: 3px;
            transition: background-color 0.3s, color 0.3s;
        }
        .steam-button:hover {
            background-color: #616c77;
            color: #ffffff;
        }
        .convert-container {
            display: flex;
            align-items: center;
        }
        .exchange-rate-input {
            margin-right: 10px;
            padding: 5px;
            border: 1px solid #4a698a;
            border-radius: 3px;
        }
        .convert-button {
            background-color: #17202f;
            color: #acdbf5;
            border: 1px solid #4a698a;
            padding: 5px 10px;
            border-radius: 3px;
            cursor: pointer;
            transition: background-color 0.3s, color 0.3s;
        }
        .convert-button:hover {
            background-color: #4a698a;
            color: #ffffff;
        }
    `;
    document.head.appendChild(style);
})();

 

Скрытый текст
  1. Заходим на страницу Sales на SteamDB.
  2. Проставляем нужную нам валюту и нужные нам фильтры, если необходимо.
  3. В выпадающем списке entries per page выбираем All (slow) и ждём, пока все предложения прогрузятся.
    siKdGDV.png
  4. Вбиваем нужный курс в текстовое поле и нажимаем “конвертировать”. Вуаля:
    YZ3ilwG.png

P.S. В поле const defaultExchangeRate = скрипта находится базовый курс. Можете заменить его на курс актуальный для вас валюты, чтобы при обновлении страницы видеть его.

Скрытый текст
  1. Заходим на страницу Sales на SteamDB.
  2. Проставляем валюту “рубли” (или любую другую) и нужные нам фильтры, если необходимо.
  3. В выпадающем списке entries per page выбираем All (slow) и ждём, пока все предложения прогрузятся.siKdGDV.png
  4. Жмём на кнопку “Список 1” и получаем уведомление, что список сохранён.
    UA0OQ9J.png
     
  5. Проставляем валюту. Снова в выпадающем списке entries per page выбираем All (slow) и ждём, пока все предложения прогрузятся. После чего жмём кнопку “Список 2”, чтобы сохранить все игры из казахского региона.
  6. После этого можем нажать кнопку фильтр. Сперва вам покажется, что всё чуть подвисло и съехало, но затем все позиции прогрузятся. Останутся только игры, которых нет в одном из списков.
    3xur1Ex.png
     
  7. Обновление (16.07.2024): Вы можете выбрать дату, нажав на символ календаря или просто вбив её вручную, а затем нажать “отсеять по дате”, чтобы вам были показаны игры, скидка на которые появилась после этой даты.
    (Важно, чтобы entries per page стояла на “All (slow)”
    kOp58sS.png

P.S. Скрипт прекрасно работает с конвертацией тенге, так что можно применять их одновременно.
P.S.2: Если после этого вы выберете рубли, снова проставите “All (Slow)” и снова нажмете фильтр — увидите игры, которые почему-то не показываются для Казахстана.

4utM6ip.png

Как вы можете понять из обеих инструкций, главное в выпадающем списке entries per page выбираем All (slow).
Всё универсально и доступно.

 

 

Изменено пользователем 0wn3df1x
  • Лайк (+1) 1

Поделиться сообщением


Ссылка на сообщение

Обновление: Улучшение парсера игр с сайта GamesForFarm. || TamperMonkey

Когда вы запустите скрипт впервые — он сохранит данные о текущем наборе игр в LocalStorage.
При следующем запуске скрипта вы получите две таблицы вместо одной.

  • В первой таблице, как и раньше, будут перечислены все игры, их наличие в вашем списке желаемого и на вашем аккаунте.
  • Во второй таблице будут указаны только игры, которые были добавлены, удалены и цены на которые изменились:
     
Steam ID Название игры Цена Размер скидки rgWishlist rgOwnedApps Адрес картинки Инфо
3154130 Space Memory: Animals 1,99 -94%     https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/3154130/ Цена изменилась с 4,99 на 1,99
647830 LEGO® Marvel Super Heroes 2 229,9 -88%     https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/647830/ Удалили



Предназначение скрипта:
Сайт GamesForFarm ориентирован на людей, которые держат фермы в Steam, т.е. закупают оптом дешёвые игры, чтобы выбивать на них карточки, продавать их и так далее. Соответственно, владельцы сайта ищут оптовые партии ключей на зарубежных площадках, а затем продают их в России с небольшой наценкой. Большая часть игр на этом сайте является откровенным мусором, но иногда можно дёшево урвать хорошие проекты (в основном они попадают на сайт из бандлов). Полезность скрипта в том, что он позволяет получить весь каталог игр сайта GamesForFarm в табличном виде. Также он позволяет узнать, есть ли в каталоге игры из вашего списка желаемого и пометить игры, которые у вас уже есть. 

Спойлер

// ==UserScript==
// @name         GamesForFarm - Парсер
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Получает все игры в GamesForFarm и ищет имеющиеся и желаемые
// @author       0wn3df1x
// @match        https://gamesforfarm.com*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Добавляем стили
    const style = document.createElement('style');
    style.textContent = `
        .load-button {
            background-color: #171a21;
            color: #acb2b8;
            border: 1px solid #171a21;
            padding: 10px 20px;
            font-size: 16px;
            cursor: pointer;
            transition: background-color 0.3s, color 0.3s;
            margin-left: 10px;
            margin-top: 10px;
        }
        .load-button:hover {
            background-color: #66c0f4;
            color: #171a21;
        }
        .json-input {
            background-color: #171a21;
            color: #acb2b8;
            border: 1px solid #171a21;
            padding: 10px;
            font-size: 14px;
            width: 300px;
            height: 100px;
            margin-left: 10px;
            margin-top: 10px;
            resize: none;
        }
        .data-link {
            margin-left: 10px;
            margin-top: 10px;
            color: #66c0f4;
            text-decoration: none;
        }
        .data-link:hover {
            text-decoration: underline;
        }
    `;
    document.head.appendChild(style);

    // Добавляем кнопку "Загрузить таблицу" и текстовое поле под <div class="main__title">Каталог</div>
    const mainTitle = document.querySelector('.main__title');
    if (mainTitle) {
        const loadButton = document.createElement('button');
        loadButton.textContent = 'Загрузить таблицу';
        loadButton.className = 'load-button';

        const jsonInput = document.createElement('textarea');
        jsonInput.className = 'json-input';
        jsonInput.placeholder = 'Вставьте JSON сюда';

        const dataLink = document.createElement('a');
        dataLink.href = 'https://store.steampowered.com/dynamicstore/userdata/';
        dataLink.target = '_blank'; // Открывает ссылку в новой вкладке
        dataLink.textContent = 'Получить данные своей UserData в Steam';
        dataLink.className = 'data-link';

        mainTitle.parentNode.insertBefore(loadButton, mainTitle.nextSibling);
        mainTitle.parentNode.insertBefore(jsonInput, loadButton.nextSibling);
        mainTitle.parentNode.insertBefore(dataLink, jsonInput.nextSibling);

        // Обработчик нажатия на кнопку "Загрузить таблицу"
        loadButton.addEventListener('click', function() {
            let jsonData = {};
            try {
                if (jsonInput.value.trim() !== '') {
                    jsonData = JSON.parse(jsonInput.value);
                }
            } catch (e) {
                alert('Неверный формат JSON');
                return;
            }

            const rgWishlist = new Set(jsonData.rgWishlist || []);
            const rgOwnedApps = new Set(jsonData.rgOwnedApps || []);

            // Находим кнопку и нажимаем на неё
            const button = document.querySelector('.btn-reset.product__more-button.fn_product_more.ddd3');
            if (button) {
                button.click();
            }

            // Ждём 5 секунд (увеличим время ожидания)
            setTimeout(() => {
                // Создаем массив для хранения данных всех игр
                const gamesData = [];

                // Находим все элементы с классом product__item внутри элемента с id gamesCatalog
                const productItems = document.querySelectorAll('#gamesCatalog .product__item');

                productItems.forEach(item => {
                    // Извлекаем Steam ID
                    const steamLink = item.querySelector('a[href*="store.steampowered.com/app/"]');
                    let steamId = '-';
                    if (steamLink) {
                        const href = steamLink.getAttribute('href');
                        const match = href.match(/\/app\/(\d+)\//);
                        if (match && match[1]) {
                            steamId = match[1];
                        }
                    }

                    // Пропускаем элемент, если нет Steam ID
                    if (steamId === '-') {
                        return;
                    }

                    // Извлекаем цену
                    const priceElement = item.querySelector('.product__box-price');
                    let price = '-';
                    if (priceElement) {
                        price = priceElement.textContent.trim();
                        price = formatPrice(price);
                    }

                    // Извлекаем адрес картинки
                    const imageElement = item.querySelector('.product__box-image img');
                    let imageUrl = '-';
                    if (imageElement) {
                        imageUrl = imageElement.getAttribute('data-src');
                    }

                    // Извлекаем название игры
                    const titleElement = item.querySelector('.product__box-title a');
                    let gameTitle = '-';
                    if (titleElement) {
                        gameTitle = titleElement.textContent.trim();
                    }

                    // Извлекаем размер скидки
                    const discountElement = item.querySelector('.product__box-prop.prop—discount');
                    let discount = '-';
                    if (discountElement) {
                        discount = discountElement.textContent.trim();
                    }

                    // Проверяем наличие Steam ID в списках
                    const inWishlist = rgWishlist.has(parseInt(steamId)) ? '[VVV]' : '';
                    const inOwnedApps = rgOwnedApps.has(parseInt(steamId)) ? '[VVV]' : '';

                    // Добавляем данные в массив
                    gamesData.push({
                        steamId,
                        price,
                        imageUrl,
                        gameTitle,
                        discount,
                        inWishlist,
                        inOwnedApps
                    });
                });

                // Сортируем массив сначала по наличию в rgWishlist, затем по цене
                gamesData.sort((a, b) => {
                    if (a.inWishlist && !b.inWishlist) return -1;
                    if (!a.inWishlist && b.inWishlist) return 1;
                    return parseFloat(b.price.replace(',', '.')) - parseFloat(a.price.replace(',', '.'));
                });

                // Создаем новое окно для вывода результатов
                const newWindow = window.open("", "_blank", "width=600,height=400");
                newWindow.document.write('<html><head><title>Результаты</title></head><body><table border="1" id="gamesTable"><tr><th>Steam ID</th><th>Название игры</th><th>Цена</th><th>Размер скидки</th>' + (jsonData.rgWishlist ? '<th>rgWishlist</th>' : '') + (jsonData.rgOwnedApps ? '<th>rgOwnedApps</th>' : '') + '<th>Адрес картинки</th></tr>');

                gamesData.forEach(game => {
                    newWindow.document.write(`<tr><td>${game.steamId}</td><td>${game.gameTitle}</td><td>${game.price}</td><td>${game.discount}</td>${jsonData.rgWishlist ? `<td>${game.inWishlist}</td>` : ''}${jsonData.rgOwnedApps ? `<td>${game.inOwnedApps}</td>` : ''}<td>${game.imageUrl}</td></tr>`);
                });

                newWindow.document.write('</table></body></html>');
                newWindow.document.close();

                // Добавляем функциональность сортировки таблицы по клику на заголовки
                const table = newWindow.document.getElementById('gamesTable');
                if (table) {
                    const headers = table.getElementsByTagName('th');
                    for (let i = 0; i < headers.length; i++) {
                        headers[i].addEventListener('click', () => {
                            sortTable(table, i);
                        });
                    }
                }
            }, 5000); // Ожидание 5 секунд
        });
    }

    // Функция для форматирования цены
    function formatPrice(price) {
        return price.replace('.', ',').replace('₽', '').trim();
    }

    // Функция для сортировки таблицы
    function sortTable(table, columnIndex) {
        const rows = Array.from(table.rows).slice(1);
        const isNumeric = columnIndex === 0 || columnIndex === 2; // Steam ID и Цена

        rows.sort((a, b) => {
            const aValue = a.cells[columnIndex].textContent;
            const bValue = b.cells[columnIndex].textContent;

            if (isNumeric) {
                return parseFloat(aValue.replace(',', '.')) - parseFloat(bValue.replace(',', '.'));
            } else {
                return aValue.localeCompare(bValue);
            }
        });

        // Определяем направление сортировки
        const header = table.rows[0].cells[columnIndex];
        const sortDirection = header.getAttribute('data-sort') || 'asc';
        if (sortDirection === 'asc') {
            rows.reverse();
            header.setAttribute('data-sort', 'desc');
        } else {
            header.setAttribute('data-sort', 'asc');
        }

        // Очищаем таблицу и добавляем отсортированные строки
        while (table.rows.length > 1) {
            table.deleteRow(1);
        }

        rows.forEach(row => table.appendChild(row));
    }
})();
Спойлер

0. Если вы хотите узнать, какие игры, продающиеся на сайте, есть у вас в списке желаемого и на аккаунте, то читайте пункты 0-4, если вы хотите просто получить таблицу имеющихся на сайте игры, то можете перейти к пункту 5.

Итак. Что касается списка имеющихся игр и списка желаемого — они содержатся в юзердате. Если вы не пользуетесь Steam в браузере и не вошли в аккаунт со своего браузера — надо войти, т.к. Userdata пользователя доступна только ему самому. После установки скрипта вы увидите на странице магазина три новых элемента:
sS8iaNW.png

1. Щёлкнув по первому элементу — вы окажетесь на странице со своей Userdata
Там хранится информация о вашем списке желаемого, имеющихся играх и тех играх, которые вы решили скрыть в магазине. Также там есть информация о рекомендуемых жанрах и кураторах. Но ничего конфиденциального — если есть желание, можете проверить.
wP5cmnD.png

2. Нужно скопировать данные своей Userdata и вставить в текстовое окно.
fuv8fee.png

3. После чего щёлкнуть “Загрузить таблицу”. Через 5 секунд откроется новое окно, в котором вы увидите:
50NfDjF.png

В самом верху списка идут игры, которые есть у вас в списке желаемого. Упорядочены по убыванию цены. Затем идут все остальные игры аналогично по убыванию цены. Можно щёлкать по заголовкам, чтобы сортировать данные.

Когда вы запустите скрипт больше двух раз, то у вас откроются ещё одна таблица, в которой будут содержаться данные только об играх, которые добавились, были удалены или цена на которые изменилась:

Steam ID Название игры Цена Размер скидки rgWishlist rgOwnedApps Адрес картинки Инфо
3154130 Space Memory: Animals 1,99 -94%     https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/3154130/header.jpg?t=1724396439 Цена изменилась с 4,99 на 1,99
647830 LEGO® Marvel Super Heroes 2 229,9 -88%     https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/647830/header.jpg?t=1635187803 Удалили


4. Вы можете скопировать данные и вставить их на Google-таблицу. После чего можете использовать формулу Image, чтобы отобразить картинки, будет выглядеть так:
GNJ4KRK.png

5. Если вы не вставите Json и просто нажмёте кнопку “Загрузить таблицу”, то таблица всё равно откроется в новом окне, просто в ней не будет информации об играх, которые есть у вас на аккаунте и в списке желаемого. Таблицу также можно будет упорядочить, щёлкая по заголовкам и аналогично можно будет скопировать в Google-таблицы для дальнейшей работы.

Изменено пользователем 0wn3df1x
  • Лайк (+1) 1
  • Спасибо (+1) 1

Поделиться сообщением


Ссылка на сообщение

Скрипт для улучшения поиска на Plati market || Tamper Monkey — функционал перемещён в Plati.Market; Ultimate Enhancer.

Скрытый текст

 

Сейчас, когда вы вбиваете название какой-то игры в поиске на плати, — вам хаотично выдаёт товары с разной ценой на нескольких страницах. Данный скрипт призван это исправить.

При использовании скрипта на первой странице результатов поиска появится кнопка “отсортировать”.

При нажатии скрипт пройдёт по всем страницам и сохранит данные в локальное хранилище, после чего восстановит их на последней странице и отсортирует по возрастанию цены.

Демонстрация:


UPD: Добавил кнопку “Скрыть туфту”, которая появляется после сортировки.
Данную кнопку можно персонализировать в этом фрагменте кода:


    // Функция для скрытия аккаунтов и других элементов
    function hideAccounts() {
        const keywords = ['аккаунт', 'офлайн', 'оффлайн', 'xbox', 'egs', 'PS5', 'PS4', 'Epic Games'];

Можете убирать или добавлять ключевые слова. При нажатии кнопки элементы, в которых были эти ключевые слова, исчезнут. 

UPD 2: Также в качестве keyword можно указать название продавца в магазине, если вы знаете, что он продаёт то, что вас не интересует, или он просто вам не нравится (обновил скрипт ещё раз).

 

Изменено пользователем 0wn3df1x
  • Лайк (+1) 1
  • Спасибо (+1) 2

Поделиться сообщением


Ссылка на сообщение

SteamMarket — Казначей || Tamper Monkey — функционал перемещён в Ultimate Steam Enhancer.

Скрытый текст

Данный скрипт предназначен для торговой площадки Steam и добавляет информацию о продажах предмета.
Предназначен для тех, кто любит считать чужие деньги и офигевать с сумм с продажи пикселей.

Скрытый текст


// ==UserScript==
// @name         SteamMarket — Казначей
// @namespace    https://steamcommunity.com/market/
// @version      1.0
// @description  Выводит информацию о продажах предмета на торговой площадке Steam
// @author       0wn3df1x
// @match        https://steamcommunity.com/market/listings/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Функция для получения информации о продажах
    async function fetchSalesInfo() {
        const urlParts = window.location.pathname.split('/');
        const appId = urlParts[3];
        const marketHashName = decodeURIComponent(urlParts[4]);
        const apiUrl = `https://steamcommunity.com/market/pricehistory/?appid=${appId}&market_hash_name=${marketHashName}`;

        try {
            const response = await fetch(apiUrl);
            const data = await response.json();

            if (data.success) {
                const salesData = data.prices;
                const yearlySales = {};
                let totalSales = 0;

                salesData.forEach(sale => {
                    const date = sale[0];
                    const price = parseFloat(sale[1]);
                    const quantity = parseInt(sale[2]);
                    const year = date.split(' ')[2];

                    const totalForDay = price * quantity;

                    if (!yearlySales[year]) {
                        yearlySales[year] = { total: 0, commission: 0, developerShare: 0, valveShare: 0 };
                    }

                    yearlySales[year].total += totalForDay;
                    totalSales += totalForDay;
                });

                // Рассчитываем комиссию и доли для каждого года
                for (const year in yearlySales) {
                    const commission = yearlySales[year].total * 0.13;
                    const developerShare = commission * 0.6667;
                    const valveShare = commission * 0.3333;

                    yearlySales[year].commission = commission;
                    yearlySales[year].developerShare = developerShare;
                    yearlySales[year].valveShare = valveShare;
                }

                displaySalesInfo(yearlySales, totalSales);
            } else {
                console.error('Не удалось получить информацию о продажах.');
            }
        } catch (error) {
            console.error('Ошибка при получении данных:', error);
        }
    }

    // Функция для отображения информации о продажах
    function displaySalesInfo(yearlySales, totalSales) {
        const salesInfoContainer = document.createElement('div');
        salesInfoContainer.style.marginTop = '20px';
        salesInfoContainer.style.padding = '10px';
        salesInfoContainer.style.border = '1px solid #4a4a4a';
        salesInfoContainer.style.backgroundColor = '#1b2838';
        salesInfoContainer.style.borderRadius = '4px';
        salesInfoContainer.style.boxShadow = '0 1px 3px rgba(0, 0, 0, 0.5)';
        salesInfoContainer.style.color = '#c7d5e0';

        const spoilerHeader = document.createElement('div');
        spoilerHeader.style.cursor = 'pointer';
        spoilerHeader.style.padding = '10px';
        spoilerHeader.style.backgroundColor = '#171a21';
        spoilerHeader.style.borderRadius = '4px 4px 0 0';
        spoilerHeader.style.color = '#c7d5e0';
        spoilerHeader.style.fontWeight = 'bold';
        spoilerHeader.style.fontFamily = '"Motiva Sans", sans-serif';
        spoilerHeader.style.fontSize = '16px';
        spoilerHeader.style.display = 'flex';
        spoilerHeader.style.alignItems = 'center';
        spoilerHeader.style.justifyContent = 'space-between';
        spoilerHeader.innerHTML = 'Информация о продажах <span style="font-size: 12px; transform: rotate(0deg); transition: transform 0.3s ease;">&#9660;</span>';

        spoilerHeader.addEventListener('click', () => {
            const content = spoilerHeader.nextElementSibling;
            content.style.display = content.style.display === 'none' ? 'block' : 'none';
            const arrow = spoilerHeader.querySelector('span');
            arrow.style.transform = content.style.display === 'none' ? 'rotate(0deg)' : 'rotate(180deg)';
        });

        const spoilerContent = document.createElement('div');
        spoilerContent.style.display = 'none';
        spoilerContent.style.padding = '10px';
        spoilerContent.style.borderTop = '1px solid #4a4a4a';

        const yearlySalesTable = document.createElement('table');
        yearlySalesTable.style.width = '100%';
        yearlySalesTable.style.borderCollapse = 'collapse';
        yearlySalesTable.style.marginBottom = '20px';
        yearlySalesTable.style.fontFamily = '"Motiva Sans", sans-serif';
        yearlySalesTable.style.fontSize = '14px';

        const yearlySalesHeader = document.createElement('tr');
        yearlySalesHeader.innerHTML = '<th style="padding: 8px; text-align: left; border-bottom: 2px solid #4a4a4a; background-color: #171a21; color: #c7d5e0;">Год</th><th style="padding: 8px; text-align: left; border-bottom: 2px solid #4a4a4a; background-color: #171a21; color: #c7d5e0;">Сумма продаж за год</th><th style="padding: 8px; text-align: left; border-bottom: 2px solid #4a4a4a; background-color: #171a21; color: #c7d5e0;">Ушло разработчику</th><th style="padding: 8px; text-align: left; border-bottom: 2px solid #4a4a4a; background-color: #171a21; color: #c7d5e0;">Ушло Valve</th>';
        yearlySalesTable.appendChild(yearlySalesHeader);

        for (const year in yearlySales) {
            const row = document.createElement('tr');
            row.innerHTML = `<td style="padding: 8px; border-bottom: 1px solid #4a4a4a; background-color: #1b2838; color: #c7d5e0;">${year}</td><td style="padding: 8px; border-bottom: 1px solid #4a4a4a; background-color: #1b2838; color: #c7d5e0;">${yearlySales[year].total.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} руб.</td><td style="padding: 8px; border-bottom: 1px solid #4a4a4a; background-color: #1b2838; color: #c7d5e0;">${yearlySales[year].developerShare.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} руб.</td><td style="padding: 8px; border-bottom: 1px solid #4a4a4a; background-color: #1b2838; color: #c7d5e0;">${yearlySales[year].valveShare.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} руб.</td>`;
            yearlySalesTable.appendChild(row);
        }

        const totalSalesParagraph = document.createElement('p');
        totalSalesParagraph.textContent = `Сумма продаж за всё время: ${totalSales.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} руб.`;
        totalSalesParagraph.style.fontWeight = 'bold';
        totalSalesParagraph.style.fontSize = '16px';
        totalSalesParagraph.style.color = '#c7d5e0';
        totalSalesParagraph.style.fontFamily = '"Motiva Sans", sans-serif';

        const commission = totalSales * 0.13;
        const developerShare = commission * 0.6667;
        const valveShare = commission * 0.3333;

        const developerShareParagraph = document.createElement('p');
        developerShareParagraph.textContent = `Ушло разработчику: ${developerShare.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} руб.`;
        developerShareParagraph.style.fontSize = '14px';
        developerShareParagraph.style.color = '#c7d5e0';
        developerShareParagraph.style.fontFamily = '"Motiva Sans", sans-serif';

        const valveShareParagraph = document.createElement('p');
        valveShareParagraph.textContent = `Ушло Valve: ${valveShare.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} руб.`;
        valveShareParagraph.style.fontSize = '14px';
        valveShareParagraph.style.color = '#c7d5e0';
        valveShareParagraph.style.fontFamily = '"Motiva Sans", sans-serif';

        spoilerContent.appendChild(yearlySalesTable);
        spoilerContent.appendChild(totalSalesParagraph);
        spoilerContent.appendChild(developerShareParagraph);
        spoilerContent.appendChild(valveShareParagraph);

        salesInfoContainer.appendChild(spoilerHeader);
        salesInfoContainer.appendChild(spoilerContent);

        const marketHeaderBg = document.querySelector('.market_header_bg');
        if (marketHeaderBg) {
            marketHeaderBg.parentNode.insertBefore(salesInfoContainer, marketHeaderBg.nextSibling);
        }
    }

    // Вызываем функцию fetchSalesInfo через несколько секунд после загрузки страницы
    setTimeout(fetchSalesInfo, 100);
})();

 


Со скриптом на страницах предметов появится спойлер “Информация о продажах”:

QW7qsp1.png

При раскрытии спойлера показываются следующие данные:ZPnzyNH.png

 

Изменено пользователем 0wn3df1x
  • Лайк (+1) 1

Поделиться сообщением


Ссылка на сообщение

Скрипт для сбора игр с The Adventure Games - Point and Click Database. || Python

nl9MjCy.png


Предназначение скрипта:

Скрипт собирает базовую информацию об играх с базы данных адвенчур:

  • Ссылку на игру в Adventure Database.
  • Название игры.
  • Год выхода игры.
  • Ссылку на страницу игры в Steam (если указана).
  • Точную дату выхода игры (если указана).
    UPD: 14.10.2024
  • Описание игры
  • Изображение игры
  • Автор игры
  • Издатель игры
Скрытый текст

aBLrAVI.png

Скрытый текст

Инструкция:

  1. Если у вас нет python и для вас это что-то далёкое, то вот готовая сборка программы.
    Скачиваете и запускаете, после чего можете перейти к пункту 7.
     
  2. Допустим, третий python у вас есть.
  3. В консоли перейдите в папку, где будет лежать программа.
  4. В папке создайте файл requirements.txt и напишите в нём
    
    requests
    beautifulsoup4

     

  5. Пропишите в консоли
    
    pip install -r requirements.txt

     

  6. Создайте файл start.py и поместите в него основной код скрипта
    Скрытый текст
    
    
    import requests
    from bs4 import BeautifulSoup
    import csv
    from datetime import datetime
    import threading
    import tkinter as tk
    from tkinter import messagebox
    from concurrent.futures import ThreadPoolExecutor
    import re
    import json
    import html
    
    # Глобальные переменные для отслеживания прогресса
    total_pages = 0
    processed_pages = 0
    total_games = 0
    processed_games = 0
    
    def fetch_page(url):
        response = requests.get(url)
        response.raise_for_status()
        return response.text
    
    def parse_games_from_page(html):
        soup = BeautifulSoup(html, 'html.parser')
        games = []
        for card in soup.find_all('div', class_='card'):
            year_tag = card.find('span', class_='new_item')
            if year_tag:
                year = int(year_tag.text.strip())
                game_link = card.find('a')['href']
                game_name = card.find('h3').text.strip()
                games.append((year, game_link, game_name))
        return games
    
    def clean_json_string(json_str):
        # Удаляем недопустимые управляющие символы
        json_str = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', json_str)
        return json_str
    
    def parse_game_details(html_content):
        soup = BeautifulSoup(html_content, 'html.parser')
        
        # Извлечение данных из JSON-LD блоков
        json_ld_blocks = soup.find_all('script', type='application/ld+json')
        game_data = {}
        
        for block in json_ld_blocks:
            try:
                cleaned_json_str = clean_json_string(block.string)
                data = json.loads(cleaned_json_str)
                if data.get("@type") == "VideoGame":
                    if 'author' in data:
                        author = data['author']['name']
                        game_data['author'] = ', '.join(author) if isinstance(author, list) else author
                    if 'publisher' in data:
                        publisher = data['publisher']['name']
                        game_data['publisher'] = ', '.join(publisher) if isinstance(publisher, list) else publisher
                elif data.get("@type") == "Game":
                    game_data['description'] = data.get('description', '')
                    game_data['image'] = data.get('image', '')
            except json.JSONDecodeError as e:
                print(f"Error decoding JSON: {e}")
                continue
        
        # Преобразуем HTML-сущности в нормальный текст только в нужных полях
        game_data['description'] = html.unescape(game_data.get('description', ''))
        game_data['author'] = html.unescape(game_data.get('author', ''))
        game_data['publisher'] = html.unescape(game_data.get('publisher', ''))
        
        release_info = soup.find('div', class_='gray', style=lambda value: value and 'padding-left: 20px;' in value)
        
        release_date = None
        if release_info:
            release_text = release_info.find('span', class_='cat_label_item')
            if release_text:
                date_span = release_text.find('span')
                if date_span:
                    release_date_str = date_span.text.strip()
                    if re.match(r'Q\d \d{4}', release_date_str):
                        release_date = release_date_str
                    elif re.match(r'[A-Za-z]+ \d{4}', release_date_str):
                        release_date = release_date_str
                    else:
                        try:
                            release_date = datetime.strptime(release_date_str, '%B %d, %Y').strftime('%d.%m.%Y')
                        except ValueError:
                            pass
        
        # Ищем ссылку на Steam по наличию вложенного тега <span> с текстом "Steam Store"
        steam_link = soup.find('a', href=lambda value: value and 'store.steampowered.com' in value)
        if steam_link:
            steam_store_span = steam_link.find('span', string=re.compile(r'Steam Store', re.IGNORECASE))
            if steam_store_span:
                steam_url = steam_link['href']
            else:
                steam_url = None
        else:
            steam_url = None
    
        # Если ссылка не найдена, ищем внутри блока с классом `bodytext`
        if not steam_url:
            # Ищем все div с классом `store_box` на странице
            store_boxes = soup.find_all('div', class_='store_box')
            for store_box in store_boxes:
                # Ищем ссылку на Steam внутри каждого блока `store_box`
                steam_link = store_box.find('a', href=lambda value: value and 'store.steampowered.com' in value)
                
                if steam_link:
                    # Проверяем, не является ли ссылка на демо-версию (исключаем упоминания "DEMO")
                    if 'demo' not in steam_link.text.lower() and 'дема' not in steam_link.text.lower():
                        steam_url = steam_link['href']
                        break  # Прекращаем поиск, если нашли подходящую ссылку
    
        # Если ссылка все еще не найдена, возможно, она находится вне блока `store_box`, но всё равно не является демо
        if not steam_url:
            # Ищем все ссылки, содержащие "store.steampowered.com", но не относящиеся к демо-версии
            all_steam_links = soup.find_all('a', href=lambda value: value and 'store.steampowered.com' in value)
            for link in all_steam_links:
                # Исключаем ссылки на демо (например, по наличию слова "DEMO" или иконок, таких как "fas fa-floppy-disk")
                if 'demo' not in link.text.lower() and not link.find('i', class_='fas fa-floppy-disk'):
                    steam_url = link['href']
                    break  # Прекращаем поиск, если нашли нужную ссылку
        
        return {
            'release_date': release_date,
            'steam_url': steam_url,
            'description': game_data.get('description', ''),
            'image': game_data.get('image', ''),
            'author': game_data.get('author', ''),
            'publisher': game_data.get('publisher', '')
        }
    
    def collect_game_links(year, progress_callback):
        global total_pages, processed_pages
        
        base_url = 'https://adventuregamers.com/games/adventure/all-year-desc/page{}'
        page_number = 1
        game_links = []
        
        while True:
            url = base_url.format(page_number)
            html = fetch_page(url)
            games = parse_games_from_page(html)
            
            if not games:
                break
            
            total_pages += 1
            processed_pages += 1
            progress_callback(f"Обработано страниц: {processed_pages}/{total_pages}")
            
            for game_year, game_link, game_name in games:
                if game_year < year:
                    return game_links
                game_links.append((game_year, game_link, game_name))
            
            page_number += 1
        
        return game_links
    
    def process_game(game_link, game_name, game_year):
        game_url = f"https://adventuregamers.com{game_link}"
        game_html = fetch_page(game_url)
        game_details = parse_game_details(game_html)
        return (game_url, game_name, game_year, game_details['release_date'], game_details['steam_url'], game_details['description'], game_details['image'], game_details['author'], game_details['publisher'])
    
    def collect_game_data(game_links, max_workers, progress_callback):
        global total_games, processed_games
        
        games_data = []
        total_games = len(game_links)
        
        with ThreadPoolExecutor(max_workers=max_workers) as executor:
            futures = []
            for game_year, game_link, game_name in game_links:
                futures.append(executor.submit(process_game, game_link, game_name, game_year))
            
            for future in futures:
                games_data.append(future.result())
                processed_games += 1
                progress_callback(f"Обработано игр: {processed_games}/{total_games}")
        
        return games_data
    
    def save_to_csv(data, filename):
        with open(filename, 'w', newline='', encoding='utf-8') as csvfile:
            writer = csv.writer(csvfile, delimiter='\t')
            writer.writerow(['Ссылка на игру', 'Название игры', 'Год', 'Дата выхода', 'Ссылка на Steam', 'Описание', 'Изображение', 'Автор', 'Издатель'])
            writer.writerows(data)
    
    def start_collection(year_entry, max_workers_entry, progress_label_pages, progress_label_games):
        year = int(year_entry.get())
        max_workers = int(max_workers_entry.get())
        progress_label_pages.config(text="Начало сбора данных...")
        
        def update_progress_pages(message):
            progress_label_pages.config(text=message)
        
        def update_progress_games(message):
            progress_label_games.config(text=message)
        
        def collect_and_save():
            game_links = collect_game_links(year, update_progress_pages)
            games_data = collect_game_data(game_links, max_workers, update_progress_games)
            save_to_csv(games_data, 'games_data.csv')
            messagebox.showinfo("Сбор данных завершен", "Сбор данных успешно завершен!")
        
        data_thread = threading.Thread(target=collect_and_save)
        data_thread.start()
    
    def main():
        root = tk.Tk()
        root.title("Adventure DB Сборщик")
        root.geometry("350x350")
    
        tk.Label(root, text="Введите стартовый год:").pack(pady=5)
        year_entry = tk.Entry(root)
        year_entry.pack(pady=5)
        
        max_workers_label = tk.Label(root, text="Количество max workers:")
        max_workers_label.pack(pady=5)
        
        max_workers_entry = tk.Entry(root)
        max_workers_entry.insert(0, "4")
        max_workers_entry.pack(pady=5)
    
        max_workers_label2 = tk.Label(root, text="(увеличение количества max workers ускорит сбор,\nно сайт может заблокировать вас за большое количество\nзапросов за короткий промежуток времени, рекомендую\nдержать количество max workers на уровне 4 и ниже)", fg="red")
        max_workers_label2.pack(pady=2)
            
        
        progress_label_pages = tk.Label(root, text="")
        progress_label_pages.pack(pady=10)
        
        progress_label_games = tk.Label(root, text="")
        progress_label_games.pack(pady=10)
        
        start_button = tk.Button(root, text="Начать сбор", command=lambda: start_collection(year_entry, max_workers_entry, progress_label_pages, progress_label_games))
        start_button.pack(pady=10)
        
        root.mainloop()
    
    if __name__ == "__main__":
        main()

     

     

  7. Запустите программу с помощью
    
    python start.py
  8. В открывшемся окне:
    - Введите стартовый год (будут собраны только игры, которые идут с начала этого года и позже)
    - Max workers можно не менять, см. предупреждение.
     
  9. После этого можете нажать на кнопку “Начать сбор” и через несколько секунд начнётся сбор информации.
  10. Во время сбора вам будет виден прогресс выполнения. В конце появится табличка, что сбор завершён. После этого можно закрывать программу.
  11. Файл в виде csv таблицы хранится в папке с программой. Можете импортировать её на гугл-таблицы или в любое табличное приложение (разделитель — табуляция).

 

Изменено пользователем 0wn3df1x
  • Лайк (+1) 1
  • +1 1

Поделиться сообщением


Ссылка на сообщение

Обновление: Улучшение скрипта для сбора игр с каталога Steam #2 || Python

Теперь:

  • Скрипт собирает данные в 4 раза быстрее и объём страниц, с которых он собирает данные, уменьшился в 4 раза, что временно* позволяет избежать необходимости постоянной смены прокси в случае со сбором игр гигантского каталога вроде каталога игр с английским языком в американском регионе (тогда страниц было больше 3000, теперь чуть больше 800);  *Временно, поскольку каталог растёт и всё равно рано дойдёт до критического числа страниц, а значит в будущем, к сожалению, всё равно придётся заниматься периодическим переключением прокси.
  • Можно поставить флажок на “игры с карточками”, чтобы остались только игры, в которых есть карточки.
  • Можно сперва скачать файлы для американского региона (us), затем для русского региона (us). После чего американский файл загрузить в качестве FullBase_US, а русский в качестве FullBase_Ru. Затем нажать кнопку “Собрать базу” и получить файл FullBase_USRU.csv, где к играм из американского региона добавятся игры, которые есть только в российском регионе. Также добавится столбец с рублёвыми ценами. Если рублёвых цен нет — будет написано [NDR] (значит игра недоступна в российском регионе).

 

jeGlvAe.png



Предназначение скрипта:
Система фильтрации каталога Steam оставляет желать лучшего. Запросы к базе данных ограничены узкой подборкой:

Скрытый текст
  • Вы можете выбрать стоимость игр только до какого-то предела. Допустим, до 300 рублей, до 150 рублей и так далее. Нельзя установить точные рамки: “от 1000 до 1500”, от “1200 до 1300” и так далее.
  • В Steam есть количество обзоров и рейтинг этих обзоров, но в каталоге нет запроса, который позволил бы оставить только игры с заданных рейтингом и количеством обзоров. Эту проблему можно исправить плагином Augmented Steam, но он не может отправить запрос для получения ограниченной выборки. Вместо этого он просто скрывает игры, которые не попадают под заданные критерии. Таким образом, если вы поставите этот плагин, зададите в нём критерии и начнёте листать магазин — ваш браузер начнёт перегружаться скрытым кодом. Из-за большого количества запросов вы и вовсе можете быть заблокированы на несколько минут — сначала у вас отвалятся картинки и видео, а затем появится надпись о том, что вы не можете получить доступ к сайту.
  • Для того, чтобы увидеть обзоры игры и жанры, к которым она относится, вам нужно навести на неё курсор и дождаться, пока появится виджет со всей информацией. 

Мой скрипт позволяет задать валюту (она же регион), с которого будут собираться данные, язык, на котором доступны игры, наличие скидок, платность/бесплатность и тэги. После чего он соберёт данные обо всех вышедших играх в определённом регионе и доступных на заданном языке и сохранит их в csv файл.

Скрытый текст
  • app_id — айди игры
  • image_url — картинка игры из магазина
  • title — название игры
  • release_date — дата выхода игры в формате день.месяц.год (есть игры с забагованными датами, которые идут в формате “April 2024” и т.п., они запихиваются в конец таблицы.
  • review_percentage — рейтинг игры.
  • review_count — количество обзоров.
  • tag_ids — основные метки, присвоенные игре (они же жанры).
  • price — текущая цена.
  • orprice — оригинальная цена (пишется только в случае наличия сейчас скидки).
  • (RuPrice — цена в рублях; актуально для FullBase_USRU.csv)
Скрытый текст

9rQIeZv.png

Скрытый текст

Эту таблицу можно загрузить на Google Spreadsheets и там навешать на неё фильтров, после чего фильтровать игры с нужными жанрами, рейтингами, обзорами и ценами. Также можно использовать формулу “image” для выведения картинок. 
Можно добавить столбец, где “https://store.steampowered.com/app/” будет соединяться с идентификатором игры, образуя ссылку.

Скрытый текст

CiHGilP.png

В общем, наличие самой базы данных позволяет извращаться с данными как душе угодно, обходя ограничения сервиса.

Скрытый текст

Инструкция:

  1. Если у вас нет python и для вас это что-то далёкое, то вот готовая сборка программы.
    Скачиваете и запускаете, после чего можете перейти к пункту 9.
     
  2. Допустим, третий python у вас есть.
  3. В консоли перейдите в папку, где будет лежать программа.
  4. В папке создайте файл requirements.txt и напишите в нём
    
    requests
    beautifulsoup4
    aiohttp
  5. Пропишите в консоли
    
    pip install -r requirements.txt

     

  6. Создайте файл config.txt и поместите в него следующие данные:
    
    cc=us
    supportedlang=english
    filename=3010ENUS.csv
    cards=False
    specials=False
    hidef2p=False
    tags=

     

  7. Создайте файл tags.txt и поместите в него следующие данные:
    Скрытый текст
    
    
    9	Strategy
    19	Action
    21	Adventure
    84	Design & Illustration
    87	Utilities
    113	Free to Play
    122	RPG
    128	Massively Multiplayer
    492	Indie
    493	Early Access
    597	Casual
    599	Simulation
    699	Racing
    701	Sports
    784	Video Production
    809	Photo Editing
    872	Animation & Modeling
    1027	Audio Production
    1036	Education
    1038	Web Publishing
    1445	Software Training
    1616	Trains
    1621	Music
    1625	Platformer
    1628	Metroidvania
    1638	Dog
    1643	Building
    1644	Driving
    1645	Tower Defense
    1646	Hack and Slash
    1647	Western
    1649	GameMaker
    1651	Satire
    1654	Relaxing
    1659	Zombies
    1662	Survival
    1663	FPS
    1664	Puzzle
    1665	Match 3
    1666	Card Game
    1667	Horror
    1669	Moddable
    1670	4X
    1671	Superhero
    1673	Aliens
    1674	Typing
    1676	RTS
    1677	Turn-Based
    1678	War
    1680	Heist
    1681	Pirates
    1684	Fantasy
    1685	Co-op
    1687	Stealth
    1688	Ninja
    1695	Open World
    1697	Third Person
    1698	Point & Click
    1702	Crafting
    1708	Tactical
    1710	Surreal
    1714	Psychedelic
    1716	Rogue-like
    1717	Hex Grid
    1718	MOBA
    1719	Comedy
    1720	Dungeon Crawler
    1721	Psychological Horror
    1723	Action RTS
    1730	Sokoban
    1732	Voxel
    1733	Unforgiving
    1734	Fast-Paced
    1736	LEGO
    1738	Hidden Object
    1741	Turn-Based Strategy
    1742	Story Rich
    1743	Fighting
    1746	Basketball
    1751	Comic Book
    1752	Rhythm
    1753	Skateboarding
    1754	MMORPG
    1755	Space
    1756	Great Soundtrack
    1759	Perma Death
    1770	Board Game
    1773	Arcade
    1774	Shooter
    1775	PvP
    1777	Steampunk
    3796	Based On A Novel
    3798	Side Scroller
    3799	Visual Novel
    3810	Sandbox
    3813	Real Time Tactics
    3814	Third-Person Shooter
    3834	Exploration
    3835	Post-apocalyptic
    3839	First-Person
    3841	Local Co-Op
    3843	Online Co-Op
    3854	Lore-Rich
    3859	Multiplayer
    3871	2D
    3877	Precision Platformer
    3878	Competitive
    3916	Old School
    3920	Cooking
    3934	Immersive
    3942	Sci-fi
    3952	Gothic
    3955	Character Action Game
    3959	Rogue-lite
    3964	Pixel Graphics
    3968	Physics
    3978	Survival Horror
    3987	Historical
    3993	Combat
    4004	Retro
    4018	Vampire
    4026	Difficult
    4036	Parkour
    4046	Dragons
    4057	Magic
    4064	Thriller
    4085	Anime
    4094	Minimalist
    4102	Combat Racing
    4106	Action-Adventure
    4115	Cyberpunk
    4136	Funny
    4137	Transhumanism
    4145	Cinematic
    4150	World War II
    4155	Class-Based
    4158	Beat 'em up
    4161	Real-Time
    4166	Atmospheric
    4168	Military
    4172	Medieval
    4175	Realistic
    4182	Singleplayer
    4184	Chess
    4191	3D
    4195	Cartoony
    4202	Trading
    4231	Action RPG
    4234	Short
    4236	Loot
    4242	Episodic
    4252	Stylized
    4255	Shoot 'Em Up
    4291	Spaceships
    4295	Futuristic
    4305	Colorful
    4325	Turn-Based Combat
    4328	City Builder
    4342	Dark
    4345	Gore
    4364	Grand Strategy
    4376	Assassin
    4400	Abstract
    4434	JRPG
    4474	CRPG
    4486	Choose Your Own Adventure
    4508	Co-op Campaign
    4520	Farming
    4559	Quick-Time Events
    4562	Cartoon
    4598	Alternate History
    4604	Dark Fantasy
    4608	Swordplay
    4637	Top-Down Shooter
    4667	Violent
    4684	Wargame
    4695	Economy
    4700	Movie
    4711	Replay Value
    4726	Cute
    4736	2D Fighter
    4747	Character Customization
    4754	Politics
    4758	Twin Stick Shooter
    4777	Spectacle fighter
    4791	Top-Down
    4821	Mechs
    4835	6DOF
    4840	4 Player Local
    4845	Capitalism
    4853	Political
    4878	Parody
    4885	Bullet Hell
    4947	Romance
    4975	2.5D
    4994	Naval Combat
    5030	Dystopian
    5055	eSports
    5094	Narration
    5125	Procedural Generation
    5153	Kickstarter
    5154	Score Attack
    5160	Dinosaurs
    5179	Cold War
    5186	Psychological
    5228	Blood
    5230	Sequel
    5300	God Game
    5310	Games Workshop
    5348	Mod
    5350	Family Friendly
    5363	Destruction
    5372	Conspiracy
    5379	2D Platformer
    5382	World War I
    5390	Time Attack
    5395	3D Platformer
    5407	Benchmark
    5411	Beautiful
    5432	Programming
    5502	Hacking
    5537	Puzzle-Platformer
    5547	Arena Shooter
    5577	RPGMaker
    5608	Emotional
    5611	Mature
    5613	Detective
    5652	Collectathon
    5673	Modern
    5708	Remake
    5711	Team-Based
    5716	Mystery
    5727	Baseball
    5752	Robots
    5765	Gun Customization
    5794	Science
    5796	Bullet Time
    5851	Isometric
    5900	Walking Simulator
    5914	Tennis
    5923	Dark Humor
    5941	Reboot
    5981	Mining
    5984	Drama
    6041	Horses
    6052	Noir
    6129	Logic
    6214	Birds
    6276	Inventory Management
    6310	Diplomacy
    6378	Crime
    6426	Choices Matter
    6506	3D Fighter
    6621	Pinball
    6625	Time Manipulation
    6650	Nudity
    6691	1990's
    6702	Mars
    6730	PvE
    6815	Hand-drawn
    6869	Nonlinear
    6910	Naval
    6915	Martial Arts
    6948	Rome
    6971	Multiple Endings
    7038	Golf
    7107	Real-Time with Pause
    7108	Party
    7113	Crowdfunded
    7178	Party Game
    7208	Female Protagonist
    7250	Linear
    7309	Skiing
    7328	Bowling
    7332	Base Building
    7368	Local Multiplayer
    7423	Sniper
    7432	Lovecraftian
    7478	Illuminati
    7481	Controller
    7569	Grid-Based Movement
    7622	Offroad
    7702	Narrative
    7743	1980s
    7918	Dwarf
    7926	Artificial Intelligence
    7948	Soundtrack
    8013	Software
    8075	TrackIR
    8093	Minigames
    8122	Level Editor
    8253	Music-Based Procedural Generation
    8369	Investigation
    8461	Well-Written
    8666	Runner
    8945	Resource Management
    9130	Hentai
    9157	Underwater
    9204	Immersive Sim
    9271	Trading Card Game
    9541	Demons
    9551	Dating Sim
    9564	Hunting
    9592	Dynamic Narration
    9803	Snow
    9994	Experience
    10235	Life Sim
    10383	Transportation
    10397	Memes
    10437	Trivia
    10679	Time Travel
    10695	Party-Based RPG
    10808	Supernatural
    10816	Split Screen
    11014	Interactive Fiction
    11095	Boss Rush
    11104	Vehicular Combat
    11123	Mouse only
    11333	Villain Protagonist
    11634	Vikings
    12057	Tutorial
    12095	Sexual Content
    12190	Boxing
    12286	Warhammer 40K
    12472	Management
    13070	Solitaire
    13190	America
    13276	Tanks
    13382	Archery
    13577	Sailing
    13782	Experimental
    13906	Game Development
    14139	Turn-Based Tactics
    14153	Dungeons & Dragons
    14720	Nostalgia
    14906	Intentionally Awkward Controls
    15045	Flight
    15172	Conversation
    15277	Philosophical
    15339	Documentary
    15564	Fishing
    15868	Motocross
    15954	Silent Protagonist
    16094	Mythology
    16250	Gambling
    16598	Space Sim
    16689	Time Management
    17015	Werewolves
    17305	Strategy RPG
    17337	Lemmings
    17389	Tabletop
    17770	Asynchronous Multiplayer
    17894	Cats
    17927	Pool
    18594	FMV
    19568	Cycling
    19780	Submarine
    19995	Dark Comedy
    21006	Underground
    21725	Tactical RPG
    21978	VR
    22602	Agriculture
    22955	Mini Golf
    24003	Word Game
    24904	NSFW
    25085	Touch-Friendly
    26921	Political Sim
    27758	Voice Control
    28444	Snowboarding
    29363	3D Vision
    29482	Souls-like
    29855	Ambient
    30358	Nature
    30927	Fox
    31275	Text-Based
    31579	Otome
    32322	Deckbuilding
    33572	Mahjong
    35079	Job Simulator
    42089	Jump Scare
    42329	Coding
    42804	Action Roguelike
    44868	LGBTQ+
    47827	Wrestling
    49213	Rugby
    51306	Foreign
    56690	On-Rails Shooter
    61357	Electronic Music
    65443	Adult Content
    71389	Spelling
    87918	Farming Sim
    91114	Shop Keeper
    92092	Jet
    96359	Skating
    97376	Cozy
    102530	Elf
    117648	8-bit Music
    123332	Bikes
    129761	ATV
    143739	Electronic
    150626	Gaming
    158638	Cricket
    176733	Tile-Matching
    176981	Battle Royale
    180368	Faith
    189941	Instrumental Music
    198631	Mystery Dungeon
    198913	Motorbike
    220585	Colony Sim
    233824	Feature Film
    252854	BMX
    255534	Automation
    323922	Musou
    324176	Hockey
    337964	Rock Music
    348922	Steam Machine
    353880	Looter Shooter
    363767	Snooker
    379975	Clicker
    454187	Traditional Roguelike
    552282	Wholesome
    603297	Hardware
    615955	Idler
    620519	Hero Shooter
    745697	Social Deduction
    769306	Escape Room
    776177	360 Video
    791774	Card Battler
    847164	Volleyball
    856791	Asymmetric VR
    916648	Creature Collector
    922563	Roguevania
    1003823	Profile Features Limited
    1023537	Boomer Shooter
    1084988	Auto Battler
    1091588	Roguelike Deckbuilder
    1100686	Outbreak Sim
    1100687	Automobile Sim
    1100688	Medical Sim
    1100689	Open World Survival Craft
    1199779	Extraction Shooter
    1220528	Hobby Sim
    1254546	Football (Soccer)
    1254552	Football (American)

     

  8. Создайте файл start.py и поместите в него основной код скрипта
    Скрытый текст
    
    
    import tkinter as tk
    from tkinter import filedialog, messagebox
    import requests
    from bs4 import BeautifulSoup
    import csv
    import asyncio
    import aiohttp
    import re
    import os
    import threading
    from datetime import datetime
    from calendar import monthrange
    import time
    
    # Функция для загрузки данных о тегах из файла
    def load_tags():
        tags = {}
        with open('tags.txt', 'r', encoding='utf-8') as file:
            for line in file:
                tag_id, tag_text = line.strip().split('\t')
                tags[tag_id] = tag_text
        return tags
    
    # Функция для извлечения данных из элемента
    def extract_data(element, tags):
        app_id = element.get('data-ds-appid', '')
        title = element.find(class_='title').text.strip() if element.find(class_='title') else ''
        tag_ids = element.get('data-ds-tagids', '')
        tag_ids = tag_ids.strip('[]')  # Удаляем квадратные скобки из строки
        tag_ids = [tags.get(tag_id, '') for tag_id in tag_ids.split(',')]
        tooltip_html = element.find(class_='search_review_summary')['data-tooltip-html'] if element.find(class_='search_review_summary') else ''
        review_percentage = re.search(r'(\d+)%', tooltip_html).group(1) if tooltip_html else ''
        review_count = re.search(r'of the (\d{1,3}(?:,\d{3})*)', tooltip_html).group(1) if tooltip_html else ''
        review_count = review_count.replace(',', '')  # Убираем запятые из числа обзоров
        image_url = element.find('img')['src'] if element.find('img') else ''
        release_date = element.find(class_='search_released').text.strip() if element.find(class_='search_released') else ''
        release_date = parse_date(release_date)  # Преобразуем дату в единый формат
        orprice = element.find(class_='discount_original_price').text.strip() if element.find(class_='discount_original_price') else 'No price'
        price = element.find(class_='discount_final_price').text.strip() if element.find(class_='discount_final_price') else 'No price'
    
        return [app_id, image_url, title, release_date, review_percentage, review_count, tag_ids, price, orprice]
    
    # Функция для преобразования даты в единый формат
    def parse_date(date_str):
        if not date_str or date_str == 'Coming Soon':
            return date_str
        try:
            date_obj = datetime.strptime(date_str, '%b %d, %Y')
        except ValueError:
            try:
                date_obj = datetime.strptime(date_str, '%d %b, %Y')
            except ValueError:
                try:
                    date_obj = datetime.strptime(date_str, '%B %Y')
                    last_day = monthrange(date_obj.year, date_obj.month)[1]
                    date_obj = date_obj.replace(day=last_day)
                except ValueError:
                    try:
                        date_obj = datetime.strptime(date_str, '%b %Y')
                        last_day = monthrange(date_obj.year, date_obj.month)[1]
                        date_obj = date_obj.replace(day=last_day)
                    except ValueError:
                        return date_str
        return date_obj.strftime('%d.%m.%Y')
    
    # Функция для получения количества страниц
    def get_total_pages(url):
        response = requests.get(url)
        data = response.json()
        total_count = data['total_count']
        return (total_count // 100) + 1
    
    # Асинхронная функция для сбора данных
    async def fetch_data(session, url, tags, writer, progress_var, total_pages, start_time, speed_var, time_left_var):
        try:
            async with session.get(url) as response:
                response.raise_for_status()
                data = await response.json()
                results_html = data['results_html']
                soup = BeautifulSoup(results_html, 'html.parser')
                elements = soup.find_all(class_='search_result_row')
                for element in elements:
                    data = extract_data(element, tags)
                    writer.writerow(data)
                progress_var.set(progress_var.get() + 1)
                update_progress_label(progress_var, total_pages, start_time, speed_var, time_left_var)
        except (aiohttp.ClientError, asyncio.TimeoutError) as e:
            print(f"Error fetching {url}: {e}")
            await asyncio.sleep(30)
            await fetch_data(session, url, tags, writer, progress_var, total_pages, start_time, speed_var, time_left_var)
    
    # Асинхронная функция для запуска сбора данных
    async def start_collecting(cc, supportedlang, filename, progress_var, speed_var, time_left_var, options):
        tags = load_tags()
        base_url = f'https://store.steampowered.com/search/results/?query&count=100&dynamic_data=&sort_by=Released_DESC&ignore_preferences=1&category1=998&cc={cc}&supportedlang={supportedlang}&ndl=1&infinite=1'
    
        # Добавляем дополнительные параметры в URL
        if options['cards']:
            base_url += '&category2=29'
        if options['specials']:
            base_url += '&specials=1'
        if options['hidef2p']:
            base_url += '&hidef2p=1'
        if options['tags']:
            base_url += f'&tags={options["tags"]}'
    
        total_pages = get_total_pages(base_url)
        progress_var.set(0)
        temp_filename = filename + '.tmp'
        with open(temp_filename, 'w', newline='', encoding='utf-8') as csvfile:
            writer = csv.writer(csvfile, delimiter='\t')
            writer.writerow(['app_id', 'image_url', 'title', 'release_date', 'review_percentage', 'review_count', 'tag_ids', 'price', 'orprice'])
            async with aiohttp.ClientSession() as session:
                start_time = time.time()
                tasks = [fetch_data(session, base_url + f'&start={start * 100}', tags, writer, progress_var, total_pages, start_time, speed_var, time_left_var) for start in range(total_pages)]
                await asyncio.gather(*tasks)
        
        # Сортировка данных по дате
        sort_data_by_date(temp_filename, filename)
        os.remove(temp_filename)
        messagebox.showinfo("Сбор завершён", "Сбор данных завершен.")
    
    # Функция для сортировки данных по дате
    def sort_data_by_date(temp_filename, filename):
        data = []
        with open(temp_filename, 'r', encoding='utf-8') as csvfile:
            reader = csv.reader(csvfile, delimiter='\t')
            header = next(reader)
            for row in reader:
                data.append(row)
        
        # Сортировка данных по дате, помещая ошибочные даты в конец
        data.sort(key=lambda x: (is_valid_date(x[3]), datetime.strptime(x[3], '%d.%m.%Y') if is_valid_date(x[3]) else datetime.min), reverse=True)
        
        with open(filename, 'w', newline='', encoding='utf-8') as csvfile:
            writer = csv.writer(csvfile, delimiter='\t')
            writer.writerow(header)
            writer.writerows(data)
    
    # Функция для проверки, является ли дата валидной
    def is_valid_date(date_str):
        try:
            datetime.strptime(date_str, '%d.%m.%Y')
            return True
        except ValueError:
            return False
    
    # Функция для сохранения конфигурации
    def save_config(cc, supportedlang, filename, options):
        with open('config.txt', 'w', encoding='utf-8') as file:
            file.write(f'cc={cc}\nsupportedlang={supportedlang}\nfilename={filename}\n')
            file.write(f'cards={options["cards"]}\nspecials={options["specials"]}\nhidef2p={options["hidef2p"]}\ntags={options["tags"]}')
    
    # Функция для загрузки конфигурации
    def load_config():
        config = {'cc': 'us', 'supportedlang': 'russian', 'filename': 'result', 'cards': False, 'specials': False, 'hidef2p': False, 'tags': ''}
        if os.path.exists('config.txt'):
            with open('config.txt', 'r', encoding='utf-8') as file:
                for line in file:
                    key, value = line.strip().split('=')
                    if value.lower() == 'true':
                        value = True
                    elif value.lower() == 'false':
                        value = False
                    config[key] = value
        return config
    
    # Функция для запуска сбора данных в отдельном потоке
    def start_collecting_thread(cc, supportedlang, filename, progress_var, speed_var, time_left_var, options):
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        loop.run_until_complete(start_collecting(cc, supportedlang, filename, progress_var, speed_var, time_left_var, options))
        loop.close()
    
    # Функция для обновления метки прогресса
    def update_progress_label(progress_var, total_pages, start_time, speed_var, time_left_var):
        current_time = time.time()
        elapsed_time = current_time - start_time
        speed = progress_var.get() / elapsed_time if elapsed_time > 0 else 0
        speed_var.set(f"{speed:.2f} стр/сек")
        remaining_pages = total_pages - progress_var.get()
        remaining_time = remaining_pages / speed if speed > 0 else 0
        minutes, seconds = divmod(remaining_time, 60)
        time_left_var.set(f"Осталось {int(minutes)} м. {int(seconds)} сек.")
        progress_label.config(text=f"{progress_var.get()}/{total_pages}")
    
    # Функция для запуска сбора данных
    def on_start_collecting():
        cc = cc_entry.get()
        supportedlang = supportedlang_entry.get()
        filename = filename_entry.get()
        options = {
            'cards': cards_var.get(),
            'specials': specials_var.get(),
            'hidef2p': hidef2p_var.get(),
            'tags': tags_entry.get()
        }
        save_config(cc, supportedlang, filename, options)
        threading.Thread(target=start_collecting_thread, args=(cc, supportedlang, filename, progress_var, speed_var, time_left_var, options)).start()
    
    # Функция для загрузки FullBase_US
    def load_fullbase_us():
        fullbase_us_path = filedialog.askopenfilename(filetypes=[("CSV files", "*.csv")])
        if fullbase_us_path:
            fullbase_us_entry.delete(0, tk.END)
            fullbase_us_entry.insert(0, fullbase_us_path)
    
    # Функция для загрузки FullBase_RU
    def load_fullbase_ru():
        fullbase_ru_path = filedialog.askopenfilename(filetypes=[("CSV files", "*.csv")])
        if fullbase_ru_path:
            fullbase_ru_entry.delete(0, tk.END)
            fullbase_ru_entry.insert(0, fullbase_ru_path)
    
    # Функция для объединения файлов
    def combine_bases():
        fullbase_us_path = fullbase_us_entry.get()
        fullbase_ru_path = fullbase_ru_entry.get()
    
        if not fullbase_us_path or not fullbase_ru_path:
            messagebox.showwarning("Предупреждение", "Выберите оба файла перед объединением.")
            return
    
        combined_data = []
    
        # Чтение FullBase_US
        with open(fullbase_us_path, 'r', encoding='utf-8') as us_file:
            us_reader = csv.reader(us_file, delimiter='\t')
            us_header = next(us_reader)
            combined_data.extend(list(us_reader))
    
        # Чтение FullBase_RU
        with open(fullbase_ru_path, 'r', encoding='utf-8') as ru_file:
            ru_reader = csv.reader(ru_file, delimiter='\t')
            ru_header = next(ru_reader)
            ru_data = list(ru_reader)
    
        us_app_ids = {row[0] for row in combined_data}
    
        # Добавление данных из FullBase_RU, которых нет в FullBase_US
        for row in ru_data:
            if row[0] not in us_app_ids:
                combined_data.append(row)
    
        # Добавление столбца RuPrice
        ru_prices = {row[0]: row[7] for row in ru_data}
        combined_data_with_ruprice = []
        for row in combined_data:
            ru_price = ru_prices.get(row[0], "[NDR]")
            combined_data_with_ruprice.append(row + [ru_price])
    
        # Сохранение результата в новый CSV файл
        output_file = 'FullBase_USRU.csv'
        with open(output_file, 'w', newline='', encoding='utf-8') as outfile:
            writer = csv.writer(outfile, delimiter='\t')
            writer.writerow(us_header + ['RuPrice'])
            writer.writerows(combined_data_with_ruprice)
    
        messagebox.showinfo("Успех", f"Файл {output_file} успешно создан.")
    
    # Создание главного окна
    root = tk.Tk()
    root.title("Steam Data Collector")
    
    # Загрузка конфигурации
    config = load_config()
    
    # Создание элементов интерфейса
    tk.Label(root, text="Валюта").grid(row=0, column=0)
    cc_entry = tk.Entry(root)
    cc_entry.insert(0, config['cc'])
    cc_entry.grid(row=0, column=1)
    
    tk.Label(root, text="Язык").grid(row=1, column=0)
    supportedlang_entry = tk.Entry(root)
    supportedlang_entry.insert(0, config['supportedlang'])
    supportedlang_entry.grid(row=1, column=1)
    
    tk.Label(root, text="Название файла").grid(row=2, column=0)
    filename_entry = tk.Entry(root)
    filename_entry.insert(0, config['filename'])
    filename_entry.grid(row=2, column=1)
    
    # Опции
    cards_var = tk.BooleanVar(value=config['cards'])
    cards_check = tk.Checkbutton(root, text="Игры с карточками", variable=cards_var)
    cards_check.grid(row=3, column=0, columnspan=2)
    
    specials_var = tk.BooleanVar(value=config['specials'])
    specials_check = tk.Checkbutton(root, text="Скидки", variable=specials_var)
    specials_check.grid(row=4, column=0, columnspan=2)
    
    hidef2p_var = tk.BooleanVar(value=config['hidef2p'])
    hidef2p_check = tk.Checkbutton(root, text="Скрыть бесплатные", variable=hidef2p_var)
    hidef2p_check.grid(row=5, column=0, columnspan=2)
    
    tk.Label(root, text="Тэги").grid(row=6, column=0)
    tags_entry = tk.Entry(root)
    tags_entry.insert(0, config['tags'])
    tags_entry.grid(row=6, column=1)
    
    start_button = tk.Button(root, text="Начать сбор", command=on_start_collecting)
    start_button.grid(row=7, column=0, columnspan=2)
    
    progress_var = tk.IntVar()
    progress_label = tk.Label(root, text="0/0")
    progress_label.grid(row=8, column=0, columnspan=2)
    
    speed_var = tk.StringVar()
    speed_label = tk.Label(root, textvariable=speed_var)
    speed_label.grid(row=9, column=0, columnspan=2)
    
    time_left_var = tk.StringVar()
    time_left_label = tk.Label(root, textvariable=time_left_var)
    time_left_label.grid(row=10, column=0, columnspan=2)
    
    # Создание элементов интерфейса для новых функций
    tk.Label(root, text="FullBase_US").grid(row=11, column=0)
    fullbase_us_entry = tk.Entry(root)
    fullbase_us_entry.grid(row=11, column=1)
    load_us_button = tk.Button(root, text="Загрузить FullBase_US", command=load_fullbase_us)
    load_us_button.grid(row=11, column=2)
    
    tk.Label(root, text="FullBase_RU").grid(row=12, column=0)
    fullbase_ru_entry = tk.Entry(root)
    fullbase_ru_entry.grid(row=12, column=1)
    load_ru_button = tk.Button(root, text="Загрузить FullBase_RU", command=load_fullbase_ru)
    load_ru_button.grid(row=12, column=2)
    
    combine_button = tk.Button(root, text="Собрать базу", command=combine_bases)
    combine_button.grid(row=13, column=0, columnspan=3)
    
    # Запуск главного цикла
    root.mainloop()

     

     

  9. Запустите программу с помощью
    
    python start.py
  10. В открывшемся окне:
    - Измените валюту/регион на нужную вам. (us — США; ru — Россия; kz — Казахстан) 
    - Измените язык на нужный вам (english — будут показаны игры, где есть английский; russian — будут показаны игры, где есть русский)
    - Можете изменить название файла, если хотите.
    - Можете поставить флажок на “игры с карточками”, чтобы показывало только игры с карточками.
    - Можете поставить флажок на скидках, чтобы выдало только игры со скидками.
    - Можете поставить флажок на “Скрыть бесплатные”, чтобы их не искало.
    - Можете через запятую перечислить числовые значения тэгов (их можно узнать в файле tags.txt)
    jeGlvAe.png
  11. После этого можете нажать на кнопку “Начать сбор” и через несколько секунд начнётся сбор информации.
  12. Во время сбора вам будет виден прогресс выполнения, скорость загрузки страниц и оставшееся время. В конце появится табличка, что сбор завершён. После этого можно закрывать программу.
  13. Файл в виде csv таблицы хранится в папке с программой. Можете импортировать её на гугл-таблицы или в любое табличное приложение (разделитель — табуляция).
     
  14. Касательно функций ниже. Вы можете прописать в валюте us и собрать все игры с russian в файле ruus. Затем прописать в валюте ru и собрать все игры с russian в файле ruru. Затем можете загрузить ruus в качестве FullBase_US, а ruru в качестве FullBase_RU и нажать кнопку “Собрать базу”. Данные будут собраны в FullBase_USRU.csv, где к играм из американского региона добавятся игры, которые есть только в российском регионе. Также добавится столбец с рублёвыми ценами. Если рублёвых цен нет — будет написано [NDR] (значит игра недоступна в российском регионе).

(!) Желательно собирать информацию под прокси или vpn, чтобы ваш родной айпишник не заблокировало на несколько минут за частое обращение к серверу. 

(!) Если вы собираете слишком большой объём данных, к примеру, собираете все игры из американского региона на английском языке (us+english), и сбор информации начнёт переваливать за тысячу страниц, то после 1000-й страницы консоль может начать сыпать ошибками, а сам скрипт перестанет собирать информацию на 30 секунд. Через 30 секунд он возобновит сбор информации. Вы можете воспользоваться этим моментом, чтобы сменить прокси и избежать временной блокировки. Скрипт соберёт пропущенную информацию в любом случае.

(!) При сборе небольшого объёма информации, к примеру, всех игр на русском языке в любом регионе, ошибок и временных блокировок возникнуть не должно. 
^^^^ После оптимизации скрипта это также относится к американскому региону с английским, пока количество страниц в нём не перевалило за тысячу. Но этот рубеж скоро будет преодолён и тогда проблема вновь может возникнуть.

Данный скрипт неоднократно использовался мной для составления разной статистики, а также для того, чтобы узнавать о появлении официальных переводов для уже вышедших игр путём сравнения сохранённых баз.

Также он удобен для ведения бэклога. В своей userdata вы можете раздобыть информацию об играх из своего списка желаемого и играх, которые есть на аккаунте, после чего воспользоваться vlookup, чтобы отметить их на таблице. Это позволит отфильтровать игры, которые у вас есть и которые вы уже желаете, чтобы найти что-нибудь новое и пометить. Также можно пометить игры, которые вы уже изучили, чтобы в дальнейшем их скрывать. 

  • Лайк (+1) 1

Поделиться сообщением


Ссылка на сообщение

Скрипт для улучшения поиска товаров у конкретного продавца на Plati market (он же Plati ru) || Tamper Monkey — функционал перемещён в Plati.Market; Ultimate Enhancer.

Изменено пользователем 0wn3df1x
  • Лайк (+1) 1

Поделиться сообщением


Ссылка на сообщение

Скрыватель игр Steam || Tamper Monkey — функционал перемещён в Ultimate Steam Enhancer.

Скрытый текст

 

В Steam есть функция добавления игр в игнорируемые. В данный момент она устроена так:

  • Сперва вам необходимо навести курсор на многоточие у значка игры
    7vpVvkZ.png
     
  • Затем выбрать “Скрыть”
    8kXDxKJ.png

После того, как вы нажимаете скрыть — Steam делает элемент невидимым, а также отправляет запрос на сервер, чтобы игра добавилась в список игнорируемых вами игр и скрывалась в дальнейшем.

С этим есть две проблемы:

  • Сам процесс скрытия является неудобным. Многоточие занимает всего 1/3 от высоты элемента и по нему легко промахнуться. Кроме того — отсутствует возможность скрытия нескольких игр сразу, каждый раз приходится нажимать многоточие, а затем выбирать скрытие.
  • Скрытие просто делает элементы невидимыми, но они продолжают присутствовать в коде. Сейчас в Steam больше 6000 будущих игр и если вы скроете, к примеру, 5000 из них — код этих 5000 игр всё равно будет присутствовать на странице, загружая вашу память и замедляя взаимодействие.

Мой скрипт:

  • Добавляет к каждому элементу кольца высотой в 99% элемента, в результате чего по ним практически невозможно промахнуться. 
  • Добавляет кнопку “Скрыть выбранное”, которая скрывает все помеченные кольцами игры.
  • Процедура, как и в оригинальном скрипте, отправляет запрос на сервер, чтобы игры добавлялись в список игнорируемых вами игр и скрывались в дальнейшем.
  • Но теперь, вместо простого скрытия с глаз пользователя, игнорируемые игры полностью удаляются из кода страницы, что заметно уменьшает нагрузку и ускоряет взаимодействие.

ZP1aeC0.png

 

Скрытый текст


// ==UserScript==
// @name         Скрыватель игр Steam
// @namespace    steam-game-hider
// @version      2
// @description  Добавляет флажки к играм в магазине Steam и позволяет скрывать выбранные игры по нажатию кнопки.
// @author       0wn3df1x
// @match        https://store.steampowered.com/search/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        unsafeWindow
// ==/UserScript==

(function () {
    "use strict";

    // Добавляет флажки к каждой игре
    function addCheckboxes() {
        const gameLinks = document.querySelectorAll("a.search_result_row:not(.ds_ignored):not(.ds_excluded_by_preferences):not(.ds_wishlist):not(.ds_owned)");
        gameLinks.forEach(link => {
            if (link.querySelector(".my-checkbox")) return;
            const checkbox = document.createElement("input");
            checkbox.type = "checkbox";
            checkbox.className = "my-checkbox";
            checkbox.dataset.appid = link.dataset.dsAppid;
            link.insertBefore(checkbox, link.firstChild);

            checkbox.addEventListener("change", function () {
                link.style.background = this.checked ? "linear-gradient(to bottom, #381616, #5d1414)" : "";
            });
        });
    }

    // Скрывает выбранные игры
    function hideSelectedGames() {
        const checkboxes = document.querySelectorAll(".my-checkbox:checked");
        checkboxes.forEach(checkbox => {
            const link = document.querySelector(`a[data-ds-appid="${checkbox.dataset.appid}"]`);
            if (link) {
                link.classList.add("ds_ignored", "ds_flagged");
                checkbox.remove();
                jQuery.ajax({
                    url: "https://store.steampowered.com/recommended/ignorerecommendation/",
                    type: "POST",
                    data: {
                        sessionid: g_sessionID,
                        appid: checkbox.dataset.appid,
                        remove: 0,
                        snr: "1_account_notinterested_",
                    },
                    success: () => {
                        console.log(`Игра с appid ${checkbox.dataset.appid} добавлена в список игнорирования`);
                        GDynamicStore.InvalidateCache();
                    },
                });
            }
        });
    }

    // Удаляет все элементы с классами ds_ignored и ds_excluded_by_preferences
    function removeIgnoredGames() {
        const ignoredGames = document.querySelectorAll("a.search_result_row.ds_ignored, a.search_result_row.ds_excluded_by_preferences,a.search_result_row.ds_wishlist");
        ignoredGames.forEach(game => game.remove());
    }

    // Добавляем кнопку "Скрыть выбранное"
    const button = document.createElement("button");
    button.textContent = "Скрыть выбранное";
    button.addEventListener("click", hideSelectedGames);
    button.classList.add("my-button", "floating-button");
    document.body.appendChild(button);

    // Стилизуем кнопку и делаем её плавающей
    GM_addStyle(`
        .my-button {
            margin-right: 10px;
            padding: 10px 20px;
            border: none;
            border-radius: 50px;
            font-size: 16px;
            font-weight: 700;
            color: #fff;
            background: linear-gradient(to right, #16202D, #1B2838);
            box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2);
            cursor: pointer;
            font-family: "Roboto", sans-serif;
        }
        .my-button:hover {
            background: linear-gradient(to right, #0072ff, #00c6ff);
            box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3);
        }
        .floating-button {
            position: fixed;
            top: 20px;
            right: 20px;
            z-index: 1000;
        }
    `);

    // Стилизуем флажки
    GM_addStyle(`
        input[type=checkbox] {
            -webkit-appearance: none;
            -moz-appearance: none;
            appearance: none;
            border: 6px inset rgba(255, 0, 0, 0.8);
            border-radius: 50%;
            width: 42px;
            height: 42px;
            outline: none;
            transition: .15s ease-in-out;
            vertical-align: middle;
            position: absolute;
            left: 0px;
            top: 50%;
            transform: translateY(-50%);
            background-color: rgba(0, 0, 0, 0.0);
            box-shadow: inset 0 0 0 0 rgba(255, 255, 255, 0.5);
            cursor: pointer;
            z-index: 9999;
        }
        input[type=checkbox]:checked {
            background-color: rgba(0, 0, 0, 0.5);
            border-color: #b71c1c;
            box-shadow: inset 0 0 0 12px rgba(255, 0, 0, 0.5);
        }
        input[type=checkbox]:after {
            content: "";
            display: block;
            position: absolute;
            left: 50%;
            top: 50%;
            transform: translate(-50%, -50%) scale(0);
            width: 25px;
            height: 25px;
            border-radius: 50%;
            background-color: rgba(0, 0, 0, 0.9);
            opacity: 0.9;
            box-shadow: 0 0 0 0 #b71c1c;
            transition: transform .15s ease-in-out, box-shadow .15s ease-in-out;
        }
        input[type=checkbox]:checked:after {
            transform: translate(-50%, -50%) scale(1);
            box-shadow: 0 0 0 4px #b71c1c;
        }
    `);

    // Добавляем MutationObserver для обработки динамически подгружаемых элементов
    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            if (mutation.type === "childList" && mutation.addedNodes.length) {
                addCheckboxes();
                removeIgnoredGames();
            }
        });
    });

    observer.observe(document.body, { childList: true, subtree: true });

    // Инициализация флажков и удаление игр с классом ds_ignored при загрузке страницы
    addCheckboxes();
    removeIgnoredGames();
})();

 


P.S. Если вдруг вы захотите видеть скрытые игры — можно будет просто отключить скрипт.

 

 

Изменено пользователем 0wn3df1x
  • Лайк (+1) 1

Поделиться сообщением


Ссылка на сообщение

СписЖел Steam || Tamper Monkey

Обновление системы списков желаемого в Steam обеспечило головную боль для владельцев крупных списков желаемого, поскольку с новой системой резко сократилось количество игр в запросе к серверу, из-за чего выросло число самих запросов. Рост числа запросов напрягает стимовскую систему защиты от DDOS, в результате чего на пользователя вешается временная блокировка и в списке желаемого перестают показываться картинки и сами игры.

Данный скрипт используют новую систему, но при этом увеличивает количество игр в едином батче до 200, что кардинально сокращает количество запросов к серверу. С ним список желаемого на 1000 игр подразумевает всего 5 запросов, обеспечивая снятие напряжения с системы защиты. Минус — это не оригинальный список желаемого, а подобие его “зеркала” в новом окне: можно открывать игры по ссылкам, но удалять их, менять порядок и тому подобное — не получится. 

Выглядит вот так:
yTtvvc6.png

Игры упорядочены по технической дате выхода (указана в скобках) от старых к новым.
Если на игру есть скидка — блок подсвечивается жёлтым.

 

Скрытый текст

// ==UserScript==
// @name         СписЖел
// @namespace    https://store.steampowered.com/wishlist/
// @version      0.1
// @description  Смотрим списки желаемого по-другому
// @author       0wn3df1x
// @match        https://store.steampowered.com/wishlist/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const BATCH_SIZE = 200;

    // Соответствия между числовыми идентификаторами тегов и текстовыми значениями
    const tagMappings = {
        9: 'Strategy',
        19: 'Action',
        21: 'Adventure',
        84: 'Design & Illustration',
        87: 'Utilities',
        113: 'Free to Play',
        122: 'RPG',
        128: 'Massively Multiplayer',
        492: 'Indie',
        493: 'Early Access',
        597: 'Casual',
        599: 'Simulation',
        699: 'Racing',
        701: 'Sports',
        784: 'Video Production',
        809: 'Photo Editing',
        872: 'Animation & Modeling',
        1027: 'Audio Production',
        1036: 'Education',
        1038: 'Web Publishing',
        1445: 'Software Training',
        1616: 'Trains',
        1621: 'Music',
        1625: 'Platformer',
        1628: 'Metroidvania',
        1638: 'Dog',
        1643: 'Building',
        1644: 'Driving',
        1645: 'Tower Defense',
        1646: 'Hack and Slash',
        1647: 'Western',
        1649: 'GameMaker',
        1651: 'Satire',
        1654: 'Relaxing',
        1659: 'Zombies',
        1662: 'Survival',
        1663: 'FPS',
        1664: 'Puzzle',
        1665: 'Match 3',
        1666: 'Card Game',
        1667: 'Horror',
        1669: 'Moddable',
        1670: '4X',
        1671: 'Superhero',
        1673: 'Aliens',
        1674: 'Typing',
        1676: 'RTS',
        1677: 'Turn-Based',
        1678: 'War',
        1680: 'Heist',
        1681: 'Pirates',
        1684: 'Fantasy',
        1685: 'Co-op',
        1687: 'Stealth',
        1688: 'Ninja',
        1695: 'Open World',
        1697: 'Third Person',
        1698: 'Point & Click',
        1702: 'Crafting',
        1708: 'Tactical',
        1710: 'Surreal',
        1714: 'Psychedelic',
        1716: 'Rogue-like',
        1717: 'Hex Grid',
        1718: 'MOBA',
        1719: 'Comedy',
        1720: 'Dungeon Crawler',
        1721: 'Psychological Horror',
        1723: 'Action RTS',
        1730: 'Sokoban',
        1732: 'Voxel',
        1733: 'Unforgiving',
        1734: 'Fast-Paced',
        1736: 'LEGO',
        1738: 'Hidden Object',
        1741: 'Turn-Based Strategy',
        1742: 'Story Rich',
        1743: 'Fighting',
        1746: 'Basketball',
        1751: 'Comic Book',
        1752: 'Rhythm',
        1753: 'Skateboarding',
        1754: 'MMORPG',
        1755: 'Space',
        1756: 'Great Soundtrack',
        1759: 'Perma Death',
        1770: 'Board Game',
        1773: 'Arcade',
        1774: 'Shooter',
        1775: 'PvP',
        1777: 'Steampunk',
        3796: 'Based On A Novel',
        3798: 'Side Scroller',
        3799: 'Visual Novel',
        3810: 'Sandbox',
        3813: 'Real Time Tactics',
        3814: 'Third-Person Shooter',
        3834: 'Exploration',
        3835: 'Post-apocalyptic',
        3839: 'First-Person',
        3841: 'Local Co-Op',
        3843: 'Online Co-Op',
        3854: 'Lore-Rich',
        3859: 'Multiplayer',
        3871: '2D',
        3877: 'Precision Platformer',
        3878: 'Competitive',
        3916: 'Old School',
        3920: 'Cooking',
        3934: 'Immersive',
        3942: 'Sci-fi',
        3952: 'Gothic',
        3955: 'Character Action Game',
        3959: 'Rogue-lite',
        3964: 'Pixel Graphics',
        3968: 'Physics',
        3978: 'Survival Horror',
        3987: 'Historical',
        3993: 'Combat',
        4004: 'Retro',
        4018: 'Vampire',
        4026: 'Difficult',
        4036: 'Parkour',
        4046: 'Dragons',
        4057: 'Magic',
        4064: 'Thriller',
        4085: 'Anime',
        4094: 'Minimalist',
        4102: 'Combat Racing',
        4106: 'Action-Adventure',
        4115: 'Cyberpunk',
        4136: 'Funny',
        4137: 'Transhumanism',
        4145: 'Cinematic',
        4150: 'World War II',
        4155: 'Class-Based',
        4158: "Beat 'em up",
        4161: 'Real-Time',
        4166: 'Atmospheric',
        4168: 'Military',
        4172: 'Medieval',
        4175: 'Realistic',
        4182: 'Singleplayer',
        4184: 'Chess',
        4191: '3D',
        4195: 'Cartoony',
        4202: 'Trading',
        4231: 'Action RPG',
        4234: 'Short',
        4236: 'Loot',
        4242: 'Episodic',
        4252: 'Stylized',
        4255: "Shoot 'Em Up",
        4291: 'Spaceships',
        4295: 'Futuristic',
        4305: 'Colorful',
        4325: 'Turn-Based Combat',
        4328: 'City Builder',
        4342: 'Dark',
        4345: 'Gore',
        4364: 'Grand Strategy',
        4376: 'Assassin',
        4400: 'Abstract',
        4434: 'JRPG',
        4474: 'CRPG',
        4486: 'Choose Your Own Adventure',
        4508: 'Co-op Campaign',
        4520: 'Farming',
        4559: 'Quick-Time Events',
        4562: 'Cartoon',
        4598: 'Alternate History',
        4604: 'Dark Fantasy',
        4608: 'Swordplay',
        4637: 'Top-Down Shooter',
        4667: 'Violent',
        4684: 'Wargame',
        4695: 'Economy',
        4700: 'Movie',
        4711: 'Replay Value',
        4726: 'Cute',
        4736: '2D Fighter',
        4747: 'Character Customization',
        4754: 'Politics',
        4758: 'Twin Stick Shooter',
        4777: 'Spectacle fighter',
        4791: 'Top-Down',
        4821: 'Mechs',
        4835: '6DOF',
        4840: '4 Player Local',
        4845: 'Capitalism',
        4853: 'Political',
        4878: 'Parody',
        4885: 'Bullet Hell',
        4947: 'Romance',
        4975: '2.5D',
        4994: 'Naval Combat',
        5030: 'Dystopian',
        5055: 'eSports',
        5094: 'Narration',
        5125: 'Procedural Generation',
        5153: 'Kickstarter',
        5154: 'Score Attack',
        5160: 'Dinosaurs',
        5179: 'Cold War',
        5186: 'Psychological',
        5228: 'Blood',
        5230: 'Sequel',
        5300: 'God Game',
        5310: 'Games Workshop',
        5348: 'Mod',
        5350: 'Family Friendly',
        5363: 'Destruction',
        5372: 'Conspiracy',
        5379: '2D Platformer',
        5382: 'World War I',
        5390: 'Time Attack',
        5395: '3D Platformer',
        5407: 'Benchmark',
        5411: 'Beautiful',
        5432: 'Programming',
        5502: 'Hacking',
        5537: 'Puzzle-Platformer',
        5547: 'Arena Shooter',
        5577: 'RPGMaker',
        5608: 'Emotional',
        5611: 'Mature',
        5613: 'Detective',
        5652: 'Collectathon',
        5673: 'Modern',
        5708: 'Remake',
        5711: 'Team-Based',
        5716: 'Mystery',
        5727: 'Baseball',
        5752: 'Robots',
        5765: 'Gun Customization',
        5794: 'Science',
        5796: 'Bullet Time',
        5851: 'Isometric',
        5900: 'Walking Simulator',
        5914: 'Tennis',
        5923: 'Dark Humor',
        5941: 'Reboot',
        5981: 'Mining',
        5984: 'Drama',
        6041: 'Horses',
        6052: 'Noir',
        6129: 'Logic',
        6214: 'Birds',
        6276: 'Inventory Management',
        6310: 'Diplomacy',
        6378: 'Crime',
        6426: 'Choices Matter',
        6506: '3D Fighter',
        6621: 'Pinball',
        6625: 'Time Manipulation',
        6650: 'Nudity',
        6691: "1990's",
        6702: 'Mars',
        6730: 'PvE',
        6815: 'Hand-drawn',
        6869: 'Nonlinear',
        6910: 'Naval',
        6915: 'Martial Arts',
        6948: 'Rome',
        6971: 'Multiple Endings',
        7038: 'Golf',
        7107: 'Real-Time with Pause',
        7108: 'Party',
        7113: 'Crowdfunded',
        7178: 'Party Game',
        7208: 'Female Protagonist',
        7250: 'Linear',
        7309: 'Skiing',
        7328: 'Bowling',
        7332: 'Base Building',
        7368: 'Local Multiplayer',
        7423: 'Sniper',
        7432: 'Lovecraftian',
        7478: 'Illuminati',
        7481: 'Controller',
        7569: 'Grid-Based Movement',
        7622: 'Offroad',
        7702: 'Narrative',
        7743: '1980s',
        7918: 'Dwarf',
        7926: 'Artificial Intelligence',
        7948: 'Soundtrack',
        8013: 'Software',
        8075: 'TrackIR',
        8093: 'Minigames',
        8122: 'Level Editor',
        8253: 'Music-Based Procedural Generation',
        8369: 'Investigation',
        8461: 'Well-Written',
        8666: 'Runner',
        8945: 'Resource Management',
        9130: 'Hentai',
        9157: 'Underwater',
        9204: 'Immersive Sim',
        9271: 'Trading Card Game',
        9541: 'Demons',
        9551: 'Dating Sim',
        9564: 'Hunting',
        9592: 'Dynamic Narration',
        9803: 'Snow',
        9994: 'Experience',
        10235: 'Life Sim',
        10383: 'Transportation',
        10397: 'Memes',
        10437: 'Trivia',
        10679: 'Time Travel',
        10695: 'Party-Based RPG',
        10808: 'Supernatural',
        10816: 'Split Screen',
        11014: 'Interactive Fiction',
        11095: 'Boss Rush',
        11104: 'Vehicular Combat',
        11123: 'Mouse only',
        11333: 'Villain Protagonist',
        11634: 'Vikings',
        12057: 'Tutorial',
        12095: 'Sexual Content',
        12190: 'Boxing',
        12286: 'Warhammer 40K',
        12472: 'Management',
        13070: 'Solitaire',
        13190: 'America',
        13276: 'Tanks',
        13382: 'Archery',
        13577: 'Sailing',
        13782: 'Experimental',
        13906: 'Game Development',
        14139: 'Turn-Based Tactics',
        14153: 'Dungeons & Dragons',
        14720: 'Nostalgia',
        14906: 'Intentionally Awkward Controls',
        15045: 'Flight',
        15172: 'Conversation',
        15277: 'Philosophical',
        15339: 'Documentary',
        15564: 'Fishing',
        15868: 'Motocross',
        15954: 'Silent Protagonist',
        16094: 'Mythology',
        16250: 'Gambling',
        16598: 'Space Sim',
        16689: 'Time Management',
        17015: 'Werewolves',
        17305: 'Strategy RPG',
        17337: 'Lemmings',
        17389: 'Tabletop',
        17770: 'Asynchronous Multiplayer',
        17894: 'Cats',
        17927: 'Pool',
        18594: 'FMV',
        19568: 'Cycling',
        19780: 'Submarine',
        19995: 'Dark Comedy',
        21006: 'Underground',
        21725: 'Tactical RPG',
        21978: 'VR',
        22602: 'Agriculture',
        22955: 'Mini Golf',
        24003: 'Word Game',
        24904: 'NSFW',
        25085: 'Touch-Friendly',
        26921: 'Political Sim',
        27758: 'Voice Control',
        28444: 'Snowboarding',
        29363: '3D Vision',
        29482: 'Souls-like',
        29855: 'Ambient',
        30358: 'Nature',
        30927: 'Fox',
        31275: 'Text-Based',
        31579: 'Otome',
        32322: 'Deckbuilding',
        33572: 'Mahjong',
        35079: 'Job Simulator',
        42089: 'Jump Scare',
        42329: 'Coding',
        42804: 'Action Roguelike',
        44868: 'LGBTQ+',
        47827: 'Wrestling',
        49213: 'Rugby',
        51306: 'Foreign',
        56690: 'On-Rails Shooter',
        61357: 'Electronic Music',
        65443: 'Adult Content',
        71389: 'Spelling',
        87918: 'Farming Sim',
        91114: 'Shop Keeper',
        92092: 'Jet',
        96359: 'Skating',
        97376: 'Cozy',
        102530: 'Elf',
        117648: '8-bit Music',
        123332: 'Bikes',
        129761: 'ATV',
        143739: 'Electronic',
        150626: 'Gaming',
        158638: 'Cricket',
        176733: 'Tile-Matching',
        176981: 'Battle Royale',
        180368: 'Faith',
        189941: 'Instrumental Music',
        198631: 'Mystery Dungeon',
        198913: 'Motorbike',
        220585: 'Colony Sim',
        233824: 'Feature Film',
        252854: 'BMX',
        255534: 'Automation',
        323922: 'Musou',
        324176: 'Hockey',
        337964: 'Rock Music',
        348922: 'Steam Machine',
        353880: 'Looter Shooter',
        363767: 'Snooker',
        379975: 'Clicker',
        454187: 'Traditional Roguelike',
        552282: 'Wholesome',
        603297: 'Hardware',
        615955: 'Idler',
        620519: 'Hero Shooter',
        745697: 'Social Deduction',
        769306: 'Escape Room',
        776177: '360 Video',
        791774: 'Card Battler',
        847164: 'Volleyball',
        856791: 'Asymmetric VR',
        916648: 'Creature Collector',
        922563: 'Roguevania',
        1003823: 'Profile Features Limited',
        1023537: 'Boomer Shooter',
        1084988: 'Auto Battler',
        1091588: 'Roguelike Deckbuilder',
        1100686: 'Outbreak Sim',
        1100687: 'Automobile Sim',
        1100688: 'Medical Sim',
        1100689: 'Open World Survival Craft',
        1199779: 'Extraction Shooter',
        1220528: 'Hobby Sim',
        1254546: 'Football (Soccer)',
        1254552: 'Football (American)',
        // Добавьте остальные соответствия здесь
    };

    // Добавляем кнопку на страницу
    setTimeout(() => {
        const button = document.createElement('button');
        button.innerText = 'СписЖел';
        button.style.marginLeft = '10px';
        button.style.backgroundColor = '#66c0f4';
        button.style.color = '#fff';
        button.style.border = 'none';
        button.style.padding = '10px 20px';
        button.style.borderRadius = '4px';
        button.style.cursor = 'pointer';
        button.style.transition = 'background-color 0.3s ease, transform 0.3s ease';
        button.onclick = openModal;
        document.querySelector('h2').appendChild(button);
    }, 1000);

    function openModal() {
        const modalOverlay = document.createElement('div');
        modalOverlay.className = 'FullModalOverlay';
        modalOverlay.style.display = 'block';
        modalOverlay.style.position = 'fixed';
        modalOverlay.style.top = '0';
        modalOverlay.style.left = '0';
        modalOverlay.style.width = '100%';
        modalOverlay.style.height = '100%';
        modalOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
        modalOverlay.style.zIndex = '1000';

        const modalContent = document.createElement('div');
        modalContent.className = 'ModalOverlayContent';
        modalContent.style.position = 'fixed';
        modalContent.style.top = '50%';
        modalContent.style.left = '50%';
        modalContent.style.transform = 'translate(-50%, -50%)';
        modalContent.style.backgroundColor = '#1b2838';
        modalContent.style.padding = '20px';
        modalContent.style.borderRadius = '8px';
        modalContent.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.2)';
        modalContent.style.zIndex = '1001';

        const closeButton = document.createElement('button');
        closeButton.innerText = '×';
        closeButton.style.position = 'absolute';
        closeButton.style.top = '10px';
        closeButton.style.right = '10px';
        closeButton.style.backgroundColor = 'transparent';
        closeButton.style.border = 'none';
        closeButton.style.color = '#c7d5e0';
        closeButton.style.fontSize = '20px';
        closeButton.style.cursor = 'pointer';
        closeButton.onclick = () => modalOverlay.remove();

        const input = document.createElement('input');
        input.type = 'text';
        input.placeholder = 'Enter Steam ID';
        input.style.marginBottom = '10px';
        input.style.width = '100%';
        input.style.padding = '10px';
        input.style.borderRadius = '4px';
        input.style.border = '1px solid #0e141b';
        input.style.backgroundColor = '#16202d';
        input.style.color = '#c7d5e0';

        const fetchButton = document.createElement('button');
        fetchButton.innerText = 'Получить СписЖел';
        fetchButton.style.padding = '10px';
        fetchButton.style.borderRadius = '4px';
        fetchButton.style.border = 'none';
        fetchButton.style.backgroundColor = '#66c0f4';
        fetchButton.style.color = '#fff';
        fetchButton.style.cursor = 'pointer';
        fetchButton.onclick = () => {
            fetchWishlist(input.value).then(() => fetchGameDetails());
            modalOverlay.remove();
        };

        modalContent.appendChild(closeButton);
        modalContent.appendChild(input);
        modalContent.appendChild(fetchButton);
        modalOverlay.appendChild(modalContent);
        document.body.appendChild(modalOverlay);
    }

    async function fetchWishlist(steamId) {
        if (!steamId) {
            alert('Please enter a Steam ID');
            return;
        }

        // Удаляем старый wshlstinf0
        localStorage.removeItem('wshlstinf0');

        const wishlistUrl = `https://api.steampowered.com/IWishlistService/GetWishlist/v1/?steamid=${steamId}`;
        const response = await fetch(wishlistUrl);
        const data = await response.json();

        if (data.response && data.response.items) {
            const wishlistItems = data.response.items;
            const wishlistData = wishlistItems.map(item => ({
                appid: item.appid,
                date_added: item.date_added
            }));
            localStorage.setItem('wshlstinf0', JSON.stringify(wishlistData));
            alert('Данные списка желаемого сохранены в local storage');
        } else {
            alert('А где эт самое?');
        }
    }

    async function fetchGameDetails() {
        const wishlistData = JSON.parse(localStorage.getItem('wshlstinf0'));
        if (!wishlistData) {
            alert('Чот в local storage нет ничего');
            return;
        }

        const appIds = wishlistData.map(item => item.appid);
        const batches = [];
        for (let i = 0; i < appIds.length; i += BATCH_SIZE) {
            batches.push(appIds.slice(i, i + BATCH_SIZE));
        }

        const results = [];
        for (const batch of batches) {
            const batchResults = await fetchGameDetailsBatch(batch);
            results.push(...batchResults);
        }

        openResultsInNewWindow(results, wishlistData);

        // Удаляем wshlstinf0 после использования
        localStorage.removeItem('wshlstinf0');
    }

    async function fetchGameDetailsBatch(appIds) {
        const gameDetailsUrl = `https://api.steampowered.com/IStoreBrowseService/GetItems/v1?input_json=${encodeURIComponent(JSON.stringify({
            ids: appIds.map(appid => ({ appid })),
            context: {
                language: 'english',
                country_code: 'RU', //Смена валюты. Можно заменить на US или KZ
                steam_realm: 1
            },
            data_request: {
                include_release: true,
                include_tag_count: true
            }
        }))}`;

        const response = await fetch(gameDetailsUrl);
        const data = await response.json();

        if (data.response && data.response.store_items) {
            return data.response.store_items.map(item => {
                const release = item.release || {};
                const releaseDate = release.steam_release_date || release.custom_release_date_message || 'N/A';
                const bestPurchaseOption = item.best_purchase_option || {};
                return {
                    appid: item.appid,
                    name: item.name || 'N/A',
                    tagids: item.tagids ? item.tagids.join(',') : 'N/A',
                    releaseDate: releaseDate,
                    comingSoonDisplay: release.coming_soon_display || 'date_full',
                    finalPrice: bestPurchaseOption.formatted_final_price || 'N/A',
                    originalPrice: bestPurchaseOption.formatted_original_price || 'N/A',
                    discountPercentage: bestPurchaseOption.discount_pct || 'N/A'
                };
            });
        } else {
            return [];
        }
    }

    function formatDate(timestamp) {
        const date = new Date(timestamp * 1000);
        const day = String(date.getDate()).padStart(2, '0');
        const month = String(date.getMonth() + 1).padStart(2, '0');
        const year = date.getFullYear();
        return `${day}.${month}.${year}`;
    }

    function formatDateTime(timestamp) {
        const date = new Date(timestamp * 1000);
        const day = String(date.getDate()).padStart(2, '0');
        const month = String(date.getMonth() + 1).padStart(2, '0');
        const year = date.getFullYear();
        const hours = String(date.getHours()).padStart(2, '0');
        const minutes = String(date.getMinutes()).padStart(2, '0');
        const seconds = String(date.getSeconds()).padStart(2, '0');
        return `${day}.${month}.${year} ${hours}:${minutes}:${seconds}`;
    }

    function formatDisplayedDate(releaseDate, comingSoonDisplay) {
        if (isNaN(releaseDate)) return releaseDate;

        const date = new Date(releaseDate * 1000);
        const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
        const year = date.getFullYear();
        const month = monthNames[date.getMonth()];
        const quarter = Math.floor(date.getMonth() / 3) + 1;

        switch (comingSoonDisplay) {
            case 'date_month':
                return `${month} ${year}`;
            case 'date_quarter':
                return `Q${quarter} ${year}`;
            case 'date_year':
                return `${year}`;
            default:
                return formatDate(releaseDate);
        }
    }

    function formatTechReleaseDate(releaseDate) {
        if (releaseDate === 'Coming soon' || releaseDate === 'To be announced') {
            return '31.12.2123';
        }
        if (releaseDate === 'N/A') {
            return '31.12.2999';
        }
        return isNaN(releaseDate) ? releaseDate : formatDate(releaseDate);
    }

    function replaceTagIdsWithNames(tagids) {
        if (!tagids) return 'N/A';
        const tags = tagids.split(',').map(tagId => tagMappings[tagId] || tagId);
        return tags.join(', ');
    }

    function openResultsInNewWindow(results, wishlistData) {
        const newWindow = window.open('', '_blank');
        newWindow.document.write(`
            <html>
            <head>
                <title>СписЖел</title>
                <style>
                    body {
                        font-family: Arial, sans-serif;
                        background-color: #1b2838;
                        color: #acb2b8;
                        margin: 0;
                        padding: 20px;
                    }
                    .container {
                        display: flex;
                        flex-wrap: wrap;
                        gap: 20px;
                    }
                    .game-card {
                        background-color: #16202d;
                        border: 1px solid #0e141b;
                        border-radius: 8px;
                        padding: 15px;
                        width: 300px;
                        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
                        display: flex;
                        flex-direction: column;
                        justify-content: space-between;
                        transition: transform 0.3s ease, box-shadow 0.3s ease;
                    }
                    .game-card:hover {
                        transform: translateY(-5px);
                        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
                    }
                    .game-card img {
                        width: 100%;
                        height: auto;
                        border-radius: 8px;
                        transition: transform 0.3s ease;
                    }
                    .game-card:hover img {
                        transform: scale(1.05);
                    }
                    .game-card h2 {
                        margin: 10px 0;
                        font-size: 18px;
                        color: #c7d5e0;
                        transition: color 0.3s ease;
                    }
                    .game-card h2:hover {
                        color: #66c0f4;
                    }
                    .game-card .tags {
                        display: flex;
                        flex-wrap: wrap;
                        gap: 5px;
                        margin-bottom: 10px;
                    }
                    .game-card .tags span {
                        background-color: #0e141b;
                        padding: 3px 8px;
                        border-radius: 4px;
                        font-size: 12px;
                        color: #acb2b8;
                        transition: background-color 0.3s ease, color 0.3s ease;
                    }
                    .game-card .tags span:hover {
                        background-color: #66c0f4;
                        color: #fff;
                    }
                    .game-card .price-info {
                        display: flex;
                        justify-content: space-between;
                        align-items: center;
                        margin-top: 10px;
                    }
                    .game-card .price-info .original-price {
                        text-decoration: line-through;
                        font-size: 14px;
                        color: #888;
                        transition: color 0.3s ease;
                    }
                    .game-card .price-info .original-price:hover {
                        color: #ff4500;
                    }
                    .game-card .price-info .discount-percentage {
                        background-color: #4c6b22;
                        color: #fff;
                        padding: 3px 8px;
                        border-radius: 4px;
                        font-size: 16px;
                        transition: background-color 0.3s ease;
                    }
                    .game-card .price-info .discount-percentage:hover {
                        background-color: #66c0f4;
                    }
                    .game-card .date-added {
                        text-align: right;
                        font-size: 12px;
                        color: #888;
                        margin-top: 10px;
                        transition: color 0.3s ease;
                    }
                    .game-card .date-added:hover {
                        color: #66c0f4;
                    }
                    .game-card.discounted {
                        border: 2px solid #ffd700; /* Золотистый цвет */
                        position: relative;
                    }
                    .game-card.discounted::before {
                        content: '';
                        position: absolute;
                        top: -2px;
                        left: -2px;
                        right: -2px;
                        bottom: -2px;
                        border: 2px solid #ffd700; /* Золотистый цвет */
                        border-radius: 8px;
                        animation: pulse 2s infinite;
                    }
                    @keyframes pulse {
                        0% {
                            box-shadow: 0 0 0 0 rgba(255, 215, 0, 0.7); /* Золотистый цвет */
                        }
                        70% {
                            box-shadow: 0 0 0 10px rgba(255, 215, 0, 0); /* Золотистый цвет */
                        }
                        100% {
                            box-shadow: 0 0 0 0 rgba(255, 215, 0, 0); /* Золотистый цвет */
                        }
                    }
                    /* Стили для кнопки */
                    button {
                        background-color: #66c0f4;
                        color: #fff;
                        border: none;
                        padding: 10px 20px;
                        border-radius: 4px;
                        cursor: pointer;
                        transition: background-color 0.3s ease;
                    }
                    button:hover {
                        background-color: #4b8ac9;
                    }
                    /* Стили для модального окна */
                    .FullModalOverlay {
                        display: none;
                        position: fixed;
                        top: 0;
                        left: 0;
                        width: 100%;
                        height: 100%;
                        background-color: rgba(0, 0, 0, 0.5);
                        z-index: 1000;
                    }
                    .ModalOverlayContent {
                        position: fixed;
                        top: 50%;
                        left: 50%;
                        transform: translate(-50%, -50%);
                        background-color: #1b2838;
                        padding: 20px;
                        border-radius: 8px;
                        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
                        z-index: 1001;
                    }
                    .ModalOverlayContent input {
                        width: 100%;
                        padding: 10px;
                        margin-bottom: 10px;
                        border-radius: 4px;
                        border: 1px solid #0e141b;
                        background-color: #16202d;
                        color: #c7d5e0;
                    }
                    .ModalOverlayContent button {
                        margin-right: 10px;
                    }
                    .ModalOverlayContent button:last-child {
                        margin-right: 0;
                    }
                    .ModalOverlayContent button.close {
                        position: absolute;
                        top: 10px;
                        right: 10px;
                        background-color: transparent;
                        border: none;
                        color: #c7d5e0;
                        font-size: 20px;
                        cursor: pointer;
                    }
                </style>
            </head>
            <body>
                <h1 style="color: #c7d5e0;">СписЖел</h1>
                <div class="container">
        `);

        // Сортировка результатов по технической дате
        results.sort((a, b) => {
            const dateA = new Date(formatTechReleaseDate(a.releaseDate).split('.').reverse().join('-'));
            const dateB = new Date(formatTechReleaseDate(b.releaseDate).split('.').reverse().join('-'));
            return dateA - dateB;
        });

        results.forEach(result => {
            const dateAdded = wishlistData.find(item => item.appid === result.appid).date_added;
            const formattedDateAdded = formatDateTime(dateAdded);
            const formattedTechReleaseDate = formatTechReleaseDate(result.releaseDate);
            const formattedDisplayedReleaseDate = formatDisplayedDate(result.releaseDate, result.comingSoonDisplay);
            const imageUrl = `https://shared.fastly.steamstatic.com/store_item_assets/steam/apps/${result.appid}/header_292x136.jpg`;
            const gameUrl = `https://store.steampowered.com/app/${result.appid}`;
            const tagNames = replaceTagIdsWithNames(result.tagids);

            const cardClass = result.discountPercentage !== 'N/A' ? 'game-card discounted' : 'game-card';

            newWindow.document.write(`
                <div class="${cardClass}">
                    <a href="${gameUrl}" target="_blank">
                        <img src="${imageUrl}" alt="Game Image">
                        <h2>${result.name}</h2>
                    </a>
                    <div class="tags">
                        ${tagNames.split(', ').map(tag => `<span>${tag}</span>`).join('')}
                    </div>
                    <div>
                        <strong>Release Date:</strong> ${formattedDisplayedReleaseDate} (${formattedTechReleaseDate})
                    </div>
                    <div class="price-info">
                        <div>
                            ${result.originalPrice !== 'N/A' ? `<span class="original-price">${result.originalPrice}</span>` : ''}
                            <span>${result.finalPrice}</span>
                        </div>
                        ${result.discountPercentage !== 'N/A' ? `<span class="discount-percentage">${result.discountPercentage}%</span>` : ''}
                    </div>
                    <div class="date-added">
                        Added: ${formattedDateAdded}
                    </div>
                </div>
            `);
        });

        newWindow.document.write(`
                </div>
            </body>
            </html>
        `);
        newWindow.document.close();
    }
})();

 

Скрытый текст
  1. Сперва вам придётся узнать ваш или чужой Steam ID, выглядит он вот так:
    https://steamcommunity.com/profiles/76561198015968954
    Нас интересуют только цифры: 76561198015968954
  2. В списке желаемого появится кнопка СписЖел: 
    GUn6a89.png
  3. При её нажатии появится модальное окно:
    HQmQDxp.png
    Вбиваем цифры и жмём “Получить СписЖел”.
     
  4. Жмём ОК
    r1P47TK.png
     
  5. Готово:
    JyfME5L.png

P.S. С крупными списками желаемого ожидание может занять какое-то время. 700 игр грузятся секунд за 7.  Тут важно понимать, что получается огромный объём данных.


 

Изменено пользователем 0wn3df1x
  • Лайк (+1) 1

Поделиться сообщением


Ссылка на сообщение
1 час назад, 0wn3df1x сказал:

Steam Reviews Extender || Tamper Monkey

Система обзоров в Steam проверена временем, но бывают ситуации, в которых она создаёт искажённое представление о продукте.

К примеру, иногда подрядчик, осуществляющий локализацию на китайский язык, делает свою работу недобросовестно. И в этом случае игроки из Китая закономерно оставляют отрицательные обзоры. Китайская популяция огромна и настолько же огромная китайская аудитория в Steam. Отрицательные обзоры от настолько огромной аудитории могут уронить рейтинг любого проекта. Как результат — игра хорошая, а обзоры к ней смешанные. И многие потребители, видя смешанные обзоры, просто закрывают страницу. Обратная ситуация связана с играми, ориентированными на китайский рынок. Вы заходите на их страницу, видите огромное количество положительных обзоров, но без изучения этих обзоров вы не сможете понять, что большинство из них оставили игроки из Китая и игра интересна преимущественно китайским пользователям.

Ещё одна проблема — во избежание злоупотреблений со стороны издателей и разработчиков, система обзоров не учитывает обзоры активировавших игру ключом. Но ведь иногда активировать игру ключом дешевле. Плюс, учитывая ситуацию последних лет, ключи остаются одним из редких вариантов активации определённых игр в России.

Третья проблема — отсутствие статистики по обзорам на языке, который использует потребитель, хотя они важны.
К примеру:

  • Иногда те же отечественные игры непонятны покупателям вне стран СНГ, либо в этих игра плохая локализация на какой-то из европейских языков. И в таком случае иностранцы оставляют отрицательные обзоры, в то время как русскоязычные в восторге от игры.
  • Это же касается и жанровых предпочтений. Ряд игр нравится русскоязычным больше, чем игрокам из Азии, Европы или США. 
  • Либо наоборот — игра имеет хорошие обзоры, но в ней просто отвратительная русская локализация. Тем не менее, количество русских обзоров не настолько велико, чтобы повлиять на рейтинг игры. И без изучения обзоров вы не сразу поймёте, что с игрой что-то не так.

Мой скрипт призван решить все перечисленные проблемы.
Поэтому он добавляет три новых вида обзоров в описание игр:

  • Тотальные обзоры — Все обзоры с учётом активаций ключами.
  • Безкитайские обзоры — Все обзоры с вычетом китайских.
  • Русские обзоры — Только русские обзоры.

spkDPBA.png

Если какие-то из обзоров вам не нужны, то вы можете заменить True на False в 17-19 строках кода.


    // Настройки для включения/отключения обзоров
    const settings = {
        showTotalReviews: true, // Показывать тотальные обзоры
        showNonChineseReviews: true, // Показывать безкитайские обзоры
        showRussianReviews: true // Показывать русские обзоры
    };

 

  Код скрипта (Показать содержимое)

 

Это имба, страшно удобно.

  • Лайк (+1) 1

Поделиться сообщением


Ссылка на сообщение

Stelicas || Electron

Stelicas (Steam Library Categories Scraper) - инструмент для извлечения категорий вашей библиотеки Steam, получения подробной информации об играх (включая теги, даты выпуска, отзывы и многое другое) и экспорта данных в структурированный CSV-формат для удобной организации и анализа.

Репозиторий на GitHub

yt1P1R0.png

Введение
Раньше категории библиотеки Steam можно было легко извлечь из файла sharedconfig.vdf. Однако с последними обновлениями клиента Steam этот файл устарел и больше не отражает текущее состояние вашей библиотеки. Самая актуальная информация теперь хранится в базе данных leveldb, которая является частью локального хранилища клиента Steam. Stelicas разработан для доступа к этой базе данных, извлечения ваших категорий библиотеки и обогащения данных подробной информацией об играх из Steam API.

Скрытый текст

Особенности

  • Извлечение категорий библиотеки Steam: Извлекайте ваши пользовательские категории библиотеки Steam напрямую из локальной базы данных.
  • Получение подробной информации об играх: Получайте подробную информацию об играх, включая:
    • Название игры
    • Тип игры
    • Теги
    • Дата выпуска
    • Оценки и количество отзывов
    • Издатели, разработчики и франшизы
    • И многое другое
  • Экспорт в CSV: Сохраняйте извлеченные данные в структурированном CSV-формате для удобного анализа и организации.
  • Удобный интерфейс: Простой и интуитивно понятный интерфейс, который проведет вас через процесс.
Скрытый текст

Как это работает
Stelicas работает, получая доступ к базе данных leveldb, расположенной в локальном хранилище клиента Steam. Он читает записи базы данных, чтобы извлечь ваши пользовательские категории библиотеки и связанные с ними идентификаторы игр. Затем он использует Steam API для получения подробной информации о каждой игре. Наконец, он компилирует все данные в CSV-файл для удобного доступа и анализа.

Во время процесса сбора данных клиент Steam должен быть закрыт. Stelicas включает встроенную проверку, чтобы убедиться, что Steam не запущен, прежде чем продолжить. Если Steam обнаружен как запущенный, инструмент откажется собирать данные, чтобы предотвратить конфликты.

Скрытый текст

Требования
Перед использованием Stelicas убедитесь, что на вашей системе установлено следующее:

  • Node.js (v16 или выше)
  • npm (обычно поставляется с Node.js)
  • Клиент Steam (установлен и выполнен вход)
Скрытый текст

Установка

В репозитории есть релиз. С ним можно сразу переходить к использованию.

Без него:
1. Клонируйте репозиторий:   


git clone https://github.com/0wn3dg0d/stelicas.git
		   cd stelicas

2. Установите зависимости:   


npm install

3. Запустите приложение:   


npm start
Скрытый текст

Использование

1. Запустите Stelicas:
   После запуска Stelicas.exe или выполнения npm start откроется окно приложения Stelicas.

2. Введите ваш Account ID (Steam 3 ID):
   - Ваш Steam3 ID - это уникальный идентификатор вашего аккаунта Steam. Обычно он имеет формат `U:1:XXXXXXX` (используйте часть `XXXXXXX`).
   - Этот ID соответствует имени папки в вашем каталоге Steam `userdata`, который обычно находится по адресу:     


{Steam folder}\userdata

Каждая папка в этом каталоге представляет собой другой аккаунт Steam, и имя папки - это часть `XXXXXXX` вашего Steam3 ID.
   - Если вы не знаете свой Steam3 ID, Stelicas может автоматически обнаружить его из вашей установки Steam. Нажмите на любой из обнаруженных ID, чтобы заполнить поле.

0WOrDBj.png

3. Выберите язык, валюту страны и поддерживаемый язык:
   - Язык: Выберите язык, на котором вы хотите получить информацию об играх (например, описания, названия). Если игра имеет перевод для выбранного языка, данные будут отображаться на этом языке.
   - Валюта страны: Выберите валюту страны. Рекомендуется оставить это как US Dollar, так как игры в других валютах могут быть недоступны в определенных регионах, что может привести к отсутствию данных.
   - Поддерживаемый язык: Этот параметр указывает, поддерживает ли игра выбранный язык. Результат отображается в формате {true;false;true} и т.д., где:

  • Первое значение указывает, есть ли у игры перевод интерфейса.
  • Второе значение указывает, есть ли у игры полный аудиоперевод.
  • Третье значение указывает, есть ли у игры перевод субтитров.
  • Если результат `{TRUE}`, это означает, что игра полностью поддерживает выбранный язык (все значения `true`).
  • Если результат `{FALSE}`, это означает, что игра не поддерживает выбранный язык вообще (все значения `false`).

JgPebA6.png

4. Запустите процесс:
   - Нажмите кнопку Start, чтобы начать извлечение категорий вашей библиотеки и получение информации об играх.
   - Прогресс-бар покажет статус процесса.

5. Просмотрите выходные данные:
   - После завершения процесса данные будут сохранены в папке output.

Скрытый текст

Выходные данные
Выходные данные состоят из двух CSV-файлов:

1. `id_categories.csv`: Этот файл содержит сопоставление идентификаторов игр с их связанными категориями. Он полезен, если вам нужна только информация о категориях без дополнительных данных об играх.

   Пример id_categories.csv:
Kbl8ZXF.png

2. `final_data.csv`: Этот файл содержит полный набор данных, включая подробную информацию об играх, такую как название, теги, дата выпуска, отзывы и многое другое. Ниже приведено подробное описание столбцов в этом файле.

Столбцы в final_data.csv

  • game_id: Уникальный идентификатор Steam игры.
  • name: Название игры.
  • categories: Пользовательские категории, которые вы назначили игре в вашей библиотеке Steam.
  • type: Тип игры (например, игра, DLC и т.д.).
  • tags: Теги, связанные с игрой.
  • release_date: Дата выпуска игры.
  • review_percentage: Процент положительных отзывов.
  • review_count: Общее количество отзывов.
  • is_free: Указывает, является ли игра бесплатной.
  • is_early_access: Указывает, находится ли игра в раннем доступе.
  • publishers: Издатели игры.
  • developers: Разработчики игры.
  • franchises: Франшизы, связанные с игрой.
  • short_description: Краткое описание игры.
  • supported_language: Указывает, поддерживает ли игра выбранный вами язык.
  • Steam-Link: Ссылка на страницу игры в Steam Community (не в магазине Steam). Это сделано намеренно, так как некоторые игры могут быть удалены из магазина Steam, но их страницы всё ещё доступны через Community. Использование ссылки на Community гарантирует, что вы сможете перейти на страницу любой игры, даже если она больше не доступна в магазине.
  • Pic: Ссылка на изображение заголовка игры.

Пример выходных данных:

14Ms9Je.png

Скрытый текст

FAQ

  • Почему Stelicas требует, чтобы клиент Steam был закрыт?

Клиент Steam блокирует базу данных `leveldb`, пока он запущен. Чтобы получить доступ к базе данных и извлечь ваши категории библиотеки, клиент Steam должен быть закрыт. Stelicas включает встроенную проверку, чтобы убедиться, что Steam не запущен, прежде чем продолжить.

  • Будет ли Stelicas работать, если на компьютере несколько аккаунтов Steam?

В настоящее время этот сценарий не тестировался. Теоретически, Stelicas должен работать с несколькими аккаунтами, так как он полагается на Steam3 ID для идентификации правильных пользовательских данных. Однако, поскольку это не проверено на практике, я не могу гарантировать его функциональность в таких случаях. Если у вас несколько аккаунтов, я рекомендую протестировать и предоставить отзыв.

  • Почему игры без категорий не включены в выходные данные?

Stelicas разработан для извлечения и организации игр на основе категорий, которые вы назначили в вашей библиотеке Steam. Если игра не назначена ни к одной категории, она не появится в выходных данных. Это связано с тем, что инструмент сосредоточен на категоризации и анализе игр, которые были явно организованы пользователем. Если вы хотите включить игры без категорий, вы можете вручную назначить их категории в вашей библиотеке Steam и перезапустить инструмент.

  • Почему изменения в категориях игр не сразу появляются в выходных данных?

База данных Steam `leveldb`, которую использует Stelicas для извлечения информации о категориях, может не сразу отражать изменения, внесенные в ваши категории библиотеки. Это может происходить по нескольким причинам:

  • Кэширование базы данных: Steam может кэшировать данные категорий для оптимизации производительности, что означает, что изменения могут быть записаны в базу данных не сразу.
  • Задержка синхронизации: Steam может задерживать синхронизацию изменений с локальной базой данных, особенно если клиент находится под высокой нагрузкой или если есть задержки, связанные с сетью.
  • Исторические данные: База данных `leveldb` может сохранять исторические или избыточные данные в течение определенного периода, даже после удаления или изменения категорий.

Хотя такое поведение может быть разочаровывающим, важно отметить, что база данных leveldb по-прежнему является самым актуальным и надежным источником информации о категориях по сравнению с устаревшим файлом sharedconfig.vdf. Если изменения не появляются сразу, попробуйте перезапустить Steam или подождать несколько минут перед повторным запуском Stelicas.

Если проблема сохраняется, это может быть связано с тем, как Steam внутренне управляет своей базой данных, и мало что можно сделать со стороны инструмента.

 

Изменено пользователем 0wn3df1x
  • Лайк (+1) 1

Поделиться сообщением


Ссылка на сообщение

Обновление: Steam Reviews Extender & Ru Indicator  || Tamper Monkey

Суть обновления:

  • Добавлены индикаторы наличия русского языка.
    Сейчас, чтобы узнать о наличии текстового перевода или озвучки приходится листать страницу игры вниз. Если вы открываете много страниц — этот скроллинг начинает надоедать. Поэтому я решил добавить индикаторы на видное место. Если в игре есть какой-то вид перевода, то индикатор горит ярко. Если нет, то индикатор тёмный. Всё интуитивно понятно. А если нет — при наведении курсора на любой индикатор всплывет подсказка. Порядок:
    • Интерфейс
    • Озвучка
    • Субтитры
       
  • Добавлена кнопка “Загрузить” доп. обзоры.
    В прошлой версии скрипта была проблема: когда вы заходили на страницу любой игры, скрипт отправлял по 3 запроса на получение дополнительных обзоров независимо от того, нужны они вам или нет. Теперь скрипт показывает обзоры только тогда, когда вы этого хотите — достаточно нажать кнопку “загрузить” и он выведет все три дополнительные категории.
     

dgrLRAW.png
 

Скрытый текст

Описание:
Система обзоров в Steam проверена временем, но бывают ситуации, в которых она создаёт искажённое представление о продукте.

К примеру, иногда подрядчик, осуществляющий локализацию на китайский язык, делает свою работу недобросовестно. И в этом случае игроки из Китая закономерно оставляют отрицательные обзоры. Китайская популяция огромна и настолько же огромная китайская аудитория в Steam. Отрицательные обзоры от настолько огромной аудитории могут уронить рейтинг любого проекта. Как результат — игра хорошая, а обзоры к ней смешанные. И многие потребители, видя смешанные обзоры, просто закрывают страницу. Обратная ситуация связана с играми, ориентированными на китайский рынок. Вы заходите на их страницу, видите огромное количество положительных обзоров, но без изучения этих обзоров вы не сможете понять, что большинство из них оставили игроки из Китая и игра интересна преимущественно китайским пользователям.

Ещё одна проблема — во избежание злоупотреблений со стороны издателей и разработчиков, система обзоров не учитывает обзоры активировавших игру ключом. Но ведь иногда активировать игру ключом дешевле. Плюс, учитывая ситуацию последних лет, ключи остаются одним из редких вариантов активации определённых игр в России.

Третья проблема — отсутствие статистики по обзорам на языке, который использует потребитель, хотя они важны.
К примеру:

  • Иногда те же отечественные игры непонятны покупателям вне стран СНГ, либо в этих игра плохая локализация на какой-то из европейских языков. И в таком случае иностранцы оставляют отрицательные обзоры, в то время как русскоязычные в восторге от игры.
  • Это же касается и жанровых предпочтений. Ряд игр нравится русскоязычным больше, чем игрокам из Азии, Европы или США. 
  • Либо наоборот — игра имеет хорошие обзоры, но в ней просто отвратительная русская локализация. Тем не менее, количество русских обзоров не настолько велико, чтобы повлиять на рейтинг игры. И без изучения обзоров вы не сразу поймёте, что с игрой что-то не так.

Мой скрипт призван решить все перечисленные проблемы.
Поэтому он добавляет три новых вида обзоров в описание игр:

  • Тотальные обзоры — Все обзоры с учётом активаций ключами.
  • Безкитайские обзоры — Все обзоры с вычетом китайских.
  • Русские обзоры — Только русские обзоры.
    • При щелчке по русским обзорам выводится модальное окно с 20-ю самыми полезными обзорами.
      Внутри модального окна есть кнопка для показа 20-ти самых актуальных обзоров.

spkDPBA.png

Если какие-то из обзоров вам не нужны, то вы можете заменить True на False в 17-19 строках кода.


    // Настройки для включения/отключения обзоров
    const settings = {
        showTotalReviews: true, // Показывать тотальные обзоры
        showNonChineseReviews: true, // Показывать безкитайские обзоры
        showRussianReviews: true // Показывать русские обзоры
    };


Сейчас, при щелчке по русским обзорам, вы получаете данные о самых полезных обзорах за всё время.
Поскольку в функции: 


    // Функция для отправки запроса и получения HTML-кода обзоров
    function fetchRussianReviewsHTML(appid, filter, callback) {
        let url = `https://store.steampowered.com/appreviews/${appid}?language=russian&purchase_type=all&filter=${filter}&day_range=9223372036854775807`;
        GM_xmlhttpRequest({
            method: "GET",
            url: url,
            onload: function(response) {
                let data = JSON.parse(response.responseText);
                callback(data.html); // Извлекаем HTML-код из ответа
            }
        });
    }

day_range выставлен на:


&day_range=9223372036854775807


9223372036854775807 — это максимально возможное значение для 64-битного целого числа со знаком (signed 64-bit integer), которое часто используется в программировании как своеобразный маркер "бесконечности".  В данном случае это значение позволяет получить данные, которые не ограничиваются ни днями, ни месяцами, а охватывают всю доступную историю.

Если вы хотите получить самые полезные обзоры за год, просто измените его на 365.


&day_range=365

Или любое другое нужное вам число.

----
Обновление от 24.12.2024:

Суть обновления: Теперь при щелчке по статистике русских обзоров открывается модальное окно с 20-ю самыми полезными обзорами на русском. Внутри модального окна есть кнопка “показать актуальные”, чтобы показать 20 последних обзоров.

Скрытый текст

bYDOPYY.gif


Обновление от 31.01.2025:

Суть обновления:

  • Добавлены индикаторы наличия русского языка.
    Сейчас, чтобы узнать о наличии текстового перевода или озвучки приходится листать страницу игры вниз. Если вы открываете много страниц — этот скроллинг начинает надоедать. Поэтому я решил добавить индикаторы на видное место.

    Если в игре есть какой-то вид перевода, то индикатор горит ярко. Если нет, то индикатор тёмный.
    Всё интуитивно понятно. А если нет — при наведении курсора на любой индикатор всплывет подсказка. Порядок такой:
    • Интерфейс
    • Озвучка
    • Субтитры
       
  • Добавлена кнопка “Загрузить” дополнительные обзоры.
    В прошлой версии скрипта была проблема: когда вы заходили на страницу любой игры, скрипт отправлял по 3 запроса на получение дополнительных обзоров независимо от того, нужны они вам или нет. Теперь скрипт показывает обзоры только тогда, когда вы этого хотите — достаточно нажать кнопку “загрузить” и он выведет все три дополнительные категории.
     

dgrLRAW.png

Скрытый текст

// ==UserScript==
// @name         Steam Reviews Extender & Ru Indicator
// @namespace    https://store.steampowered.com/
// @version      3.0
// @description  Расширяет обзоры и добавляет значки о наличии русского языка на странице игры в Steam.
// @author       0wn3df1x
// @match        https://store.steampowered.com/app/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// ==/UserScript==

(function () {
    'use strict';

    // ==== Часть 1: Индикатор русского языка ====

    function createFruitIndicator(apple, hasSupport, orange) {
        const banana = document.createElement('div');
        banana.style.position = 'relative';
        banana.style.cursor = 'pointer';

        const grape = document.createElement('div');
        grape.style.width = '60px';
        grape.style.height = '60px';
        grape.style.borderRadius = '4px';
        grape.style.display = 'flex';
        grape.style.alignItems = 'center';
        grape.style.justifyContent = 'center';
        grape.style.background = hasSupport ? 'rgba(66, 135, 245, 0.2)' : 'rgba(0, 0, 0, 0.1)';
        grape.style.border = `1px solid ${hasSupport ? '#2A5891' : '#3c3c3c'}`;
        grape.style.opacity = '0.95';
        grape.style.transition = 'transform 0.3s ease, box-shadow 0.3s ease';
        grape.style.overflow = 'hidden';
        grape.style.position = 'relative';
        grape.style.transform = 'translateZ(0)';

        const kiwi = document.createElement('div');
        kiwi.innerHTML = apple;
        kiwi.style.width = '30px';
        kiwi.style.height = '30px';
        kiwi.style.display = 'block';
        kiwi.style.margin = '0 auto';
        kiwi.style.transition = 'fill 0.3s ease';

        grape.appendChild(kiwi);

        const svgElement = kiwi.querySelector('svg');

        function setColor(hasSupport) {
            const borderColor = hasSupport ? '#2A5891' : '#3c3c3c';
            const svgFill = hasSupport ? '#FFFFFF' : '#0E1C25';

            grape.style.border = `1px solid ${borderColor}`;
            svgElement.style.fill = svgFill;
        }

        setColor(hasSupport);

        const pineapple = document.createElement('div');
        const hasLabel = hasSupport ? orange : getGenitiveCase(orange);
        pineapple.textContent = hasSupport ? `Есть ${orange}` : `Нет ${hasLabel}`;
        pineapple.style.position = 'absolute';
        pineapple.style.top = '50%';
        pineapple.style.left = '100%';
        pineapple.style.transform = 'translateY(-50%) translateX(10px)';
        pineapple.style.background = 'rgba(0, 0, 0, 0.8)';
        pineapple.style.color = '#fff';
        pineapple.style.padding = '8px 12px';
        pineapple.style.borderRadius = '8px';
        pineapple.style.fontSize = '14px';
        pineapple.style.whiteSpace = 'nowrap';
        pineapple.style.opacity = '0';
        pineapple.style.transition = 'opacity 0.3s ease';
        pineapple.style.zIndex = '10000';
        pineapple.style.pointerEvents = 'none';
        banana.appendChild(pineapple);

        banana.addEventListener('mouseenter', () => {
            grape.style.transform = 'scale(1.1) translateZ(0)';
            pineapple.style.opacity = '1';
        });

        banana.addEventListener('mouseleave', () => {
            grape.style.transform = 'scale(1) translateZ(0)';
            pineapple.style.opacity = '0';
        });

        banana.appendChild(grape);
        return banana;
    }

    function getGenitiveCase(orange) {
        switch (orange) {
            case 'интерфейс': return 'интерфейса';
            case 'озвучка': return 'озвучки';
            case 'субтитры': return 'субтитров';
            default: return orange;
        }
    }

    function checkRussianSupport() {
        const mango = document.querySelector('#languageTable table.game_language_options');
        if (!mango) return { interface: false, voice: false, subtitles: false };

        const strawberry = mango.querySelectorAll('tr');
        for (let blueberry of strawberry) {
            const watermelon = blueberry.querySelector('td.ellipsis');
            if (watermelon && /русский|Russian/i.test(watermelon.textContent.trim())) {
                const cherry = blueberry.querySelector('td.checkcol:nth-child(2) span');
                const raspberry = blueberry.querySelector('td.checkcol:nth-child(3) span');
                const blackberry = blueberry.querySelector('td.checkcol:nth-child(4) span');

                return {
                    interface: cherry !== null,
                    voice: raspberry !== null,
                    subtitles: blackberry !== null
                };
            }
        }
        return { interface: false, voice: false, subtitles: false };
    }

    function addRussianIndicators() {
        const russianSupport = checkRussianSupport();
        if (!russianSupport) return;

        let lemon = document.querySelector('#gameHeaderImageCtn');
        if (!lemon) return;

        const lime = document.createElement('div');
        lime.style.position = 'absolute';
        lime.style.top = '-10px';
        lime.style.left = 'calc(100% + 10px)';
        lime.style.display = 'flex';
        lime.style.flexDirection = 'column';
        lime.style.gap = '15px';
        lime.style.alignItems = 'flex-start';
        lime.style.zIndex = '1002';
        lime.style.marginTop = '10px';

        const peach = createFruitIndicator(`<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12,0C5.38,0,0,5.38,0,12s5.38,12,12,12s12-5.38,12-12S18.62,0,12,0z M12,22C6.49,22,2,17.51,2,12S6.49,2,12,2	s10,4.49,10,10S17.51,22,12,22z M10.5,10h3v8h-3V10z M10.5,5h3v3h-3V5z" /></svg>`, russianSupport.interface, 'интерфейс');
        const plum = createFruitIndicator(`<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M15,21v-2c3.86,0,7-3.14,7-7s-3.14-7-7-7V3c4.96,0,9,4.04,9,9S19.96,21,15,21z M15,17v-2c1.65,0,3-1.35,3-3s-1.35-3-3-3V7 c2.76,0,5,2.24,5,5S17.76,17,15,17z M1,12v4h5l6,5V3L6,8H1V12" /></svg>`, russianSupport.voice, 'озвучка');
        const apricot = createFruitIndicator(`<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M11,24l-4.4-5H0V0h23v19h-7.6L11,24z M2,17h5.4l3.6,4l3.6-4H21V2H2V17z" /></g><g><rect x="5" y="8" width="3" height="3" /></g><g><rect x="10" y="8" width="3" height="3" /></g><g><rect x="15" y="8" width="3" height="3" /></g></svg>`, russianSupport.subtitles, 'субтитры');

        lime.appendChild(peach);
        lime.appendChild(plum);
        lime.appendChild(apricot);

        lemon.style.position = 'relative';
        lemon.appendChild(lime);

        const appName = document.querySelector('#appHubAppName.apphub_AppName');
        if (appName) {
            appName.style.maxWidth = '530px';
            appName.style.overflow = 'hidden';
            appName.style.textOverflow = 'ellipsis';
            appName.style.whiteSpace = 'nowrap';
            appName.title = appName.textContent;
        }
    }

    // ==== Часть 2: Расширение обзоров ====

    const settings = {
        showTotalReviews: true,
        showNonChineseReviews: true,
        showRussianReviews: true
    };

    function fetchReviews(appid, language, callback) {
        let url = `https://store.steampowered.com/appreviews/${appid}?json=1&language=${language}&purchase_type=all`;
        GM_xmlhttpRequest({
            method: "GET",
            url: url,
            onload: function (response) {
                let data = JSON.parse(response.responseText);
                callback(data);
            }
        });
    }

    function fetchRussianReviewsHTML(appid, filter, callback) {
        let url = `https://store.steampowered.com/appreviews/${appid}?language=russian&purchase_type=all&filter=${filter}&day_range=365`;
        GM_xmlhttpRequest({
            method: "GET",
            url: url,
            onload: function (response) {
                let data = JSON.parse(response.responseText);
                callback(data.html);
            }
        });
    }

    function addStyles() {
        GM_addStyle(`
            .additional-reviews {
                margin-top: 10px;
            }
            .additional-reviews .user_reviews_summary_row {
                display: flex;
                line-height: 16px;
                cursor: pointer;
                margin-bottom: 5px;
            }
            .additional-reviews .subtitle {
                flex: 1;
                color: #556772;
                font-size: 12px;
            }
            .additional-reviews .summary {
                flex: 3;
                color: #c6d4df;
                font-size: 12px;
                overflow: hidden;
                white-space: nowrap;
                text-overflow: ellipsis;
            }
            .additional-reviews .game_review_summary {
                font-weight: normal;
            }
            .additional-reviews .positive {
                color: #66c0f4;
            }
            .additional-reviews .mixed {
                color: #B9A074;
            }
            .additional-reviews .negative {
                color: #a34c25;
            }
            .additional-reviews .no_reviews {
                color: #929396;
            }
            .additional-reviews .responsive_hidden {
                color: #556772;
                margin-left: 5px;
            }
            .modal {
                display: none;
                position: fixed;
                z-index: 1000;
                left: 0;
                top: 0;
                width: 100%;
                height: 100%;
                overflow: auto;
                background-color: rgba(0,0,0,0.8);
            }
            .modal-content {
                background-color: #1b2838;
                margin: 10% auto;
                padding: 20px;
                border: 1px solid #888;
                width: 80%;
                max-width: 800px;
                color: #c6d4df;
                position: relative;
                max-height: 80vh;
                overflow-y: auto;
            }
            .close {
                color: #aaa;
                position: sticky;
                top: 0;
                float: right;
                font-size: 28px;
                font-weight: bold;
                cursor: pointer;
                background: rgba(0,0,0,0.8);
                padding: 5px 10px;
                border-radius: 5px;
                transition: color 0.2s ease, background 0.2s ease, transform 0.2s ease;
                box-shadow: 0 2px 4px rgba(0,0,0,0.2);
            }
            .close:hover {
                color: #fff;
                background: #e64a4a;
                transform: scale(1.1);
            }
            .close:active {
                background: #c43c3c;
                transform: scale(0.95);
            }
            .refresh-button {
                position: left;
                top: 10px;
                right: 50px;
                background: #66c0f4;
                color: #1b2838;
                padding: 10px 20px;
                border: none;
                cursor: pointer;
                z-index: 1001;
                border-radius: 2px;
                transition: background 0.2s ease, color 0.2s ease;
            }
            .refresh-button:hover {
                background: #45b0e6;
                color: #fff;
            }
            .refresh-button:active {
                background: #329cd4;
                transform: translateY(1px);
            }
        `);
    }

    function formatNumber(number) {
        return number.toLocaleString('en-US');
    }

    function getReviewClass(percent, totalReviews) {
        if (totalReviews === 0) return 'no_reviews';
        if (percent >= 70) return 'positive';
        if (percent >= 40) return 'mixed';
        if (percent >= 1) return 'negative';
        return 'negative';
    }

    function addLoadButton() {
        let reviewsContainer = document.querySelector('.user_reviews');
        if (reviewsContainer) {
            let additionalReviews = document.createElement('div');
            additionalReviews.className = 'additional-reviews';

            additionalReviews.innerHTML = `
                <div class="user_reviews_summary_row" id="load-reviews-button">
                    <div class="subtitle column all">Доп. обзоры:</div>
                    <div class="summary column">
                        <span class="game_review_summary no_reviews">Загрузить</span>
                    </div>
                </div>
            `;

            reviewsContainer.appendChild(additionalReviews);

            document.getElementById('load-reviews-button').addEventListener('click', function () {
                loadAdditionalReviews();
            });
        }
    }

    function loadAdditionalReviews() {
        let appid = window.location.pathname.match(/\/app\/(\d+)/)[1];
        let languages = [];
        let data = {};

        if (settings.showTotalReviews || settings.showNonChineseReviews) {
            languages.push('all');
        }
        if (settings.showNonChineseReviews) {
            languages.push('schinese');
        }
        if (settings.showRussianReviews) {
            languages.push('russian');
        }

        languages.forEach(language => {
            fetchReviews(appid, language, (response) => {
                data[language] = response;
                if (Object.keys(data).length === languages.length) {
                    displayAdditionalReviews(data['all'], data['schinese'], data['russian']);
                }
            });
        });
    }

    function displayAdditionalReviews(allData, schineseData, russianData) {
        let allReviews = allData ? allData.query_summary : null;
        let schineseReviews = schineseData ? schineseData.query_summary : null;
        let russianReviews = russianData ? russianData.query_summary : null;

        let additionalReviews = document.querySelector('.additional-reviews');
        if (additionalReviews) {
            additionalReviews.innerHTML = '';

            if (settings.showTotalReviews && allReviews) {
                let allPercent = allReviews.total_reviews > 0 ? Math.round((allReviews.total_positive / allReviews.total_reviews) * 100) : 0;
                let allClass = getReviewClass(allPercent, allReviews.total_reviews);
                additionalReviews.innerHTML += `
                    <div class="user_reviews_summary_row">
                        <div class="subtitle column all">Тотальные:</div>
                        <div class="summary column">
                            <span class="game_review_summary ${allClass}">${allPercent}% из ${formatNumber(allReviews.total_reviews)} положительные</span>
                        </div>
                    </div>
                `;
            }

            if (settings.showNonChineseReviews && allReviews && schineseReviews) {
                let schintotalrev = allReviews.total_reviews - schineseReviews.total_reviews;
                let schintotapos = allReviews.total_positive - schineseReviews.total_positive;
                let schinpercent = schintotalrev > 0 ? Math.round((schintotapos / schintotalrev) * 100) : 0;
                let schinClass = getReviewClass(schinpercent, schintotalrev);
                additionalReviews.innerHTML += `
                    <div class="user_reviews_summary_row">
                        <div class="subtitle column all">Безкитайские:</div>
                        <div class="summary column">
                            <span class="game_review_summary ${schinClass}">${schinpercent}% из ${formatNumber(schintotalrev)} положительные</span>
                        </div>
                    </div>
                `;
            }

            if (settings.showRussianReviews && russianReviews) {
                let rustotalrev = russianReviews.total_reviews;
                let ruspositive = russianReviews.total_positive;
                let ruspercent = rustotalrev > 0 ? Math.round((ruspositive / rustotalrev) * 100) : 0;
                let rusClass = getReviewClass(ruspercent, rustotalrev);
                additionalReviews.innerHTML += `
                    <div class="user_reviews_summary_row" id="russian-reviews-row">
                        <div class="subtitle column all">Русские:</div>
                        <div class="summary column">
                            <span class="game_review_summary ${rusClass}">${ruspercent}% из ${formatNumber(rustotalrev)} положительные</span>
                        </div>
                    </div>
                `;

                document.getElementById('russian-reviews-row').addEventListener('click', function () {
                    openModal();
                });
            }
        }
    }

    function openModal() {
        let modal = document.createElement('div');
        modal.className = 'modal';
        modal.innerHTML = `
            <div class="modal-content">
                <span class="close">&times;</span>
                <button class="refresh-button" id="refresh-reviews">Загрузить актуальные</button>
                <div id="reviews-container"></div>
            </div>
        `;
        document.body.appendChild(modal);

        modal.querySelector('.close').addEventListener('click', function () {
            modal.style.display = 'none';
        });

        modal.querySelector('#refresh-reviews').addEventListener('click', function () {
            refreshReviews(modal);
        });

        modal.style.display = 'block';

        loadReviews(modal, 'all');
    }

    function refreshReviews(modal) {
        modal.querySelector('#reviews-container').innerHTML = '';
        loadReviews(modal, 'recent');
    }

    function loadReviews(modal, filter) {
        fetchRussianReviewsHTML(window.location.pathname.match(/\/app\/(\d+)/)[1], filter, function (html) {
            modal.querySelector('#reviews-container').innerHTML = html;
            modal.querySelector('#LoadMoreReviewsall')?.remove();
            modal.querySelector('#LoadMoreReviewsrecent')?.remove();
        });
    }

    // ==== Основная функция ====

    function main() {
        addStyles();
        addRussianIndicators();
        addLoadButton();
    }

    main();
})();

 

 

Изменено пользователем 0wn3df1x
  • Лайк (+1) 1

Поделиться сообщением


Ссылка на сообщение

Steam Store - Информатор || Tamper Monkey — функционал перемещён в Ultimate Steam Enhancer.

Скрытый текст

 

Steam Store - Информатор  || Tamper Monkey

Описание:

Во время поиска по каталогу Steam количество выдаваемой информации ограничено. Изначально вы видите изображение, название, дату выхода, индикатор качества обзоров и цену. 

При наведении на элемент справа всплывает подсказка, в которой показываются скриншоты, пользовательские метки, наличие игры у друзей, характеристику обзоров и их количество.

Мой скрипт добавляет ещё одну подсказку слева, в которой отображает:

  • Издателей
  • Разработчиков
  • Франшизу (серию игр)
  • Фильтрованные отзывы (просеянные алгоритмом Steam, без учёта аномальных).
  • Информацию о том, находится ли игра в раннем доступе
  • Информацию о переводе игры на русский язык (указывает, есть ли перевод интерфейса, озвучки и субтитров).
  • [Информацию об английском языке, в версии для переводчиков, под вторым спойлером]
  • Короткое описание игры.

krLCGPz.jpeg

 

Скрытый текст

1. Инициализация:

  • Скрипт запускается с задержкой в 2 секунды (WAIT_TIME = 2000), чтобы страница успела загрузиться.
  • После задержки начинается сбор appId всех видимых игр на странице.

2. Сбор данных:

  • Скрипт ищет все элементы с классом a.search_result_row[data-ds-appid], которые представляют игры в каталоге Steam.
  • Для каждого найденного элемента извлекается appId и добавляется в коллекцию collectedAppIds, если он ещё не был добавлен.

3. Запрос данных:

  • Если найдены новые appId, скрипт отправляет запрос к API Steam (IStoreBrowseService/GetItems/v1) для получения дополнительной информации о играх.

4. Обработка данных:

  • Полученные данные обрабатываются и сохраняются в атрибуте data-game-info соответствующего элемента игры.

5. Отображение подсказки:

  • При наведении на элемент игры запускается таймер hoverTimer, который через 300 мс отображает кастомную подсказку слева от элемента.
  • Подсказка содержит всю собранную информацию о игре, включая издателей, разработчиков, франшизу, отзывы, ранний доступ, поддержку русского языка и краткое описание.

6. Динамическая подгрузка:

  • Скрипт использует MutationObserver для отслеживания изменений в DOM (например, при прокрутке страницы и подгрузке новых игр).
  • При обнаружении новых элементов игры, скрипт запускает процесс сбора appId из них для запроса данных.
Скрытый текст



// ==UserScript==
// @name         Steam Store - Информатор
// @namespace    https://store.steampowered.com/
// @version      0.5
// @description  Выводит дополнительную информацию об играх в поиске по каталогу
// @author       You
// @match        https://store.steampowered.com/search/*
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function() {
    'use strict';

    const API_URL = "https://api.steampowered.com/IStoreBrowseService/GetItems/v1";
    const WAIT_TIME = 2000; // 2 seconds
    const VISIBLE_ELEMENTS_SELECTOR = "a.search_result_row[data-ds-appid]";
    const HOVER_ELEMENT_SELECTOR = "a.search_result_row";

    let collectedAppIds = new Set();
    let tooltip = null;
    let hoverTimer = null;
    let hideTimer = null;

    function fetchGameData(appIds) {
        const inputJson = {
            ids: Array.from(appIds).map(appid => ({ appid })),
            context: { language: "russian", country_code: "US", steam_realm: 1 },
            data_request: {
                include_assets: true,
                include_release: true,
                include_platforms: true,
                include_all_purchase_options: true,
                include_screenshots: true,
                include_trailers: true,
                include_ratings: true,
                include_tag_count: true,
                include_reviews: true,
                include_basic_info: true,
                include_supported_languages: true,
                include_full_description: true,
                include_included_items: true,
                included_item_data_request: {
                    include_assets: true,
                    include_release: true,
                    include_platforms: true,
                    include_all_purchase_options: true,
                    include_screenshots: true,
                    include_trailers: true,
                    include_ratings: true,
                    include_tag_count: true,
                    include_reviews: true,
                    include_basic_info: true,
                    include_supported_languages: true,
                    include_full_description: true,
                    include_included_items: true,
                    include_assets_without_overrides: true,
                    apply_user_filters: false,
                    include_links: true
                },
                include_assets_without_overrides: true,
                apply_user_filters: false,
                include_links: true
            }
        };

        GM_xmlhttpRequest({
            method: "GET",
            url: `${API_URL}?input_json=${encodeURIComponent(JSON.stringify(inputJson))}`,
            onload: function(response) {
                const data = JSON.parse(response.responseText);
                processGameData(data);
            }
        });
    }

    function processGameData(data) {
        const items = data.response.store_items;
        items.forEach(item => {
            const appId = item.id;
            const gameElement = document.querySelector(`a.search_result_row[data-ds-appid="${appId}"]`);
            if (gameElement) {
                const gameData = {
                    is_early_access: item.is_early_access,
                    review_count: item.reviews?.summary_filtered?.review_count,
                    percent_positive: item.reviews?.summary_filtered?.percent_positive,
                    short_description: item.basic_info?.short_description,
                    publishers: item.basic_info?.publishers?.map(p => p.name).join(", "),
                    developers: item.basic_info?.developers?.map(d => d.name).join(", "),
                    franchises: item.basic_info?.franchises?.map(f => f.name).join(", "),
                    language_support: item.supported_languages?.find(lang => lang.elanguage === 8)
                };

                gameElement.dataset.gameInfo = JSON.stringify(gameData);
            }
        });
    }

    function collectAndFetchAppIds() {
        const visibleElements = document.querySelectorAll(VISIBLE_ELEMENTS_SELECTOR);
        const newAppIds = new Set();

        visibleElements.forEach(element => {
            const appId = element.dataset.dsAppid;
            if (!collectedAppIds.has(appId)) {
                newAppIds.add(parseInt(appId, 10));
                collectedAppIds.add(appId);
            }
        });

        if (newAppIds.size > 0) {
            fetchGameData(newAppIds);
        }
    }

    function handleHover(event) {
        const gameElement = event.target.closest(HOVER_ELEMENT_SELECTOR);

        if (gameElement && gameElement.dataset.gameInfo) {
            clearTimeout(hoverTimer);
            clearTimeout(hideTimer);

            hoverTimer = setTimeout(() => {
                const gameData = JSON.parse(gameElement.dataset.gameInfo);
                displayGameInfo(gameElement, gameData);
            }, 300);
        } else {
            clearTimeout(hoverTimer);
            clearTimeout(hideTimer);
            if (tooltip) {
                tooltip.style.opacity = 0;
                setTimeout(() => {
                    tooltip.style.display = 'none';
                }, 300);
            }
        }
    }

    function displayGameInfo(element, data) {
        if (!tooltip) {
            tooltip = document.createElement('div');
            tooltip.className = 'custom-tooltip';
            tooltip.innerHTML = '<div class="tooltip-arrow"></div><div class="tooltip-content"></div>';
            document.body.appendChild(tooltip);
        }

        const tooltipContent = tooltip.querySelector('.tooltip-content');

        let languageSupportText = "Отсутствует";
        if (data.language_support) {
            languageSupportText = "";
            if (data.language_support.supported) languageSupportText += "<br>Интерфейс: ✔ ";
            if (data.language_support.full_audio) languageSupportText += "<br>Озвучка: ✔ ";
            if (data.language_support.subtitles) languageSupportText += "<br>Субтитры: ✔";
            if (languageSupportText === "") languageSupportText = "Отсутствует";
        }

        tooltipContent.innerHTML = `
            <div style="margin-bottom: 0px;"><strong>Издатели:</strong> ${data.publishers || "Нет данных"}</div>
            <div style="margin-bottom: 0px;"><strong>Разработчики:</strong> ${data.developers || "Нет данных"}</div>
            <div style="margin-bottom: 10px;"><strong>Серия игр:</strong> ${data.franchises || "Нет данных"}</div>
            <div style="margin-bottom: 10px;"><strong>Отзывы:</strong> ${data.review_count || "0"} (${data.percent_positive || "0"}% положительных)</div>
            <div style="margin-bottom: 10px;"><strong>Ранний доступ:</strong> ${data.is_early_access ? "Да" : "Нет"}</div>
            <div style="margin-bottom: 10px;"><strong>Русский язык:</strong> ${languageSupportText}</div>
            <div style="margin-bottom: 10px;"><strong>Описание:</strong> ${data.short_description || "Нет данных"}</div>
        `;

        tooltip.style.display = 'block';

        const rect = element.getBoundingClientRect();
        const tooltipRect = tooltip.getBoundingClientRect();
        tooltip.style.left = `${rect.left + window.scrollX - tooltipRect.width - 4}px`;
        tooltip.style.top = `${rect.top + window.scrollY - 20}px`;

        tooltip.style.opacity = 0;
        tooltip.style.display = 'block';
        setTimeout(() => {
            tooltip.style.opacity = 1;
        }, 10);

        element.addEventListener('mouseleave', () => {
            clearTimeout(hideTimer);
            hideTimer = setTimeout(() => {
                tooltip.style.opacity = 0;
                setTimeout(() => {
                    tooltip.style.display = 'none';
                }, 300);
            }, 200);
        }, { once: true });

        element.addEventListener('mouseover', () => {
            clearTimeout(hideTimer);
        });
    }

    function observeNewElements() {
        const observer = new MutationObserver((mutations) => {
            mutations.forEach(mutation => {
                if (mutation.type === 'childList') {
                    collectAndFetchAppIds();
                }
            });
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }

    function initialize() {
        setTimeout(() => {
            collectAndFetchAppIds();
            observeNewElements();
            document.addEventListener('mouseover', handleHover);
        }, WAIT_TIME);
    }

    initialize();

    const style = document.createElement('style');
    style.innerHTML = `
        .custom-tooltip {
            position: absolute;
            background: linear-gradient(to bottom, #e3eaef, #c7d5e0);
            color: #30455a;
            padding: 12px;
            border-radius: 0px;
            box-shadow: 0 0 12px #000;
            font-size: 12px;
            max-width: 300px;
            display: none;
            z-index: 1000;
            opacity: 0;
            transition: opacity 0.4s ease-in-out;
        }
        .tooltip-arrow {
            position: absolute;
            right: -9px;
            top: 32px;
            width: 0;
            height: 0;
            border-top: 10px solid transparent;
            border-bottom: 10px solid transparent;
            border-left: 10px solid #E1E8ED;
        }
    `;
    document.head.appendChild(style);
})();

 


 

Изменено пользователем 0wn3df1x
  • Лайк (+1) 1

Поделиться сообщением


Ссылка на сообщение

@0wn3df1x Блин, сделай уже один большой скрипт чтоб я как царь его поставил.

Мелкие тоже поставил, все страшно полезные. Удивительно, что большие аддоны типа Augmented, SIH и прочие всем этим пренебрегают.

  • Лайк (+1) 1
  • Хаха (+1) 1

Поделиться сообщением


Ссылка на сообщение

Создайте аккаунт или войдите в него для комментирования

Вы должны быть пользователем, чтобы оставить комментарий

Создать аккаунт

Зарегистрируйтесь для получения аккаунта. Это просто!

Зарегистрировать аккаунт

Войти

Уже зарегистрированы? Войдите здесь.

Войти сейчас



Zone of Games © 2003–2025 | Реклама на сайте.

×