forked from Boria138/PortProtonQt
Compare commits
8 Commits
230ce904d9
...
599644c4f6
Author | SHA1 | Date | |
---|---|---|---|
599644c4f6
|
|||
|
409e06f531 | ||
4818cf5b67
|
|||
59bfcdbbba
|
|||
989af36e5b
|
|||
8300857aaa
|
|||
aea1a36cfd
|
|||
f7a4fa6a17
|
@@ -13,6 +13,7 @@
|
|||||||
### Changed
|
### Changed
|
||||||
- Оптимизированны обложки автоинсталлов
|
- Оптимизированны обложки автоинсталлов
|
||||||
- Папка custom_data исключена из сборки модуля для уменьшение его размера
|
- Папка custom_data исключена из сборки модуля для уменьшение его размера
|
||||||
|
- Бейдж PortProton теперь открывает PortProtonDB
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Запрос к GitHub API при загрузке legendary теперь не игнорирует настройки прокси
|
- Запрос к GitHub API при загрузке legendary теперь не игнорирует настройки прокси
|
||||||
|
73
README.md
73
README.md
@@ -4,79 +4,6 @@
|
|||||||
<p align="center">Удобный графический интерфейс для управления и запуска игр из PortProton, Steam и Epic Games Store. Оно объединяет библиотеки игр в единый центр для лёгкой навигации и организации. Лёгкая структура и кроссплатформенная поддержка обеспечивают цельный игровой опыт без необходимости использования нескольких лаунчеров. Интеграция с PortProton упрощает запуск Windows-игр на Linux с минимальной настройкой.</p>
|
<p align="center">Удобный графический интерфейс для управления и запуска игр из PortProton, Steam и Epic Games Store. Оно объединяет библиотеки игр в единый центр для лёгкой навигации и организации. Лёгкая структура и кроссплатформенная поддержка обеспечивают цельный игровой опыт без необходимости использования нескольких лаунчеров. Интеграция с PortProton упрощает запуск Windows-игр на Linux с минимальной настройкой.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## В планах
|
|
||||||
|
|
||||||
- [X] Адаптировать структуру проекта для поддержки инструментов сборки
|
|
||||||
- [X] Добавить возможность управления с геймпада
|
|
||||||
- [ ] Добавить возможность управления с тачскрина
|
|
||||||
- [X] Добавить возможность управления с мыши и клавиатуры
|
|
||||||
- [X] Добавить систему тем [Документация](documentation/theme_guide)
|
|
||||||
- [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено)
|
|
||||||
- [X] Добавить метаданные для тем (скриншоты, описание, домашняя страница и автор)
|
|
||||||
- [ ] Продумать систему вкладок вместо текущей
|
|
||||||
- [ ] [Добавить сессию Gamescope, аналогичную той, что используется в SteamOS](https://git.linux-gaming.ru/Boria138/gamescope-session-portprotonqt)
|
|
||||||
- [X] Разобраться почему теряется часть стилей в Gamescope
|
|
||||||
- [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800)
|
|
||||||
- [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots)
|
|
||||||
- [X] Получать описания и названия игр из базы данных Steam
|
|
||||||
- [X] Получать обложки для игр из SteamGridDB или CDN Steam
|
|
||||||
- [X] Оптимизировать работу со Steam API для ускорения времени запуска
|
|
||||||
- [X] Улучшить функцию поиска в Steam API для исправления некорректного определения ID (например, Graven определялся как ENGRAVEN или GRAVENFALL, Spore — как SporeBound или Spore Valley)
|
|
||||||
- [ ] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода
|
|
||||||
- [X] Решить проблему с ограничением Steam API в 50 тысяч игр за один запрос (иногда нужные игры не попадают в выборку и остаются без обложки)
|
|
||||||
- [X] Избавиться от вызовов yad
|
|
||||||
- [X] Реализовать собственный механизм запрета ухода в спящий режим вместо использования механизма PortProton (оставлено для [PortProton 2.0](https://github.com/Castro-Fidel/PortProton_2.0))
|
|
||||||
- [X] Реализовать собственный системный трей вместо использования трея PortProton
|
|
||||||
- [X] Добавить экранную клавиатуру в поиск (реализация собственной клавиатуры слишком затратна, поэтому используется встроенная в DE клавиатура: Maliit в KDE, gjs-osk в GNOME, Squeekboard в Phosh, клавиатура SteamOS и т.д.)
|
|
||||||
- [X] Добавить сортировку карточек по различным критериям (доступны: по недавности, количеству наигранного времени, избранному или алфавиту)
|
|
||||||
- [X] Добавить индикацию запуска приложения
|
|
||||||
- [X] Достигнуть паритета функциональности с Ingame
|
|
||||||
- [ ] Достигнуть паритета функциональности с PortProton
|
|
||||||
- [X] Добавить возможность изменения названия, описания и обложки через файлы `.local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}`
|
|
||||||
- [X] Добавить встроенное переопределение названия, описания и обложки, например, по пути `portprotonqt/custom_data` [Документация](documentation/metadata_override/)
|
|
||||||
- [X] Добавить переводы в переопределения
|
|
||||||
- [ ] Придумать как переопределять launcher.exe
|
|
||||||
- [X] Добавить в карточку игры сведения о поддержке геймпада
|
|
||||||
- [X] Добавить в карточки данные с ProtonDB
|
|
||||||
- [X] Добавить в карточки данные с AreWeAntiCheatYet
|
|
||||||
- [X] Продублировать бейджи с карточки на страницу с деталями игры
|
|
||||||
- [X] Добавить парсинг ярлыков из Steam
|
|
||||||
- [X] Добавить парсинг ярлыков из EGS
|
|
||||||
- [ ] Избавиться от бинарника legendary
|
|
||||||
- [X] Добавить запуск игр из EGS
|
|
||||||
- [ ] Добавить скачивание игр из EGS
|
|
||||||
- [ ] Добавить поддержку запуска сторонних игр из EGS
|
|
||||||
- [ ] Добавить авторизацию в EGS через WebView вместо ручного ввода
|
|
||||||
- [X] Получать описания для игр из EGS через их [API](https://store-content.ak.epicgames.com/api)
|
|
||||||
- [X] Получать slug через GraphQL [запрос](https://launcher.store.epicgames.com/graphql)
|
|
||||||
- [X] Добавить на карточку бейдж, указывающий, что игра из Steam
|
|
||||||
- [X] Добавить поддержку версий Steam для Flatpak и Snap
|
|
||||||
- [X] Отображать данные о самом последнем пользователе Steam, а не первом попавшемся
|
|
||||||
- [X] Исправить склонения в детальном выводе времени, например, не «3 часов назад», а «3 часа назад»
|
|
||||||
- [X] Добавить перевод через gettext [Документация](documentation/localization_guide)
|
|
||||||
- [X] Отображать описания игр и другие данные на языке системы
|
|
||||||
- [X] Добавить недокументированные параметры конфигурации в GUI (time_detail_level, games_sort_method, games_display_filter)
|
|
||||||
- [X] Добавить систему избранного для карточек
|
|
||||||
- [X] Заменить все `print` на `logging`
|
|
||||||
- [ ] Привести все логи к единому языку
|
|
||||||
- [X] Уменьшить количество подстановок в переводах
|
|
||||||
- [X] Стилизовать все элементы без стилей (QMessageBox, QSlider, QDialog)
|
|
||||||
- [X] Убрать жёсткую привязку путей к стрелочкам QComboBox в `styles.py`
|
|
||||||
- [X] Исправить частичное применение тем на лету
|
|
||||||
- [X] Исправить наложение подписей скриншотов при первом перелистывании в полноэкранном режиме
|
|
||||||
- [ ] Добавить поддержку GOG (?)
|
|
||||||
- [X] Определиться с названием (PortProtonQt или PortProtonQT или вообще третий вариант)
|
|
||||||
- [ ] Добавить данные с HowLongToBeat на страницу с деталями игры (?)
|
|
||||||
- [X] Добавить виброотдачу на геймпаде при запуске игры
|
|
||||||
- [X] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63)
|
|
||||||
- [X] Исправить баг с наложением карточек друг на друга при изменении фильтра отображения ([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63))
|
|
||||||
- [X] Скопировать логику управления с D-pad на стрелки с клавиатуры
|
|
||||||
- [ ] Доделать светлую тему
|
|
||||||
- [ ] Добавить подсказки к управлению с геймпада
|
|
||||||
- [ ] Добавить загрузку звуков в темы например для добавления звука запуска в тему и тд
|
|
||||||
- [X] Добавить миниатюры к выбору файлов в диалоге добавления игры
|
|
||||||
- [X] Добавить быстрый доступ к смонтированным дискам к выбору файлов в диалоге добавления игры
|
|
||||||
|
|
||||||
### Установка (devel)
|
### Установка (devel)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
69
TODO.md
Normal file
69
TODO.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
- [X] Адаптировать структуру проекта для поддержки инструментов сборки
|
||||||
|
- [X] Добавить возможность управления с геймпада
|
||||||
|
- [ ] Добавить возможность управления с тачскрина
|
||||||
|
- [X] Добавить возможность управления с мыши и клавиатуры
|
||||||
|
- [X] Добавить систему тем [Документация](documentation/theme_guide)
|
||||||
|
- [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено)
|
||||||
|
- [X] Добавить метаданные для тем (скриншоты, описание, домашняя страница и автор)
|
||||||
|
- [ ] Продумать систему вкладок вместо текущей
|
||||||
|
- [X] [Добавить сессию Gamescope, аналогичную той, что используется в SteamOS](https://git.linux-gaming.ru/Boria138/gamescope-session-portprotonqt)
|
||||||
|
- [X] Разобраться почему теряется часть стилей в Gamescope
|
||||||
|
- [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800)
|
||||||
|
- [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots)
|
||||||
|
- [X] Получать описания и названия игр из базы данных Steam
|
||||||
|
- [X] Получать обложки для игр из SteamGridDB или CDN Steam
|
||||||
|
- [X] Оптимизировать работу со Steam API для ускорения времени запуска
|
||||||
|
- [X] Улучшить функцию поиска в Steam API для исправления некорректного определения ID (например, Graven определялся как ENGRAVEN или GRAVENFALL, Spore — как SporeBound или Spore Valley)
|
||||||
|
- [ ] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода
|
||||||
|
- [X] Решить проблему с ограничением Steam API в 50 тысяч игр за один запрос (иногда нужные игры не попадают в выборку и остаются без обложки)
|
||||||
|
- [X] Избавиться от вызовов yad
|
||||||
|
- [X] Реализовать собственный механизм запрета ухода в спящий режим вместо использования механизма PortProton (оставлено для [PortProton 2.0](https://github.com/Castro-Fidel/PortProton_2.0))
|
||||||
|
- [X] Реализовать собственный системный трей вместо использования трея PortProton
|
||||||
|
- [X] Добавить экранную клавиатуру в поиск (реализация собственной клавиатуры слишком затратна, поэтому используется встроенная в DE клавиатура: Maliit в KDE, gjs-osk в GNOME, Squeekboard в Phosh, клавиатура SteamOS и т.д.)
|
||||||
|
- [X] Добавить сортировку карточек по различным критериям (доступны: по недавности, количеству наигранного времени, избранному или алфавиту)
|
||||||
|
- [X] Добавить индикацию запуска приложения
|
||||||
|
- [X] Достигнуть паритета функциональности с Ingame
|
||||||
|
- [ ] Достигнуть паритета функциональности с PortProton
|
||||||
|
- [X] Добавить возможность изменения названия, описания и обложки через файлы `.local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}`
|
||||||
|
- [X] Добавить встроенное переопределение названия, описания и обложки, например, по пути `portprotonqt/custom_data` [Документация](documentation/metadata_override/)
|
||||||
|
- [X] Добавить переводы в переопределения
|
||||||
|
- [X] Добавить в карточку игры сведения о поддержке геймпада
|
||||||
|
- [X] Добавить в карточки данные с ProtonDB
|
||||||
|
- [X] Добавить в карточки данные с AreWeAntiCheatYet
|
||||||
|
- [X] Продублировать бейджи с карточки на страницу с деталями игры
|
||||||
|
- [X] Добавить парсинг ярлыков из Steam
|
||||||
|
- [X] Добавить парсинг ярлыков из EGS
|
||||||
|
- [ ] Избавиться от бинарника legendary
|
||||||
|
- [X] Добавить запуск игр из EGS
|
||||||
|
- [ ] Добавить скачивание игр из EGS
|
||||||
|
- [ ] Добавить поддержку запуска сторонних игр из EGS
|
||||||
|
- [ ] Добавить авторизацию в EGS через WebView вместо ручного ввода
|
||||||
|
- [X] Получать описания для игр из EGS через их [API](https://store-content.ak.epicgames.com/api)
|
||||||
|
- [X] Получать slug через GraphQL [запрос](https://launcher.store.epicgames.com/graphql)
|
||||||
|
- [X] Добавить на карточку бейдж, указывающий, что игра из Steam
|
||||||
|
- [X] Добавить поддержку версий Steam для Flatpak и Snap
|
||||||
|
- [X] Отображать данные о самом последнем пользователе Steam, а не первом попавшемся
|
||||||
|
- [X] Исправить склонения в детальном выводе времени, например, не «3 часов назад», а «3 часа назад»
|
||||||
|
- [X] Добавить перевод через gettext [Документация](documentation/localization_guide)
|
||||||
|
- [X] Отображать описания игр и другие данные на языке системы
|
||||||
|
- [X] Добавить недокументированные параметры конфигурации в GUI (time_detail_level, games_sort_method, games_display_filter)
|
||||||
|
- [X] Добавить систему избранного для карточек
|
||||||
|
- [X] Заменить все `print` на `logging`
|
||||||
|
- [ ] Привести все логи к единому языку
|
||||||
|
- [X] Уменьшить количество подстановок в переводах
|
||||||
|
- [X] Стилизовать все элементы без стилей (QMessageBox, QSlider, QDialog)
|
||||||
|
- [X] Убрать жёсткую привязку путей к стрелочкам QComboBox в `styles.py`
|
||||||
|
- [X] Исправить частичное применение тем на лету
|
||||||
|
- [X] Исправить наложение подписей скриншотов при первом перелистывании в полноэкранном режиме
|
||||||
|
- [ ] Добавить поддержку GOG (?)
|
||||||
|
- [X] Определиться с названием (PortProtonQt или PortProtonQT или вообще третий вариант)
|
||||||
|
- [ ] Добавить данные с HowLongToBeat на страницу с деталями игры (?)
|
||||||
|
- [X] Добавить виброотдачу на геймпаде при запуске игры
|
||||||
|
- [X] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63)
|
||||||
|
- [X] Исправить баг с наложением карточек друг на друга при изменении фильтра отображения ([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63))
|
||||||
|
- [X] Скопировать логику управления с D-pad на стрелки с клавиатуры
|
||||||
|
- [ ] Доделать светлую тему
|
||||||
|
- [ ] Добавить подсказки к управлению с геймпада
|
||||||
|
- [ ] Добавить загрузку звуков в темы например для добавления звука запуска в тему и тд
|
||||||
|
- [X] Добавить миниатюры к выбору файлов в диалоге добавления игры
|
||||||
|
- [X] Добавить быстрый доступ к смонтированным дискам к выбору файлов в диалоге добавления игры
|
@@ -1405,7 +1405,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"normalized_name": "wuthering waves",
|
"normalized_name": "wuthering waves",
|
||||||
"status": "Planned"
|
"status": "Running"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"normalized_name": "dota underlords",
|
"normalized_name": "dota underlords",
|
||||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,4 +1,136 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"normalized_title": "return alive",
|
||||||
|
"slug": "return-alive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "s.t.a.l.k.e.r. anomaly g.a.m.m.a",
|
||||||
|
"slug": "s-t-a-l-k-e-r-anomaly-g-a-m-m-a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "recore",
|
||||||
|
"slug": "recore-definitive-edition"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "no man's sky",
|
||||||
|
"slug": "no-mans-sky"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "alan wake 2",
|
||||||
|
"slug": "alan-wake-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "architect life a house design simulator",
|
||||||
|
"slug": "architect-life-a-house-design-simulator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "clair obscur expedition 33",
|
||||||
|
"slug": "clair-obscur-expedition-33"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "metro 2033 redux",
|
||||||
|
"slug": "metro-2033-redux"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "nova drift",
|
||||||
|
"slug": "nova-drift"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "deathloop",
|
||||||
|
"slug": "deathloop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "mullet madjack",
|
||||||
|
"slug": "mullet-madjack"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "luma island",
|
||||||
|
"slug": "luma-island"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "cash cleaner simulator",
|
||||||
|
"slug": "cash-cleaner-simulator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "the plucky squire (отважный паж)",
|
||||||
|
"slug": "the-plucky-squire-otvazhnyj-pazh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "crsed cuisine royale",
|
||||||
|
"slug": "crsed-cuisine-royale"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "tainted grail the fall of avalon",
|
||||||
|
"slug": "tainted-grail-the-fall-of-avalon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "battle of space raiders",
|
||||||
|
"slug": "battle-of-space-raiders"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "gzdoom",
|
||||||
|
"slug": "gzdoom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "rain on your parade",
|
||||||
|
"slug": "rain-on-your-parade"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "партизан (partisan widerstand hinter feindlichen linien)",
|
||||||
|
"slug": "partizan-partisan-widerstand-hinter-feindlichen-linien"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "ebola 2",
|
||||||
|
"slug": "ebola-2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "monster care simulator",
|
||||||
|
"slug": "monster-care-simulator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "steins;gate the distant valhalla",
|
||||||
|
"slug": "steins-gate-the-distant-valhalla"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "hogwarts legacy",
|
||||||
|
"slug": "hogwarts-legacy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "osu!",
|
||||||
|
"slug": "osu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "stalker online (stay out)",
|
||||||
|
"slug": "stalker-online-stay-out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "slitterhead",
|
||||||
|
"slug": "slitterhead"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "indiana jones and the great circle",
|
||||||
|
"slug": "indiana-jones-and-the-great-circle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "crossout",
|
||||||
|
"slug": "crossout"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "days gone",
|
||||||
|
"slug": "days-gone"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "warcraft iii reforged 2.0",
|
||||||
|
"slug": "warcraft-iii-reforged-2-0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "biomutant",
|
||||||
|
"slug": "biomutant"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "overwatch 2",
|
||||||
|
"slug": "overwatch-2"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"normalized_title": "settlement survival",
|
"normalized_title": "settlement survival",
|
||||||
"slug": "settlement-survival"
|
"slug": "settlement-survival"
|
||||||
@@ -551,10 +683,6 @@
|
|||||||
"normalized_title": "snowrunner (ранее mudrunner 2)",
|
"normalized_title": "snowrunner (ранее mudrunner 2)",
|
||||||
"slug": "snowrunner-ranee-mudrunner-2"
|
"slug": "snowrunner-ranee-mudrunner-2"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"normalized_title": "alan wake 2",
|
|
||||||
"slug": "alan-wake-2"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"normalized_title": "verse project",
|
"normalized_title": "verse project",
|
||||||
"slug": "verse-project"
|
"slug": "verse-project"
|
||||||
|
Binary file not shown.
@@ -5,12 +5,19 @@ import json
|
|||||||
import asyncio
|
import asyncio
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import tarfile
|
import tarfile
|
||||||
|
import ssl
|
||||||
|
|
||||||
# Получаем ключи и данные из переменных окружения
|
# Получаем ключи и данные из переменных окружения
|
||||||
STEAM_KEY = os.environ.get('STEAM_KEY')
|
STEAM_KEY = os.environ.get('STEAM_KEY')
|
||||||
LINUX_GAMING_API_KEY = os.environ.get('LINUX_GAMING_API_KEY')
|
LINUX_GAMING_API_KEY = os.environ.get('LINUX_GAMING_API_KEY')
|
||||||
LINUX_GAMING_API_USERNAME = os.environ.get('LINUX_GAMING_API_USERNAME')
|
LINUX_GAMING_API_USERNAME = os.environ.get('LINUX_GAMING_API_USERNAME')
|
||||||
|
|
||||||
|
# Флаги для включения/отключения источников
|
||||||
|
ENABLE_STEAM = os.environ.get('ENABLE_STEAM', 'true').lower() == 'true'
|
||||||
|
ENABLE_ANTICHEAT = os.environ.get('ENABLE_ANTICHEAT', 'true').lower() == 'true'
|
||||||
|
ENABLE_LINUX_GAMING = os.environ.get('ENABLE_LINUX_GAMING', 'true').lower() == 'true'
|
||||||
|
DEBUG_MODE = os.environ.get('DEBUG_MODE', 'false').lower() == 'true'
|
||||||
|
|
||||||
# Конфигурация API
|
# Конфигурация API
|
||||||
STEAM_BASE_URL = "https://api.steampowered.com/IStoreService/GetAppList/v1/?"
|
STEAM_BASE_URL = "https://api.steampowered.com/IStoreService/GetAppList/v1/?"
|
||||||
LINUX_GAMING_BASE_URL = "https://linux-gaming.ru"
|
LINUX_GAMING_BASE_URL = "https://linux-gaming.ru"
|
||||||
@@ -21,6 +28,10 @@ LINUX_GAMING_HEADERS = {
|
|||||||
"Api-Username": LINUX_GAMING_API_USERNAME
|
"Api-Username": LINUX_GAMING_API_USERNAME
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Отключаем предупреждения об SSL в дебаг-режиме
|
||||||
|
if DEBUG_MODE:
|
||||||
|
print("DEBUG_MODE enabled: SSL verification is disabled (insecure, use for debugging only).")
|
||||||
|
|
||||||
def normalize_name(s):
|
def normalize_name(s):
|
||||||
"""
|
"""
|
||||||
Приведение строки к нормальному виду:
|
Приведение строки к нормальному виду:
|
||||||
@@ -69,7 +80,7 @@ async def get_app_list(session, last_appid, endpoint):
|
|||||||
url = endpoint
|
url = endpoint
|
||||||
if last_appid:
|
if last_appid:
|
||||||
url = f"{url}&last_appid={last_appid}"
|
url = f"{url}&last_appid={last_appid}"
|
||||||
async with session.get(url) as response:
|
async with session.get(url, verify_ssl=not DEBUG_MODE) as response:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return await response.json()
|
return await response.json()
|
||||||
|
|
||||||
@@ -79,7 +90,7 @@ async def fetch_games_json(session):
|
|||||||
"""
|
"""
|
||||||
url = "https://raw.githubusercontent.com/AreWeAntiCheatYet/AreWeAntiCheatYet/HEAD/games.json"
|
url = "https://raw.githubusercontent.com/AreWeAntiCheatYet/AreWeAntiCheatYet/HEAD/games.json"
|
||||||
try:
|
try:
|
||||||
async with session.get(url) as response:
|
async with session.get(url, verify_ssl=not DEBUG_MODE) as response:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
text = await response.text()
|
text = await response.text()
|
||||||
data = json.loads(text)
|
data = json.loads(text)
|
||||||
@@ -89,52 +100,130 @@ async def fetch_games_json(session):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
async def get_linux_gaming_topics(session, category_slug):
|
async def get_linux_gaming_topics(session, category_slug):
|
||||||
"""
|
|
||||||
Получает все темы из указанной категории linux-gaming.ru.
|
|
||||||
Сохраняет только нормализованное название (normalized_title) и slug.
|
|
||||||
"""
|
|
||||||
page = 0
|
page = 0
|
||||||
all_topics = []
|
all_topics = []
|
||||||
|
max_pages = 100
|
||||||
|
|
||||||
while True:
|
while page < max_pages:
|
||||||
page += 1
|
# Пробуем несколько вариантов URL
|
||||||
url = f"{LINUX_GAMING_BASE_URL}/c/{category_slug}/l/latest.json?page={page}"
|
urls_to_try = [
|
||||||
try:
|
f"{LINUX_GAMING_BASE_URL}/c/{category_slug}/5/l/latest.json", # с id категории
|
||||||
async with session.get(url, headers=LINUX_GAMING_HEADERS) as response:
|
f"{LINUX_GAMING_BASE_URL}/c/{category_slug}/l/latest.json", # только slug
|
||||||
response.raise_for_status()
|
f"{LINUX_GAMING_BASE_URL}/c/5/l/latest.json", # только id
|
||||||
data = await response.json()
|
f"{LINUX_GAMING_BASE_URL}/latest.json" # все темы
|
||||||
topics = data.get("topic_list", {}).get("topics", [])
|
]
|
||||||
if not topics:
|
|
||||||
|
success = False
|
||||||
|
data = None
|
||||||
|
|
||||||
|
for url in urls_to_try:
|
||||||
|
try:
|
||||||
|
# Добавляем параметры пагинации
|
||||||
|
params = {
|
||||||
|
'page': page,
|
||||||
|
'order': 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
async with session.get(url, headers=LINUX_GAMING_HEADERS,
|
||||||
|
params=params, verify_ssl=not DEBUG_MODE) as response:
|
||||||
|
if response.status == 429:
|
||||||
|
print(f"Слишком много запросов на странице {page}, ожидание...")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if response.status == 404:
|
||||||
|
if DEBUG_MODE:
|
||||||
|
print(f"URL не найден: {url}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
data = await response.json()
|
||||||
|
|
||||||
|
# Проверяем структуру ответа
|
||||||
|
topic_list = data.get("topic_list", {})
|
||||||
|
topics = topic_list.get("topics", [])
|
||||||
|
|
||||||
|
if not topics:
|
||||||
|
if page == 0:
|
||||||
|
if DEBUG_MODE:
|
||||||
|
print(f"Нет тем в URL: {url}")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
print(f"Страница {page} пуста, завершаем пагинацию.")
|
||||||
|
return all_topics
|
||||||
|
|
||||||
|
if DEBUG_MODE and page == 0:
|
||||||
|
print(f"Успешно подключились к URL: {url}")
|
||||||
|
|
||||||
|
success = True
|
||||||
break
|
break
|
||||||
for topic in topics:
|
|
||||||
all_topics.append({
|
except Exception as e:
|
||||||
"normalized_title": normalize_name(topic["title"]),
|
if DEBUG_MODE:
|
||||||
"slug": topic["slug"]
|
print(f"Ошибка с URL {url}: {e}")
|
||||||
})
|
continue
|
||||||
print(f"Обработано {len(topics)} тем на странице {page}, всего: {len(all_topics)}.")
|
|
||||||
except Exception as error:
|
if not success:
|
||||||
print(f"Ошибка получения тем для страницы {page}: {error}")
|
print(f"Не удалось загрузить страницу {page}")
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Обрабатываем темы (этот блок должен быть внутри основного цикла)
|
||||||
|
try:
|
||||||
|
topic_list = data.get("topic_list", {})
|
||||||
|
topics = topic_list.get("topics", [])
|
||||||
|
|
||||||
|
page_topics_added = 0
|
||||||
|
for topic in topics:
|
||||||
|
slug = topic["slug"]
|
||||||
|
|
||||||
|
# Пропускаем тему описания категории
|
||||||
|
if slug is None or slug == "opisanie-kategorii-portprotondb":
|
||||||
|
if DEBUG_MODE:
|
||||||
|
print(f"Пропущена тема описания категории")
|
||||||
|
continue
|
||||||
|
|
||||||
|
normalized_title = normalize_name(topic["title"])
|
||||||
|
|
||||||
|
# Добавляем только валидные темы
|
||||||
|
all_topics.append({
|
||||||
|
"normalized_title": normalized_title,
|
||||||
|
"slug": slug,
|
||||||
|
})
|
||||||
|
page_topics_added += 1
|
||||||
|
|
||||||
|
if DEBUG_MODE and page_topics_added <= 3: # Показываем первые 3 темы
|
||||||
|
print(f"Добавлена тема: {normalized_title} (slug: {slug}")
|
||||||
|
|
||||||
|
print(f"Обработано {len(topics)} тем на странице {page}, добавлено: {page_topics_added}, всего: {len(all_topics)}.")
|
||||||
|
|
||||||
|
# Проверяем, есть ли еще страницы
|
||||||
|
more_topics_url = topic_list.get("more_topics_url")
|
||||||
|
if not more_topics_url:
|
||||||
|
print("Больше тем нет, завершаем пагинацию.")
|
||||||
|
break
|
||||||
|
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
# Добавляем небольшую задержку между запросами
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при обработке тем на странице {page}: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not all_topics:
|
||||||
|
print("Предупреждение: не удалось получить ни одной темы из linux-gaming.ru.")
|
||||||
|
else:
|
||||||
|
print(f"Всего получено {len(all_topics)} тем из категории {category_slug}")
|
||||||
|
|
||||||
return all_topics
|
return all_topics
|
||||||
|
|
||||||
|
|
||||||
async def request_data():
|
async def request_data():
|
||||||
"""
|
"""
|
||||||
Получает данные из Steam, AreWeAntiCheatYet и linux-gaming.ru,
|
Получает данные из Steam, AreWeAntiCheatYet и linux-gaming.ru,
|
||||||
обрабатывает их и сохраняет в JSON-файлы и tar.xz архивы.
|
обрабатывает их и сохраняет в JSON-файлы и tar.xz архивы.
|
||||||
"""
|
"""
|
||||||
# Параметры запроса для Steam
|
|
||||||
game_param = "&include_games=true"
|
|
||||||
dlc_param = "&include_dlc=false"
|
|
||||||
software_param = "&include_software=false"
|
|
||||||
videos_param = "&include_videos=false"
|
|
||||||
hardware_param = "&include_hardware=false"
|
|
||||||
|
|
||||||
endpoint = (
|
|
||||||
f"{STEAM_BASE_URL}key={STEAM_KEY}"
|
|
||||||
f"{game_param}{dlc_param}{software_param}{videos_param}{hardware_param}"
|
|
||||||
f"&max_results=50000"
|
|
||||||
)
|
|
||||||
|
|
||||||
output_json = []
|
output_json = []
|
||||||
total_parsed = 0
|
total_parsed = 0
|
||||||
linux_gaming_topics = []
|
linux_gaming_topics = []
|
||||||
@@ -143,26 +232,48 @@ async def request_data():
|
|||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
# Загружаем данные Steam
|
# Загружаем данные Steam
|
||||||
have_more_results = True
|
if ENABLE_STEAM:
|
||||||
last_appid_val = None
|
# Параметры запроса для Steam
|
||||||
while have_more_results:
|
game_param = "&include_games=true"
|
||||||
app_list = await get_app_list(session, last_appid_val, endpoint)
|
dlc_param = "&include_dlc=false"
|
||||||
apps = app_list['response']['apps']
|
software_param = "&include_software=false"
|
||||||
apps = process_steam_apps(apps)
|
videos_param = "&include_videos=false"
|
||||||
output_json.extend(apps)
|
hardware_param = "&include_hardware=false"
|
||||||
total_parsed += len(apps)
|
|
||||||
have_more_results = app_list['response'].get('have_more_results', False)
|
endpoint = (
|
||||||
last_appid_val = app_list['response'].get('last_appid')
|
f"{STEAM_BASE_URL}key={STEAM_KEY}"
|
||||||
print(f"Обработано {len(apps)} игр Steam, всего: {total_parsed}.")
|
f"{game_param}{dlc_param}{software_param}{videos_param}{hardware_param}"
|
||||||
|
f"&max_results=50000"
|
||||||
|
)
|
||||||
|
|
||||||
|
have_more_results = True
|
||||||
|
last_appid_val = None
|
||||||
|
while have_more_results:
|
||||||
|
app_list = await get_app_list(session, last_appid_val, endpoint)
|
||||||
|
apps = app_list['response']['apps']
|
||||||
|
apps = process_steam_apps(apps)
|
||||||
|
output_json.extend(apps)
|
||||||
|
total_parsed += len(apps)
|
||||||
|
have_more_results = app_list['response'].get('have_more_results', False)
|
||||||
|
last_appid_val = app_list['response'].get('last_appid')
|
||||||
|
print(f"Обработано {len(apps)} игр Steam, всего: {total_parsed}.")
|
||||||
|
else:
|
||||||
|
print("Пропущена загрузка данных Steam (ENABLE_STEAM=false).")
|
||||||
|
|
||||||
# Загружаем данные AreWeAntiCheatYet
|
# Загружаем данные AreWeAntiCheatYet
|
||||||
anticheat_games = await fetch_games_json(session)
|
if ENABLE_ANTICHEAT:
|
||||||
|
anticheat_games = await fetch_games_json(session)
|
||||||
|
else:
|
||||||
|
print("Пропущена загрузка данных AreWeAntiCheatYet (ENABLE_ANTICHEAT=false).")
|
||||||
|
|
||||||
# Загружаем данные linux-gaming.ru
|
# Загружаем данные linux-gaming.ru
|
||||||
if LINUX_GAMING_API_KEY and LINUX_GAMING_API_USERNAME:
|
if ENABLE_LINUX_GAMING:
|
||||||
linux_gaming_topics = await get_linux_gaming_topics(session, CATEGORY_LINUX_GAMING)
|
if LINUX_GAMING_API_KEY and LINUX_GAMING_API_USERNAME:
|
||||||
|
linux_gaming_topics = await get_linux_gaming_topics(session, CATEGORY_LINUX_GAMING)
|
||||||
|
else:
|
||||||
|
print("Предупреждение: LINUX_GAMING_API_KEY или LINUX_GAMING_API_USERNAME не установлены.")
|
||||||
else:
|
else:
|
||||||
print("Предупреждение: LINUX_GAMING_API_KEY или LINUX_GAMING_API_USERNAME не установлены.")
|
print("Пропущена загрузка данных linux-gaming.ru (ENABLE_LINUX_GAMING=false).")
|
||||||
|
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
print(f"Ошибка получения данных: {error}")
|
print(f"Ошибка получения данных: {error}")
|
||||||
@@ -173,55 +284,55 @@ async def request_data():
|
|||||||
os.makedirs(data_dir, exist_ok=True)
|
os.makedirs(data_dir, exist_ok=True)
|
||||||
|
|
||||||
# Сохранение данных Steam
|
# Сохранение данных Steam
|
||||||
output_json_full = os.path.join(data_dir, f"{CATEGORY_STEAM}_appid.json")
|
if ENABLE_STEAM and output_json:
|
||||||
output_json_min = os.path.join(data_dir, f"{CATEGORY_STEAM}_appid_min.json")
|
output_json_full = os.path.join(data_dir, f"{CATEGORY_STEAM}_appid.json")
|
||||||
with open(output_json_full, "w", encoding="utf-8") as f:
|
output_json_min = os.path.join(data_dir, f"{CATEGORY_STEAM}_appid_min.json")
|
||||||
json.dump(output_json, f, ensure_ascii=False, indent=2)
|
with open(output_json_full, "w", encoding="utf-8") as f:
|
||||||
with open(output_json_min, "w", encoding="utf-8") as f:
|
json.dump(output_json, f, ensure_ascii=False, indent=2)
|
||||||
json.dump(output_json, f, ensure_ascii=False, separators=(',',':'))
|
with open(output_json_min, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(output_json, f, ensure_ascii=False, separators=(',',':'))
|
||||||
|
|
||||||
|
# Упаковка минифицированного JSON Steam в tar.xz архив
|
||||||
|
steam_archive_path = os.path.join(data_dir, f"{CATEGORY_STEAM}_appid.tar.xz")
|
||||||
|
try:
|
||||||
|
with tarfile.open(steam_archive_path, "w:xz", preset=9) as tar:
|
||||||
|
tar.add(output_json_min, arcname=os.path.basename(output_json_min))
|
||||||
|
print(f"Упаковано минифицированное JSON Steam в архив: {steam_archive_path}")
|
||||||
|
os.remove(output_json_min)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при упаковке архива Steam: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
# Сохранение данных AreWeAntiCheatYet
|
# Сохранение данных AreWeAntiCheatYet
|
||||||
anticheat_json_full = os.path.join(data_dir, "anticheat_games.json")
|
if ENABLE_ANTICHEAT and anticheat_games:
|
||||||
anticheat_json_min = os.path.join(data_dir, "anticheat_games_min.json")
|
anticheat_json_full = os.path.join(data_dir, "anticheat_games.json")
|
||||||
with open(anticheat_json_full, "w", encoding="utf-8") as f:
|
anticheat_json_min = os.path.join(data_dir, "anticheat_games_min.json")
|
||||||
json.dump(anticheat_games, f, ensure_ascii=False, indent=2)
|
with open(anticheat_json_full, "w", encoding="utf-8") as f:
|
||||||
with open(anticheat_json_min, "w", encoding="utf-8") as f:
|
json.dump(anticheat_games, f, ensure_ascii=False, indent=2)
|
||||||
json.dump(anticheat_games, f, ensure_ascii=False, separators=(',',':'))
|
with open(anticheat_json_min, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(anticheat_games, f, ensure_ascii=False, separators=(',',':'))
|
||||||
|
|
||||||
|
# Упаковка минифицированного JSON AreWeAntiCheatYet в tar.xz архив
|
||||||
|
anticheat_archive_path = os.path.join(data_dir, "anticheat_games.tar.xz")
|
||||||
|
try:
|
||||||
|
with tarfile.open(anticheat_archive_path, "w:xz", preset=9) as tar:
|
||||||
|
tar.add(anticheat_json_min, arcname=os.path.basename(anticheat_json_min))
|
||||||
|
print(f"Упаковано минифицированное JSON AreWeAntiCheatYet в архив: {anticheat_archive_path}")
|
||||||
|
os.remove(anticheat_json_min)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при упаковке архива AreWeAntiCheatYet: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
# Сохранение данных linux-gaming.ru
|
# Сохранение данных linux-gaming.ru
|
||||||
linux_gaming_json_full = os.path.join(data_dir, "linux_gaming_topics.json")
|
if ENABLE_LINUX_GAMING and linux_gaming_topics:
|
||||||
linux_gaming_json_min = os.path.join(data_dir, "linux_gaming_topics_min.json")
|
linux_gaming_json_full = os.path.join(data_dir, "linux_gaming_topics.json")
|
||||||
if linux_gaming_topics:
|
linux_gaming_json_min = os.path.join(data_dir, "linux_gaming_topics_min.json")
|
||||||
with open(linux_gaming_json_full, "w", encoding="utf-8") as f:
|
with open(linux_gaming_json_full, "w", encoding="utf-8") as f:
|
||||||
json.dump(linux_gaming_topics, f, ensure_ascii=False, indent=2)
|
json.dump(linux_gaming_topics, f, ensure_ascii=False, indent=2)
|
||||||
with open(linux_gaming_json_min, "w", encoding="utf-8") as f:
|
with open(linux_gaming_json_min, "w", encoding="utf-8") as f:
|
||||||
json.dump(linux_gaming_topics, f, ensure_ascii=False, separators=(',',':'))
|
json.dump(linux_gaming_topics, f, ensure_ascii=False, separators=(',',':'))
|
||||||
|
|
||||||
# Упаковка минифицированных JSON в tar.xz архивы
|
# Упаковка минифицированного JSON linux-gaming.ru в tar.xz архив
|
||||||
# Архив для Steam
|
|
||||||
steam_archive_path = os.path.join(data_dir, f"{CATEGORY_STEAM}_appid.tar.xz")
|
|
||||||
try:
|
|
||||||
with tarfile.open(steam_archive_path, "w:xz", preset=9) as tar:
|
|
||||||
tar.add(output_json_min, arcname=os.path.basename(output_json_min))
|
|
||||||
print(f"Упаковано минифицированное JSON Steam в архив: {steam_archive_path}")
|
|
||||||
os.remove(output_json_min)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Ошибка при упаковке архива Steam: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Архив для AreWeAntiCheatYet
|
|
||||||
anticheat_archive_path = os.path.join(data_dir, "anticheat_games.tar.xz")
|
|
||||||
try:
|
|
||||||
with tarfile.open(anticheat_archive_path, "w:xz", preset=9) as tar:
|
|
||||||
tar.add(anticheat_json_min, arcname=os.path.basename(anticheat_json_min))
|
|
||||||
print(f"Упаковано минифицированное JSON AreWeAntiCheatYet в архив: {anticheat_archive_path}")
|
|
||||||
os.remove(anticheat_json_min)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Ошибка при упаковке архива AreWeAntiCheatYet: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Архив для linux-gaming.ru
|
|
||||||
if linux_gaming_topics:
|
|
||||||
linux_gaming_archive_path = os.path.join(data_dir, "linux_gaming_topics.tar.xz")
|
linux_gaming_archive_path = os.path.join(data_dir, "linux_gaming_topics.tar.xz")
|
||||||
try:
|
try:
|
||||||
with tarfile.open(linux_gaming_archive_path, "w:xz", preset=9) as tar:
|
with tarfile.open(linux_gaming_archive_path, "w:xz", preset=9) as tar:
|
||||||
|
@@ -9,6 +9,8 @@ from portprotonqt.config_utils import read_favorites, save_favorites, read_displ
|
|||||||
from portprotonqt.theme_manager import ThemeManager
|
from portprotonqt.theme_manager import ThemeManager
|
||||||
from portprotonqt.config_utils import read_theme_from_config
|
from portprotonqt.config_utils import read_theme_from_config
|
||||||
from portprotonqt.custom_widgets import ClickableLabel
|
from portprotonqt.custom_widgets import ClickableLabel
|
||||||
|
from portprotonqt.portproton_api import PortProtonAPI
|
||||||
|
from portprotonqt.downloader import Downloader
|
||||||
import weakref
|
import weakref
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
@@ -56,6 +58,8 @@ class GameCard(QFrame):
|
|||||||
|
|
||||||
self.display_filter = read_display_filter()
|
self.display_filter = read_display_filter()
|
||||||
self.current_theme_name = read_theme_from_config()
|
self.current_theme_name = read_theme_from_config()
|
||||||
|
self.downloader = Downloader(max_workers=4)
|
||||||
|
self.portproton_api = PortProtonAPI(self.downloader)
|
||||||
|
|
||||||
self.steam_visible = (str(game_source).lower() == "steam" and self.display_filter in ("all", "favorites"))
|
self.steam_visible = (str(game_source).lower() == "steam" and self.display_filter in ("all", "favorites"))
|
||||||
self.egs_visible = (str(game_source).lower() == "epic" and self.display_filter in ("all", "favorites"))
|
self.egs_visible = (str(game_source).lower() == "epic" and self.display_filter in ("all", "favorites"))
|
||||||
@@ -194,13 +198,13 @@ class GameCard(QFrame):
|
|||||||
parent=coverWidget,
|
parent=coverWidget,
|
||||||
icon_size=icon_size,
|
icon_size=icon_size,
|
||||||
icon_space=icon_space,
|
icon_space=icon_space,
|
||||||
font_scale_factor=font_scale_factor,
|
font_scale_factor=font_scale_factor
|
||||||
change_cursor=False
|
|
||||||
)
|
)
|
||||||
self.portprotonLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
|
self.portprotonLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
|
||||||
self.portprotonLabel.setFixedWidth(badge_width)
|
self.portprotonLabel.setFixedWidth(badge_width)
|
||||||
self.portprotonLabel.setCardWidth(card_width)
|
self.portprotonLabel.setCardWidth(card_width)
|
||||||
self.portprotonLabel.setVisible(self.portproton_visible)
|
self.portprotonLabel.setVisible(self.portproton_visible)
|
||||||
|
self.portprotonLabel.clicked.connect(self.open_portproton_forum_topic)
|
||||||
|
|
||||||
# WeAntiCheatYet бейдж
|
# WeAntiCheatYet бейдж
|
||||||
anticheat_text = self.getAntiCheatText(anticheat_status)
|
anticheat_text = self.getAntiCheatText(anticheat_status)
|
||||||
@@ -385,6 +389,16 @@ class GameCard(QFrame):
|
|||||||
return "broken"
|
return "broken"
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
def open_portproton_forum_topic(self):
|
||||||
|
"""Open the PortProton forum topic or search page for this game."""
|
||||||
|
result = self.portproton_api.get_forum_topic_slug(self.name)
|
||||||
|
base_url = "https://linux-gaming.ru/"
|
||||||
|
if result.startswith("search?q="):
|
||||||
|
url = QUrl(f"{base_url}{result}")
|
||||||
|
else:
|
||||||
|
url = QUrl(f"{base_url}t/{result}")
|
||||||
|
QDesktopServices.openUrl(url)
|
||||||
|
|
||||||
def open_protondb_report(self):
|
def open_protondb_report(self):
|
||||||
url = QUrl(f"https://www.protondb.com/app/{self.appid}")
|
url = QUrl(f"https://www.protondb.com/app/{self.appid}")
|
||||||
QDesktopServices.openUrl(url)
|
QDesktopServices.openUrl(url)
|
||||||
|
@@ -248,6 +248,16 @@ class MainWindow(QMainWindow):
|
|||||||
self.updateGameGrid()
|
self.updateGameGrid()
|
||||||
self.progress_bar.setVisible(False)
|
self.progress_bar.setVisible(False)
|
||||||
|
|
||||||
|
def open_portproton_forum_topic(self, topic_name: str):
|
||||||
|
"""Open the PortProton forum topic or search page for this game."""
|
||||||
|
result = self.portproton_api.get_forum_topic_slug(topic_name)
|
||||||
|
base_url = "https://linux-gaming.ru/"
|
||||||
|
if result.startswith("search?q="):
|
||||||
|
url = QUrl(f"{base_url}{result}")
|
||||||
|
else:
|
||||||
|
url = QUrl(f"{base_url}t/{result}")
|
||||||
|
QDesktopServices.openUrl(url)
|
||||||
|
|
||||||
def _on_card_focused(self, game_name: str, is_focused: bool):
|
def _on_card_focused(self, game_name: str, is_focused: bool):
|
||||||
"""Обработчик сигнала focusChanged от GameCard."""
|
"""Обработчик сигнала focusChanged от GameCard."""
|
||||||
card_key = None
|
card_key = None
|
||||||
@@ -1639,11 +1649,11 @@ class MainWindow(QMainWindow):
|
|||||||
parent=coverFrame,
|
parent=coverFrame,
|
||||||
icon_size=16,
|
icon_size=16,
|
||||||
icon_space=5,
|
icon_space=5,
|
||||||
change_cursor=False
|
|
||||||
)
|
)
|
||||||
portprotonLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
|
portprotonLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
|
||||||
portprotonLabel.setFixedWidth(badge_width)
|
portprotonLabel.setFixedWidth(badge_width)
|
||||||
portprotonLabel.setVisible(portproton_visible)
|
portprotonLabel.setVisible(portproton_visible)
|
||||||
|
portprotonLabel.clicked.connect(lambda: self.open_portproton_forum_topic(name))
|
||||||
|
|
||||||
# WeAntiCheatYet бейдж
|
# WeAntiCheatYet бейдж
|
||||||
anticheat_text = GameCard.getAntiCheatText(anticheat_status)
|
anticheat_text = GameCard.getAntiCheatText(anticheat_status)
|
||||||
|
@@ -1,19 +1,58 @@
|
|||||||
import os
|
import os
|
||||||
|
import tarfile
|
||||||
|
import orjson
|
||||||
import requests
|
import requests
|
||||||
|
import urllib.parse
|
||||||
|
import time
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from portprotonqt.downloader import Downloader, download_with_cache
|
from portprotonqt.downloader import Downloader
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
CACHE_DURATION = 30 * 24 * 60 * 60 # 30 days in seconds
|
||||||
|
|
||||||
|
def normalize_name(s):
|
||||||
|
"""
|
||||||
|
Приведение строки к нормальному виду:
|
||||||
|
- перевод в нижний регистр,
|
||||||
|
- удаление символов ™ и ®,
|
||||||
|
- замена разделителей (-, :, ,) на пробел,
|
||||||
|
- удаление лишних пробелов,
|
||||||
|
- удаление суффиксов 'bin' или 'app' в конце строки,
|
||||||
|
- удаление ключевых слов типа 'ultimate', 'edition' и т.п.
|
||||||
|
"""
|
||||||
|
s = s.lower()
|
||||||
|
for ch in ["™", "®"]:
|
||||||
|
s = s.replace(ch, "")
|
||||||
|
for ch in ["-", ":", ","]:
|
||||||
|
s = s.replace(ch, " ")
|
||||||
|
s = " ".join(s.split())
|
||||||
|
for suffix in ["bin", "app"]:
|
||||||
|
if s.endswith(suffix):
|
||||||
|
s = s[:-len(suffix)].strip()
|
||||||
|
|
||||||
|
keywords_to_remove = {"ultimate", "edition", "definitive", "complete", "remastered"}
|
||||||
|
words = s.split()
|
||||||
|
filtered_words = [word for word in words if word not in keywords_to_remove]
|
||||||
|
return " ".join(filtered_words)
|
||||||
|
|
||||||
|
def get_cache_dir():
|
||||||
|
"""Return the cache directory path, creating it if necessary."""
|
||||||
|
xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache"))
|
||||||
|
cache_dir = os.path.join(xdg_cache_home, "PortProtonQt")
|
||||||
|
os.makedirs(cache_dir, exist_ok=True)
|
||||||
|
return cache_dir
|
||||||
|
|
||||||
class PortProtonAPI:
|
class PortProtonAPI:
|
||||||
"""API to fetch game assets (cover, metadata) from the PortProtonQt repository."""
|
"""API to fetch game assets (cover, metadata) and forum topics from the PortProtonQt repository."""
|
||||||
def __init__(self, downloader: Downloader | None = None):
|
def __init__(self, downloader: Downloader | None = None):
|
||||||
self.base_url = "https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/portprotonqt/custom_data"
|
self.base_url = "https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/portprotonqt/custom_data"
|
||||||
|
self.topics_url = "https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/linux_gaming_topics.tar.xz"
|
||||||
self.downloader = downloader or Downloader(max_workers=4)
|
self.downloader = downloader or Downloader(max_workers=4)
|
||||||
self.xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
|
self.xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
|
||||||
self.custom_data_dir = os.path.join(self.xdg_data_home, "PortProtonQt", "custom_data")
|
self.custom_data_dir = os.path.join(self.xdg_data_home, "PortProtonQt", "custom_data")
|
||||||
os.makedirs(self.custom_data_dir, exist_ok=True)
|
os.makedirs(self.custom_data_dir, exist_ok=True)
|
||||||
|
self._topics_data = None
|
||||||
|
|
||||||
def _get_game_dir(self, exe_name: str) -> str:
|
def _get_game_dir(self, exe_name: str) -> str:
|
||||||
game_dir = os.path.join(self.custom_data_dir, exe_name)
|
game_dir = os.path.join(self.custom_data_dir, exe_name)
|
||||||
@@ -40,7 +79,7 @@ class PortProtonAPI:
|
|||||||
cover_url = f"{cover_url_base}{ext}"
|
cover_url = f"{cover_url_base}{ext}"
|
||||||
if self._check_file_exists(cover_url, timeout):
|
if self._check_file_exists(cover_url, timeout):
|
||||||
local_cover_path = os.path.join(game_dir, f"cover{ext}")
|
local_cover_path = os.path.join(game_dir, f"cover{ext}")
|
||||||
result = download_with_cache(cover_url, local_cover_path, timeout, self.downloader)
|
result = self.downloader.download(cover_url, local_cover_path, timeout=timeout)
|
||||||
if result:
|
if result:
|
||||||
results["cover"] = result
|
results["cover"] = result
|
||||||
logger.info(f"Downloaded cover for {exe_name} to {result}")
|
logger.info(f"Downloaded cover for {exe_name} to {result}")
|
||||||
@@ -52,7 +91,7 @@ class PortProtonAPI:
|
|||||||
|
|
||||||
if self._check_file_exists(metadata_url, timeout):
|
if self._check_file_exists(metadata_url, timeout):
|
||||||
local_metadata_path = os.path.join(game_dir, "metadata.txt")
|
local_metadata_path = os.path.join(game_dir, "metadata.txt")
|
||||||
result = download_with_cache(metadata_url, local_metadata_path, timeout, self.downloader)
|
result = self.downloader.download(metadata_url, local_metadata_path, timeout=timeout)
|
||||||
if result:
|
if result:
|
||||||
results["metadata"] = result
|
results["metadata"] = result
|
||||||
logger.info(f"Downloaded metadata for {exe_name} to {result}")
|
logger.info(f"Downloaded metadata for {exe_name} to {result}")
|
||||||
@@ -123,3 +162,66 @@ class PortProtonAPI:
|
|||||||
logger.debug(f"No assets found for {exe_name}")
|
logger.debug(f"No assets found for {exe_name}")
|
||||||
if callback:
|
if callback:
|
||||||
callback(results)
|
callback(results)
|
||||||
|
|
||||||
|
def _load_topics_data(self):
|
||||||
|
"""Load and cache linux_gaming_topics_min.json from the archive."""
|
||||||
|
if self._topics_data is not None:
|
||||||
|
return self._topics_data
|
||||||
|
|
||||||
|
cache_dir = get_cache_dir()
|
||||||
|
cache_tar = os.path.join(cache_dir, "linux_gaming_topics.tar.xz")
|
||||||
|
cache_json = os.path.join(cache_dir, "linux_gaming_topics_min.json")
|
||||||
|
|
||||||
|
if os.path.exists(cache_json) and (time.time() - os.path.getmtime(cache_json) < CACHE_DURATION):
|
||||||
|
logger.info("Using cached topics JSON: %s", cache_json)
|
||||||
|
try:
|
||||||
|
with open(cache_json, "rb") as f:
|
||||||
|
self._topics_data = orjson.loads(f.read())
|
||||||
|
logger.debug("Loaded %d topics from cache", len(self._topics_data))
|
||||||
|
return self._topics_data
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Error reading cached topics JSON: %s", e)
|
||||||
|
self._topics_data = []
|
||||||
|
|
||||||
|
def process_tar(result: str | None):
|
||||||
|
if not result or not os.path.exists(result):
|
||||||
|
logger.error("Failed to download topics archive")
|
||||||
|
self._topics_data = []
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
with tarfile.open(result, mode="r:xz") as tar:
|
||||||
|
member = next((m for m in tar.getmembers() if m.name == "linux_gaming_topics_min.json"), None)
|
||||||
|
if member is None:
|
||||||
|
raise RuntimeError("linux_gaming_topics_min.json not found in archive")
|
||||||
|
fobj = tar.extractfile(member)
|
||||||
|
if fobj is None:
|
||||||
|
raise RuntimeError("Failed to extract linux_gaming_topics_min.json from archive")
|
||||||
|
raw = fobj.read()
|
||||||
|
fobj.close()
|
||||||
|
self._topics_data = orjson.loads(raw)
|
||||||
|
with open(cache_json, "wb") as f:
|
||||||
|
f.write(orjson.dumps(self._topics_data))
|
||||||
|
if os.path.exists(cache_tar):
|
||||||
|
os.remove(cache_tar)
|
||||||
|
logger.info("Archive %s deleted after extraction", cache_tar)
|
||||||
|
logger.info("Loaded %d topics from archive", len(self._topics_data))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Error processing topics archive: %s", e)
|
||||||
|
self._topics_data = []
|
||||||
|
|
||||||
|
self.downloader.download_async(self.topics_url, cache_tar, timeout=5, callback=process_tar)
|
||||||
|
# Wait for async download to complete if called synchronously
|
||||||
|
while self._topics_data is None:
|
||||||
|
time.sleep(0.1)
|
||||||
|
return self._topics_data
|
||||||
|
|
||||||
|
def get_forum_topic_slug(self, game_name: str) -> str:
|
||||||
|
"""Get the forum topic slug or search URL for a given game name."""
|
||||||
|
topics = self._load_topics_data()
|
||||||
|
normalized_name = normalize_name(game_name)
|
||||||
|
for topic in topics:
|
||||||
|
if topic["normalized_title"] == normalized_name:
|
||||||
|
return topic["slug"]
|
||||||
|
logger.debug("No forum topic found for game: %s, redirecting to search", game_name)
|
||||||
|
encoded_name = urllib.parse.quote(f"#ppdb {game_name}")
|
||||||
|
return f"search?q={encoded_name}"
|
||||||
|
Reference in New Issue
Block a user