Перейти к публикации
[ {"link":"https://topdeck.ru/apps/toptrade/member/32/promo/1", "image":"https://topdeck.ru/apps/toptrade/member/32/promo/1/image"}, {"link":"https://topdeck.ru/apps/toptrade/member/32/promo/2", "image":"https://topdeck.ru/apps/toptrade/member/32/promo/2/image"} ]

Автоподнятие темы.


 Поделиться

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

Привет. Кто-нибудь знает как сделать авто поднятие темы? С программированием дружу

Ссылка на комментарий
Поделиться на других сайтах

29.11.2025 в 13:20, GrannyCola сказал:

Привет. Кто-нибудь знает как сделать авто поднятие темы? С программированием дружу

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

Ссылка на комментарий
Поделиться на других сайтах

Вообще я уже сделал, я думал, просто кто-то конкретно элемент HTML напишет, по которому тыкать надо, а то у меня кнопка заблокирована на поднятие)

Ссылка на комментарий
Поделиться на других сайтах

29.11.2025 в 14:00, GrannyCola сказал:

Вообще я уже сделал, я думал, просто кто-то конкретно элемент HTML напишет, по которому тыкать надо, а то у меня кнопка заблокирована на поднятие)

Поделись с общественностью трудами своими

Ссылка на комментарий
Поделиться на других сайтах

from playwright.sync_api import sync_playwright
import logging
import time


TOPIC_URL = (
    "<URL вашей торговой темы>"
)

COOKIES = [
    {
        "name": "ips4_device_key",
        "value": "<ПОДСТАВИТЬ СВОЕ ЗНАЧЕНИЕ>",
        "domain": "topdeck.ru",
        "path": "/",
    },
    {
        "name": "ips4_member_id",
        "value": "<ПОДСТАВИТЬ СВОЕ ЗНАЧЕНИЕ>",
        "domain": "topdeck.ru",
        "path": "/",
    },
    {
        "name": "ips4_login_key",
        "value": "<ПОДСТАВИТЬ СВОЕ ЗНАЧЕНИЕ>",
        "domain": "topdeck.ru",
        "path": "/",
    },
    {
        "name": "ips4_IPSSessionFront",
        "value": "<ПОДСТАВИТЬ СВОЕ ЗНАЧЕНИЕ>",
        "domain": "topdeck.ru",
        "path": "/",
    },
    {
        "name": "ips4_loggedIn",
        "value": "<ПОДСТАВИТЬ СВОЕ ЗНАЧЕНИЕ>",
        "domain": "topdeck.ru",
        "path": "/",
    },
]

LOG_FILE = "topdeck_bump_playwright.log"


def setup_logging() -> None:
    logging.basicConfig(
        filename=LOG_FILE,
        level=logging.INFO,
        format="%(asctime)s [%(levelname)s] %(message)s",
    )


def bump_via_browser() -> None:
    logging.info("Starting bump attempt via Playwright")

    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        try:
            context = browser.new_context()
            context.add_cookies(COOKIES)

            page = context.new_page()
            logging.info("Navigating to topic URL")
            page.goto(TOPIC_URL, wait_until="networkidle")
            page.wait_for_timeout(3000)

            li_selector = 'li[data-controller*="BumpUpTopics"]'
            li_loc = page.locator(li_selector)

            li_count = li_loc.count()
            logging.info("BumpUpTopics <li> count: %s", li_count)

            if li_count == 0:
                logging.warning("No <li> with BumpUpTopics found on page")
                with open("last_page.html", "w", encoding="utf-8") as f:
                    f.write(page.content())
                return

            li_first = li_loc.first
            spans_in_li = li_first.locator("span")
            span_count = spans_in_li.count()
            logging.info("Span count inside BumpUpTopics li: %s", span_count)

            # Пробуем найти именно disabled / enabled по id
            bump_span = page.locator("#elBumpDisabled, #elBumpEnabled").first
            bump_span_count = bump_span.count()
            logging.info("Bump span by id count: %s", bump_span_count)

            if bump_span_count == 0:
                logging.info("Bump span with id not found")
                with open("last_page.html", "w", encoding="utf-8") as f:
                    f.write(page.content())
                return

            if not bump_span.is_visible():
                logging.info(
                    "Bump span found by id, but not visible "
                    "(cooldown or CSS-hidden)"
                )
                return

            logging.info("Clicking bump span by id...")
            bump_span.click()
            page.wait_for_timeout(3000)
            logging.info("Bump span click finished")
        finally:
            browser.close()


if __name__ == "__main__":
    setup_logging()
    while True:
        try:
            logging.info("=== New hourly bump cycle started ===")
            bump_via_browser()
        except Exception as exc:
            logging.exception("Unhandled error in main loop: %s", exc)
        logging.info("Sleeping for 1 hour before next attempt")
        time.sleep(3600)

Пока сделал так. Каждый час скрипт пытается через эмуляцию браузера нажать на кнопку. Разворачивать на VDS сервере. Предварительно вы должны узнать свои куки (подставить соответствующие значения в места, где написано """<ПОДСТАВИТЬ СВОЕ ЗНАЧЕНИЕ>", через пункт Сеть в Панели разработчика браузера, которая открывается через F12. Потом как раз блокируется кнопка, можно будет сделать и без эмуляции браузера, когда подсмотрю элемент.

Если надо подробнее - пишите, постараюсь ответить.

Ссылка на комментарий
Поделиться на других сайтах

29.11.2025 в 19:46, GrannyCola сказал:
from playwright.sync_api import sync_playwright
import logging
import time


TOPIC_URL = (
    "<URL вашей торговой темы>"
)

COOKIES = [
    {
        "name": "ips4_device_key",
        "value": "<ПОДСТАВИТЬ СВОЕ ЗНАЧЕНИЕ>",
        "domain": "topdeck.ru",
        "path": "/",
    },
    {
        "name": "ips4_member_id",
        "value": "<ПОДСТАВИТЬ СВОЕ ЗНАЧЕНИЕ>",
        "domain": "topdeck.ru",
        "path": "/",
    },
    {
        "name": "ips4_login_key",
        "value": "<ПОДСТАВИТЬ СВОЕ ЗНАЧЕНИЕ>",
        "domain": "topdeck.ru",
        "path": "/",
    },
    {
        "name": "ips4_IPSSessionFront",
        "value": "<ПОДСТАВИТЬ СВОЕ ЗНАЧЕНИЕ>",
        "domain": "topdeck.ru",
        "path": "/",
    },
    {
        "name": "ips4_loggedIn",
        "value": "<ПОДСТАВИТЬ СВОЕ ЗНАЧЕНИЕ>",
        "domain": "topdeck.ru",
        "path": "/",
    },
]

LOG_FILE = "topdeck_bump_playwright.log"


def setup_logging() -> None:
    logging.basicConfig(
        filename=LOG_FILE,
        level=logging.INFO,
        format="%(asctime)s [%(levelname)s] %(message)s",
    )


def bump_via_browser() -> None:
    logging.info("Starting bump attempt via Playwright")

    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        try:
            context = browser.new_context()
            context.add_cookies(COOKIES)

            page = context.new_page()
            logging.info("Navigating to topic URL")
            page.goto(TOPIC_URL, wait_until="networkidle")
            page.wait_for_timeout(3000)

            li_selector = 'li[data-controller*="BumpUpTopics"]'
            li_loc = page.locator(li_selector)

            li_count = li_loc.count()
            logging.info("BumpUpTopics <li> count: %s", li_count)

            if li_count == 0:
                logging.warning("No <li> with BumpUpTopics found on page")
                with open("last_page.html", "w", encoding="utf-8") as f:
                    f.write(page.content())
                return

            li_first = li_loc.first
            spans_in_li = li_first.locator("span")
            span_count = spans_in_li.count()
            logging.info("Span count inside BumpUpTopics li: %s", span_count)

            # Пробуем найти именно disabled / enabled по id
            bump_span = page.locator("#elBumpDisabled, #elBumpEnabled").first
            bump_span_count = bump_span.count()
            logging.info("Bump span by id count: %s", bump_span_count)

            if bump_span_count == 0:
                logging.info("Bump span with id not found")
                with open("last_page.html", "w", encoding="utf-8") as f:
                    f.write(page.content())
                return

            if not bump_span.is_visible():
                logging.info(
                    "Bump span found by id, but not visible "
                    "(cooldown or CSS-hidden)"
                )
                return

            logging.info("Clicking bump span by id...")
            bump_span.click()
            page.wait_for_timeout(3000)
            logging.info("Bump span click finished")
        finally:
            browser.close()


if __name__ == "__main__":
    setup_logging()
    while True:
        try:
            logging.info("=== New hourly bump cycle started ===")
            bump_via_browser()
        except Exception as exc:
            logging.exception("Unhandled error in main loop: %s", exc)
        logging.info("Sleeping for 1 hour before next attempt")
        time.sleep(3600)

Пока сделал так. Каждый час скрипт пытается через эмуляцию браузера нажать на кнопку. Разворачивать на VDS сервере. Предварительно вы должны узнать свои куки (подставить соответствующие значения в места, где написано """<ПОДСТАВИТЬ СВОЕ ЗНАЧЕНИЕ>", через пункт Сеть в Панели разработчика браузера, которая открывается через F12. Потом как раз блокируется кнопка, можно будет сделать и без эмуляции браузера, когда подсмотрю элемент.

Если надо подробнее - пишите, постараюсь ответить.

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

Ссылка на комментарий
Поделиться на других сайтах

20 минут назад, ak8 сказал:

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

Согласен. Пока так, костыльно сделал. Можно использовать crontab и просто запускать этот скрипт раз в сутки, в одно и то же время

Ссылка на комментарий
Поделиться на других сайтах

29.11.2025 в 20:15, GrannyCola сказал:

Согласен. Пока так, костыльно сделал. Можно использовать crontab и просто запускать этот скрипт раз в сутки, в одно и то же время

Вообще можно и реже, ежедневный подъем подразумевает, что у вас что-то новое в теме появилось, достаточно наверное раз в неделю поднимать, чтобы тема из поиска не пропадала

Ссылка на комментарий
Поделиться на других сайтах

Автоматизируйте и обновление цен с остатками заодно, что толку от обычных апов

Ссылка на комментарий
Поделиться на других сайтах

46 минут назад, Pooffick сказал:

Автоматизируйте и обновление цен с остатками заодно, что толку от обычных апов

В теории можно синхронизировать с какой-нибудь Google таблицей.

Ссылка на комментарий
Поделиться на других сайтах

Сделал небольшую утилиту. Мб кому-то пригодится. Вам нужно расширение для браузера TamperMonkey. Работает так, что для размеченных карт (которые подсвечиваются после "Разметить Карты", например Opt) в темах выводит минимальную/среднюю/макс. цену на карту по Scryfall. Скрипт, который надо туда добавить.
П.с. Запускать на свое усмотрение. Ответственности за неполадки не несу.

// ==UserScript==
// @name         Topdeck card prices tooltip (hover left of cursor)
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  Show min/avg/max prices in tooltip on hover
// @author       GrannyCola
// @match        https://topdeck.ru/*
// @grant        GM_xmlhttpRequest
// @connect      api.scryfall.com
// ==/UserScript==

(function () {
    'use strict';

    const TARGET_SELECTOR = 'a.topdeck_tooltipCard';

    // If empty, tooltip shows for all cards.
    // If not empty, only for names from this array.
    const ALLOWED_CARD_NAMES = [
        // 'Spare Supplies'
    ];

    const SOURCES = [
        {
            name: 'usd',
            extract: json => {
                if (!json || !json.prices) return null;
                const v = parseFloat(json.prices.usd);
                return Number.isFinite(v) ? v : null;
            }
        },
        {
            name: 'eur',
            extract: json => {
                if (!json || !json.prices) return null;
                const v = parseFloat(json.prices.eur);
                return Number.isFinite(v) ? v : null;
            }
        },
        {
            name: 'usd_foil',
            extract: json => {
                if (!json || !json.prices) return null;
                const v = parseFloat(json.prices.usd_foil);
                return Number.isFinite(v) ? v : null;
            }
        }
    ];

    const cardCache = new Map();
    let tooltipEl = null;
    let currentLink = null;

    function gmGetJson(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url,
                onload: response => {
                    try {
                        const data = JSON.parse(response.responseText);
                        resolve(data);
                    } catch (e) {
                        console.error('Failed to parse JSON', e);
                        reject(e);
                    }
                },
                onerror: err => {
                    console.error('Request failed', err);
                    reject(err);
                },
                ontimeout: () => {
                    reject(new Error('Request timeout'));
                }
            });
        });
    }

    async function fetchCardData(cardName) {
        const url =
            'https://api.scryfall.com/cards/named?exact=' +
            encodeURIComponent(cardName);
        return gmGetJson(url);
    }

    async function getCardPrices(cardName) {
        if (cardCache.has(cardName)) {
            return cardCache.get(cardName);
        }

        let json;
        try {
            json = await fetchCardData(cardName);
        } catch (e) {
            console.warn('Cannot fetch card data for', cardName, e);
            cardCache.set(cardName, null);
            return null;
        }

        const prices = SOURCES
            .map(source => source.extract(json))
            .filter(v => v !== null);

        if (!prices.length) {
            cardCache.set(cardName, null);
            return null;
        }

        const minPrice = Math.min.apply(null, prices);
        const maxPrice = Math.max.apply(null, prices);
        const avgPrice =
            prices.reduce((acc, v) => acc + v, 0) / prices.length;

        const result = { min: minPrice, avg: avgPrice, max: maxPrice };
        cardCache.set(cardName, result);
        return result;
    }

    function formatPrice(value) {
        return '$' + value.toFixed(2);
    }

    function createTooltip() {
        if (tooltipEl) return;

        tooltipEl = document.createElement('div');
        tooltipEl.id = 'tm-card-price-tooltip';
        tooltipEl.style.position = 'absolute';
        tooltipEl.style.zIndex = '9999';
        tooltipEl.style.background = 'rgba(20, 20, 20, 0.95)';
        tooltipEl.style.color = '#fff';
        tooltipEl.style.padding = '4px 6px';
        tooltipEl.style.borderRadius = '4px';
        tooltipEl.style.fontSize = '11px';
        tooltipEl.style.whiteSpace = 'nowrap';
        tooltipEl.style.pointerEvents = 'none';
        tooltipEl.style.display = 'none';
        document.body.appendChild(tooltipEl);
    }

    function showTooltipAt(mouseX, mouseY, text) {
        if (!tooltipEl) createTooltip();
        if (!tooltipEl) return;

        tooltipEl.textContent = text;
        tooltipEl.style.display = 'block';

        const rect = tooltipEl.getBoundingClientRect();
        const tooltipWidth = rect.width;
        const tooltipHeight = rect.height;
        const offset = 10;

        const top = mouseY - tooltipHeight / 2;
        const left = mouseX - tooltipWidth - offset;

        tooltipEl.style.top = top + 'px';
        tooltipEl.style.left = left + 'px';
    }

    function moveTooltip(mouseX, mouseY) {
        if (!tooltipEl || tooltipEl.style.display === 'none') return;

        const rect = tooltipEl.getBoundingClientRect();
        const tooltipWidth = rect.width;
        const tooltipHeight = rect.height;
        const offset = 10;

        const top = mouseY - tooltipHeight / 2;
        const left = mouseX - tooltipWidth - offset;

        tooltipEl.style.top = top + 'px';
        tooltipEl.style.left = left + 'px';
    }

    function hideTooltip() {
        if (tooltipEl) tooltipEl.style.display = 'none';
        currentLink = null;
    }

    function isAllowedCardName(cardName) {
        if (!ALLOWED_CARD_NAMES.length) return true;
        return ALLOWED_CARD_NAMES.includes(cardName);
    }

    function attachHandlersToLink(link) {
        if (link.dataset.tmPriceBound === '1') return;
        link.dataset.tmPriceBound = '1';

        link.addEventListener('mouseenter', async event => {
            const cardName = link.textContent.trim();
            if (!cardName || !isAllowedCardName(cardName)) return;

            currentLink = link;
            showTooltipAt(event.pageX, event.pageY, 'Loading prices...');

            try {
                const prices = await getCardPrices(cardName);
                if (!prices) {
                    if (currentLink === link) {
                        showTooltipAt(event.pageX, event.pageY, 'No prices');
                    }
                    return;
                }

                const text =
                    'min ' +
                    formatPrice(prices.min) +
                    ' / avg ' +
                    formatPrice(prices.avg) +
                    ' / max ' +
                    formatPrice(prices.max);

                if (currentLink === link) {
                    showTooltipAt(event.pageX, event.pageY, text);
                }
            } catch (e) {
                console.error('Error processing card', cardName, e);
                if (currentLink === link) {
                    showTooltipAt(event.pageX, event.pageY, 'Error');
                }
            }
        });

        link.addEventListener('mousemove', event => {
            if (currentLink === link) {
                moveTooltip(event.pageX, event.pageY);
            }
        });

        link.addEventListener('mouseleave', () => {
            if (currentLink === link) {
                hideTooltip();
            }
        });
    }

    function processAllLinks(root) {
        const links = (root || document).querySelectorAll(TARGET_SELECTOR);
        links.forEach(attachHandlersToLink);
    }

    function init() {
        createTooltip();
        processAllLinks(document);

        const observer = new MutationObserver(mutations => {
            for (const mutation of mutations) {
                mutation.addedNodes.forEach(node => {
                    if (!(node instanceof HTMLElement)) return;
                    if (node.matches && node.matches(TARGET_SELECTOR)) {
                        attachHandlersToLink(node);
                    }
                    processAllLinks(node);
                });
            }
        });

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

    init();
})();

 

Ссылка на комментарий
Поделиться на других сайтах

38 минут назад, GrannyCola сказал:

Сделал небольшую утилиту. Мб кому-то пригодится. Вам нужно расширение для браузера TamperMonkey. Работает так, что для размеченных карт (которые подсвечиваются после "Разметить Карты", например Opt) в темах выводит минимальную/среднюю/макс. цену на карту по Scryfall. Скрипт, который надо туда добавить.
П.с. Запускать на свое усмотрение. Ответственности за неполадки не несу.

 

А по какой лицензии распространяется ПО?

Ссылка на комментарий
Поделиться на других сайтах

11 минут назад, ph.zombowsky сказал:

А по какой лицензии распространяется ПО?

Malware

Ссылка на комментарий
Поделиться на других сайтах

Открытое ПО

1 час назад, ph.zombowsky сказал:

А по какой лицензии распространяется ПО?

 

1 час назад, Nikitozz сказал:

Malware

Тут все открыто, пусть люди напишут, где тут уязвимость, если она вообще есть)

Ссылка на комментарий
Поделиться на других сайтах

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

Ссылка на комментарий
Поделиться на других сайтах

51 минуту назад, GrannyCola сказал:

Открытое ПО

Видов лицензий на открытое ПО великое множество )

Ссылка на комментарий
Поделиться на других сайтах

13 минут назад, ph.zombowsky сказал:

Видов лицензий на открытое ПО великое множество )

MIT

Ссылка на комментарий
Поделиться на других сайтах

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

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

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

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

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

Войти

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

Войти сейчас
 Поделиться

  • Сейчас на странице   0 пользователей

    Нет пользователей, просматривающих эту страницу.

×
×
  • Создать...