-
Публикации
31 -
Зарегистрирован
-
Посещение
Тип публикации
Профили
Форум
Календарь
Галерея
Все публикации пользователя GrannyCola
-
Еще тейблтоп симулятор существует
-
А какой бюджет примерно? Вообще одноцветные и двухцветные обычно подешевле дечки, чем трехцвет. Меньше проблем с маной. Мысль о том, чтобы брать прекончик и улучшать его потихоньку, тоже неплоха.
-
Скиньтесь с друзьями на преконы с Озона из китая за 4-5к (правда ждать месяц минимум их). Либо напечатайте и играйте прокси-картами (печатайте и нарезаете карты, кладете их в протекторы, а в качестве подкладки карты базовых земель или просто напечатать на плотной бумаге). Только надо чтобы в командер играло минимум 3, а лучше 4 человека
-
Всем привет, ищем +1 или +2 в компанию ирл в Волгограде игры в EDH или в Tabletop Simulator (любой город). Мы уже стабильно играем почти полгода в онлайне. За это время поиграли много форматов, 2 и 3 Bracket, двухголового, 2 Bracket с ограничениями по соли, рандомные командиры и многое другое, в общем открыты ко всему. Можете играть настоящими картами + прокси или фулл прокси, как вам удобно. По полу и возрасту ограничений нет. Те, кто узнал меня из своих (волгоградцев) - пишите в личку телеге @GrannyCola, либо здесь в личке.
-
Ну меня решили не в порядке очереди, а отпускали всех кроме меня) Да, с упаковкой согласен, проще со своей.
-
Если идёте в Озон - со входа спрашивайте насколько быстро они вас оформят, потому что есть ПВЗ, где видимо у них неотработанная схема, они тупят и пропускают по 100 человек, и стоишь ждёшь по 15-20 мин, когда с тобой разберутся. Конвертов у них нет. В моих соседних ПВЗ, то коробок нет, то школьники-стажеры на операторе.
-
MIT
-
Открытое ПО Тут все открыто, пусть люди напишут, где тут уязвимость, если она вообще есть)
-
Сделал небольшую утилиту. Мб кому-то пригодится. Вам нужно расширение для браузера 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(); })();
-
В теории можно синхронизировать с какой-нибудь Google таблицей.
-
Согласен. Пока так, костыльно сделал. Можно использовать crontab и просто запускать этот скрипт раз в сутки, в одно и то же время
-
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. Потом как раз блокируется кнопка, можно будет сделать и без эмуляции браузера, когда подсмотрю элемент. Если надо подробнее - пишите, постараюсь ответить.
-
Вообще я уже сделал, я думал, просто кто-то конкретно элемент HTML напишет, по которому тыкать надо, а то у меня кнопка заблокирована на поднятие)
-
Привет. Кто-нибудь знает как сделать авто поднятие темы? С программированием дружу
-
Люди, бросившие окончательно магию, вряд ли оставят комментарий здесь)
- 252 ответа
-
- 38
-
-
-
-
@Strein К сожалению пока да, по причине отсутствия свободного времени...
-
Такой возможности пока нет. Думаю эту проблему на первых парах решит вывод количества карт (слава богу такой параметр есть), не додумался его добавить что-то сразу. Спасибо за идею.
-
Теперь можно отслеживать до 50-ти карт
-
Сделаю больше
-
"тыж программистов"
-
Добрый день, друзья. Занимаюсь разработкой telegram-бота, который может: 1. Отображать список цен на карту у продавцов на Topdeck. 2. Отслеживать цену карты. Так, например, мы можем указать название карты и желаемую цену. Когда карта станет дешевле, то Вам придёт уведомление. Можно отслеживать 50 карт. 3. Бот сам сформирует ссылку на новую переписку с продавцом карты, вы можете ему оперативно написать. 4. Можно просматривать список отслеживаемых карт, прекращать отслеживание, если есть необходимость. 2-ой пункт предусматривает ведение базы данных (без этого никак), облачный сервер был приобретен по самому дешевому тарифу, поэтому требуется тестирование нагрузки. Собственно, ищу людей желающих протестировать бот. Сам бот @topdeck_price_grabber_bot Важная информация: я, как администратор бота, к сожалению(или к счастью), буду иметь доступ к списку отслеживаемых карт в виде: Название карты - Желаемая цена - ваш telegram user_id (такова структура БД), принимая участие вы принимаете сей факт без претензий. Также, возможна нестабильность в работе, бот всё ещё находится в стадии разработки. Спасибо за внимание и понимание, жду ваши замечания, предложения, вопросы и т.д. Может быть уже кто-то опередил меня с идеей... Рассчитываю на Вашу поддержку! Upd: Вы можете поставить заведомо большую отслеживаемую цену, написав например, (/track Наглый Воришка 10000000), Вам придёт уведомление, что карта стала дешевле. Пожалуйста, проверьте, и отпишитесь о работе данной функции. Upd: Увидел, что есть проблемы с отслеживанием некоторых карт. Исправляю. Ошибку в скрипте выдают предложения от "магазинов" в списке найденных карт на topdeck, по желанию пользователей могу отключить игнорирование таких предложений, стоит ли? Исправлено Upd: Вы замечали, что сам поисковик topdeck "сходит с ума" при поиске таких карт, как например, Opt или Censor, выдавая ненужные предложения. Попытался ввести хотя бы на платформе бота, точный поиск по названию карты. Так, например, чтобы при написании "Выбор" показывался "Opt" и "Выбор". Оказывается внутри каждой позиции есть недочёт. В списке данных на некоторые предложения фигурирует следующее. Говоря простым языков у предложения с картой Sai, Master Thopterist, есть такие "атрибуты", как 'rus_name' и 'eng_name', которые карте, как мы видим, не соответствуют. Придётся делать строгий поиск с учётом языка по атрибуту 'name'. Также есть проблема с поиск карт, таких как "Гора", "Остров" и т.д., в одно сообщение в Телеграме такие результаты не помещается (это 4096 символов), а разбиваются на несколько и иногда вызывает ошибки, т.к. бьются ссылки продавцов на стыке двух сообщений.