Как скачивать игры на Steam Deck по ссылке с телефона: метод через CEF и Mega

Прокомментировать Просмотры: 4

DeckDrop на смартфоне

Я искренне обожаю проекты вне экосистемы Steam: DRM-free релизы с GOG и itch.io, разнообразные визуальные новеллы, а также фанатские локализации и патчи. На Steam Deck всё это запускается без проблем, но сам процесс превращается в унылую рутину: загрузка, переход в рабочий стол, распаковка архива, добавление в Steam как стороннего приложения, выбор Proton, утомительный поиск обложек и возвращение в игровой режим. А бонусом — ещё один поход в десктоп, потому что ярлык по умолчанию гордо именуется «game.exe». Бесконечно переключаться ради этого туда-сюда быстро надоело.

Так и родилась концепция DeckDrop: один раз настроил всё в режиме рабочего стола — и забыл о нём. В дальнейшем любые манипуляции производятся со смартфона или персонального компьютера: закинул ссылку в браузер, и спустя мгновения игра уже красуется в библиотеке Steam с корректным наименованием. Заодно утилита сама подтягивает обложки для новелл с VNDB, избавляя от ручной возни.

Вторая постоянная боль — скриншоты. Перекидывать снимки экрана с «деки» на телефон через штатный Steam неудобно, а объёмные видеоролики отправить и вовсе целая проблема. Поэтому в DeckDrop появилась выделенная вкладка «Медиа»: вся медиатека Steam доступна прямо со смартфона, откуда её можно запрограммировать на скачивание в пару касаний.

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

В этой публикации не будет перечисления рекламных преимуществ — с ними можно ознакомиться в файле README. Я сосредоточусь на четырёх архитектурных вызовах, над которыми пришлось поразмыслить: как управлять Steam, когда он постоянно перезаписывает собственные конфиги; как реализовать скачивание с Mega без сторонних зависимостей; зачем упаковывать весь софт в единый скрипт; и каким образом тестировать обновления, чтобы они не затирали пользовательские настройки.

Особенности разработки

Сразу оговорюсь насчёт инструментов разработки, поскольку история коммитов не врёт: весь код написан с помощью Claude Code. Моя зона ответственности заключалась в проектировании логики, ограничений, архитектуры, сценариев для неочевидных ситуаций и реакций на сбои. Я формировал требования, анализировал результат, тестировал его «в поле» на консоли и возвращал правки. Большинство интересных решений родилось именно так: натыкаясь на реальное препятствие на устройстве, мы совместно искали оптимальный обходной путь.

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

  • исключительно стандартная библиотека Python;

  • полный запрет на root-права и модификацию системных разделов;

  • абсолютная устойчивость к обновлениям SteamOS.

Архитектура SteamOS устроена так, что корневая директория защищена от записи и полностью накатывается заново при каждом апдейте. Всё, установленное через pacman, бесповоротно стирается. Команда pip install --user технически функционирует, однако пакеты жестко привязаны к конкретной версии интерпретатора, которую система регулярно обновляет. В результате вся логика функционирует внутри домашней директории в качестве пользовательской службы systemd. Сторонние утилиты вроде ffmpeg или 7z задействуются опционально при наличии, но прекрасно работают и в их отсутствие.

Конфликты с файловой системой Steam

Ранние сборки добавляли игры предсказуемым методом: через утилиту steamos-add-to-steam с прямой записью названий и параметров Proton в файлы shortcuts.vdf и config.vdf. На обычном ПК при выключенном Steam это работало безупречно. Однако на Deck клиент Steam запущен постоянно, удерживая актуальные конфигурации в оперативной памяти, из-за чего при закрытии он просто перезаписывал изменения поверх наших правок. Файлы на носителе выглядели корректно, но Steam их игнорировал.

Элегантное решение было подсмотрено у Decky Loader. Интерфейс клиента Steam базируется на Chromium Embedded Framework (CEF), обладающем специальным отладочным портом. Он активируется автоматически, если в рабочей директории клиента разместить пустой файл с именем .cef-enable-remote-debugging. Порт принимает соединения исключительно локально, оставаясь невидимым извне. Через него можно выполнять JavaScript во внутреннем окружении контекста SharedJSContext, где открывается доступ к объекту SteamClient — тому самому API, задействованному в стандартном интерфейсе и плагинах Decky:

def add_shortcut(self, name, exe, start_dir):
    appid = self.call("SteamClient.Apps.AddShortcut", name, exe, start_dir, "")
    return int(appid) & 0xFFFFFFFF

def set_compat(self, appid, tool):
    self.call("SteamClient.Apps.SpecifyCompatTool", appid, tool or "")

def set_artwork(self, appid, data, ext, asset_type):
    self.call("SteamClient.Apps.SetCustomArtworkForApp",
              appid, base64.b64encode(data).decode(), ext, asset_type)

В результате клиент самостоятельно формирует ярлык, запоминает профиль Proton, а кастомные обложки интегрируются в библиотеку моментально без перезагрузки.

Поскольку отладочный протокол функционирует поверх WebSocket, а стандартная библиотека Python лишена готового клиента, пришлось с нуля написать компактную реализацию по стандарту RFC 6455: процедуру рукопожатия, маскирование пакетов, сборку фрагментов и обработку пинг-запросов. Кода вышло около ста строк, и больше ничего не потребовалось.

Не обошлось без парадоксов. Отладочный порт Steam по умолчанию занимает 8080-й порт — ровно на нём изначально запускался и DeckDrop. Пока файл-маркер отсутствовал, всё функционировало, но после перезапуска они начинали отчаянно делить один порт. Пришлось мигрировать на 8088 и обучить кнопку обновления автоматически перенаправлять пользователя на новый адрес. Второй нюанс заключался в том, что маркер вступает в силу лишь при последующем старте Steam. Чтобы избавить пользователя от лишних перезагрузок, до этого момента утилита добавляет игры старым методом, а переименование и привязка Proton встают в очередь, выполняясь автоматически сразу после активации управляющего API.

Интеграция с Mega без сторонних библиотек

Сервис Mega шифрует контент непосредственно в браузере конечного пользователя. На удаленных серверах хранятся исключительно зашифрованные массивы, а секретный ключ передается в URL-фрагменте после символа #, который браузеры намеренно не передают на сервер. Следовательно, скачивание с Mega подразумевает локальную расшифровку потока. Но в стандартной библиотеке Python отсутствуют средства работы с AES.

Зато наличествует модуль ctypes, а сам интерпретатор Python собирается в связке с OpenSSL, где модулю ssl необходима динамическая библиотека libcrypto. Это гарантирует её присутствие на любом дистрибутиве, поддерживающем HTTPS, включая SteamOS. DeckDrop подтягивает её через ctypes для вызова функции EVP_aes_128_ctr. На случай полного отсутствия библиотеки предусмотрен резервный алгоритм AES на чистом Python, но разница в производительности колоссальна. Мои тесты на ПК показали следующие результаты:

  • libcrypto через ctypes — порядка 700 МБ/с;

  • чистый Python — около 0,2 МБ/с.

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

Остальной функционал сводится к точному воспроизведению протокола. 32-байтный ключ из ссылки объединяется побитовым XOR из двух частей, образуя 16-байтный AES-ключ, из хвостовой части извлекаются параметры nonce для режима CTR и контрольная сумма. Имена шифруются обособленно в режиме AES-CBC с нулевым вектором инициализации (IV), превращаясь после дешифровки в строку вида MEGA{"n":"имя"}. На завершающем этапе DeckDrop проверяет MAC: сервис рассчитывает CBC-MAC по блокам нарастающего объема от 128 КБ до 1 МБ. Поврежденные при транспортировке данные моментально отсеиваются.

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

Самый курьезный баг крылся вовсе не в криптографии. Стоило мне вставить ссылку на директорию с игрой на движке Unity, как интерфейс заполнился несколькими десятками заданий: каждый файл с расширением .dll из папки BepInEx/plugins система попыталась загрузить как отдельную самостоятельную «игру». Теперь директории с Mega обрабатываются как единая задача со сквозным прогрессом: файлы аккуратно собираются во временном каталоге с сохранением исходной иерархии и лишь под конец превращаются в единую игровую папку. Если же по ссылке находится архив, разбитый на тома (вроде .part1.rar или .7z.001), распаковывается корневой файл, а сопутствующие части автоматически помечаются как вспомогательные компоненты.

Единый файл без монолитной каши в коде

Мне хотелось сделать процесс установки и обновления максимально элементарным: вызов wget, одна команда в терминале — и далее всё управляется кнопкой «Обновить утилиту» из веб-интерфейса. Идеальное решение для таких задач — распространение в виде единственного файла: его удобно загрузить, проверить целостность и атомарно подменить. Поначалу так оно и было. Однако со временем кодовая база разрослась до пяти с лишним тысяч строк, объединяющих серверную логику, разметку HTML, стили CSS и встроенные скрипты, превратившись в трудноподдерживаемое полотно.

Теперь исходный код структурирован как классический пакет: директория src/deckdrop/ с логическими модулями, отдельные файлы index.html, app.css, app.js и JSON-словари локализации. Специальный скрипт сборки tools/build.py компилирует это богатство в единый файл deckdrop.py. Внутри него исходники модулей хранятся в виде строк в обычном словаре, а префикс дополнен миниатюрным загрузчиком, эмулирующим пакет deckdrop:

class _BundleImporter(importlib.abc.MetaPathFinder, importlib.abc.Loader):
    def find_spec(self, name, path=None, target=None):
        if name not in _MODULES:
            return None
        is_pkg, rel, _ = _MODULES[name]
        return importlib.util.spec_from_loader(name, self, origin=rel, is_package=is_pkg)

    def exec_module(self, module):
        _, rel, src = _MODULES[module.__name__]
        # tracebacks show the lines of src/deckdrop/
        linecache.cache[rel] = (len(src), None, src.splitlines(True), rel)
        exec(compile(src, rel, "exec"), module.__dict__)

Для меня было принципиально, чтобы скомпилированный файл вел себя идентично исходникам из папки src/. Код не слеплен в слепую простыню, а выполняется в виде полноценных изолированных модулей. Благодаря интеграции с механизмом linecache, аварийные трейсбеки на консоли указывают на реальный файл и строчку в репозитории, а не на абстрактную строку 4817 слитного скрипта.

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

Гарантия сохранности настроек при апдейте

Больше всего я опасался сценария, при котором рядовое нажатие кнопки «Обновить» неожиданно обнулит пользовательские конфигурации. DeckDrop хранит немало критически важных данных: административный PIN-код, пароль галереи скриншотов (защищенный хешем PBKDF2), параметры прокси-сервера с учетными данными, перечни добавленных тайтлов и отложенные команды для Steam. Подобные проблемы не отловить стандартными юнит-тестами — сбои обычно случаются на стыке устаревших структур данных и новой программной логики.

Поэтому главный сценарий комплексного тестирования проекта в точности повторяет мои действия:

  1. подтягивает из git несколько исторических релизов по тегам и запускает каждый в изолированной чистой домашней папке;

  2. конфигурирует устаревшую версию через её веб-API: изменяет PIN, переводит параметры из дефолтных значений, устанавливает пароль галереи, скрывает одну игру и импортирует другую;

  3. активирует процедуру обновления в старом клиенте, подсовывая ему актуальную сборку;

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

В пайплайне непрерывной интеграции (CI) это обязательный этап, прогоняемый в окружении Python версий 3.8, 3.11 и 3.13. Версия 3.8 присутствует в матрице не для галочки: единожды в веб-API затесалась конструкция вида dict | dict, доступная лишь с версии 3.9, и именно этот автоматический тест её своевременно зафиксировал. Теперь юнит-тесты содержат отдельную проверку на подобные синтаксические конструкции.

Релизы также формируются в автоматическом режиме: при изменении переменной version в ветке main система собирает скрипт, прогоняет тестовый цикл, выставляет тег и публикует deckdrop.py вместе с install.sh в разделе релизов GitHub. Кнопка обновления на Steam Deck обращается именно туда.

Внутреннее убранство

Кратко о сопутствующих возможностях:

  • Генерация обложек для всех пяти слотов Steam: подтягивание с VNDB для визуальных новелл или извлечение из иконки exe-файла. Иконка парсится через собственный разборщик PE-ресурсов без подключения внешних библиотек.

  • Встроенная галерея скриншотов и видеозаписей: прямой доступ со смартфона без использования облачных хранилищ и проводного подключения. Видеопоток поддерживает HTTP Range-запросы, обеспечивая корректную перемотку в iOS.

  • Интеграция патчей в игровые директории: архив предварительно распаковывается во временную зону, позволяя DeckDrop наглядно продемонстрировать, какие файлы будут добавлены, а какие перезаписаны. Оригиналы сохраняются с расширением .bak.

  • Создание резервных копий сохранений в zip-архивы и обратное восстановление, включая подкаталоги внутри префиксов Proton.

  • Двуязычный интерфейс (русский и английский). Англоязычная локализация — единственное, что я добавил не для себя, а для сообщества. Языковой стандарт считывается из настроек десктопного клиента Steam.

Страница игры
Интерфейс страницы игры

Прокси-сервер как необходимость

Проживая в России, мы прекрасно осведомлены о специфике доступа к некоторым ресурсам. Изначально база VNDB с консоли банально не пинговалась: на ПК у меня настроен VPN, а Deck Deck выходил в сеть напрямую, из-за чего соединения с VNDB обрывались. Разворачивать VPN-клиент на уровне всей консоли ради обложек не хотелось, поэтому в DeckDrop интегрировали собственный прокси. Он обслуживает исключительно нужды самого приложения, оставляя системный трафик консоли нетронутым.

Наиболее элегантное решение оказалось следующим: у VPN-клиента на ПК задействован локальный SOCKS5-шлюз, в котором разрешены подключения из домашней локальной сети, после чего в конфигурации DeckDrop прописывается адрес этого компьютера. Поскольку полноценного SOCKS5-клиента в стандартной библиотеке Python тоже нет, его пришлось реализовать самостоятельно. Адрес прокси может защищаться логином и паролем, поэтому функционал заблокирован под PIN-кодом и недоступен со страниц без авторизации.

Вопросы безопасности без иллюзий

DeckDrop проектировался под условия закрытой домашней сети. Протокол HTTPS отсутствует: для доменного имени вроде steamdeck.local невозможно выпустить валидный сертификат доверия без предупреждений браузера, а самоподписанные сертификаты лишь приучают пользователей бездумно кликать «продолжить несмотря на риск». Функции удаления, апдейтов и настройки прокси защищены PIN-кодом, медиагалерея закрыта паролем, однако закинуть произвольный файл на консоль способен любой пользователь в пределах вашего Wi-Fi. В домашних условиях это допустимо, но запускать утилиту в публичных сетях категорически не рекомендуется.

Опыт взаимодействия с Claude Code

Пара выводов относительно подобного стиля разработки. Наилучших результатов удавалось достичь, когда в первую очередь формулировались жесткие ограничения, а не готовые алгоритмы: «без сторонних зависимостей», «ничего не модифицировать автоматический без подтверждения кнопкой», «прокси исключительно для нужд приложения». В таком случае предложения ИИ сразу ложились в заданные рамки. Хуже всего дела шли, когда верификация кода производилась исключительно на ПК: добрая половина интересных багов — начиная со Steam, затирающего конфигурации, и заканчивая десятками фейковых задач из одной директории Mega — проявилась только при живом тестировании на консоли. Тест на сохранность пользовательских данных также родился эмпирически: перенастраивать всё заново после каждого минорного апдейта мне категорически не улыбалось.

Установка

В режиме рабочего стола запустите терминал Konsole и выполните команды:

wget https://github.com/Aniforka/deckdrop/releases/latest/download/install.sh
sh install.sh

Установщик выведет в консоль сгенерированный адрес страницы и административный PIN. После этого можно возвращаться в игровой режим (Gaming Mode) и открывать полученный адрес со смартфона.

Исходный код распространяется под свободной лицензией MIT: github.com/Aniforka/deckdrop. Если на вашей консоли что-то пойдет не так, смело оформляйте issue в репозитории, туда же приветствуются любые идеи. Особенно интересно услышать предложения по поддержке альтернативных файлообменников.

Обновление: самодиагностика и бенчмаркинг

Начиная с версии 0.4.1, в DeckDrop появился подраздел «Настройки» → «Производительность», содержащий две полезные кнопки.

Самодиагностика сканирует все компоненты, от которых зависит работоспособность утилиты: автозапуск, файлы конфигурации, свободное пространство на носителях, статус Steam и ярлыков, распаковщики, модули дешифровки Mega, сетевое соединение, доступность VNDB и системные логи. Напротив каждого пункта выводится детальный вердикт о состоянии и возможных путях решения проблем.

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

Раз уж я позиционирую DeckDrop как легковесный сервер, приведем цифры телеметрии с моего Steam Deck OLED в состоянии покоя:

Параметр

Значение

ЦП, процесс DeckDrop

0,67% от одного ядра

ЦП, вся система в целом

1,5%

Энергопотребление консоли

3,7 Вт от аккумулятора

ОЗУ, DeckDrop

42 МБ (около 0,3% от 16 ГБ)

Скорость расшифровки Mega

908 МБ/с

Расчет контрольной суммы Mega

477 МБ/с

Распаковка zip-архивов

612 МБ/с

Запись на внутренний накопитель

546 МБ/с

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

Обновление 2: ещё больше оптимизации

В релизе 0.4.2 я сделал DeckDrop ещё более ресурсоэффективным. Если раньше при каждом обновлении страницы утилита заново сканировала директории всех добавленных игр, то теперь проверка каталогов запускается исключительно при реальном изменении их файлов. На моей библиотеке (5 игр) время обновления интерфейса сократилось с 6 до 2,5 миллисекунд, а потребление ЦП в простое снизилось с 0,67% до 0,33% от одного ядра. Главное — нагрузка больше не масштабируется линейно с ростом библиотеки: на тестовой базе из 100 игр сканирование ускорилось с 311 до 16 миллисекунд. Более того, если свернуть вкладку в мобильном браузере, страница полностью прекращает опрос консоли.

В закрытом состоянии веб-интерфейса DeckDrop практически погружается в сон. За минуту отсутствия входящих запросов утилита расходует около 20 мс процессорного времени (примерно 0,03% одного ядра) и удерживает порядка 28 МБ оперативной памяти. Единственная фоновая задача раз в 20 секунд проверяет очередь изменений для Steam и, не обнаружив таковых, моментально засыпает, не взаимодействуя ни со Steam, ни с дисковой подсистемой. Перечни игр, дисковое пространство и обложки вычисляются исключительно в моменты активности пользователя, а загрузки и распаковка задействуют независимые потоки лишь на время выполнения конкретной задачи. Таким образом, на автономность Steam Deck в режиме ожидания DeckDrop не оказывает никакого влияния.

 

Источник

Поделиться:

Похожие статьи

Поиск по играм, новостям и статьям…

Введите не менее двух символов

Введите не менее двух символов