ebenbruyns / junkstore

Imagine a Kodi like interface for all your non-Steam games! Transform your gaming experience with Junk-Store. Say goodbye to clunky work arounds and hello to a world of endless gaming possibilities. Get ready to elevate your gaming to the next level with Junk-Store!
https://junkstore.xyz/
Other
312 stars 12 forks source link

Itch.io Integration #46

Open Aeonitis opened 5 months ago

Aeonitis commented 5 months ago

Is your feature request related to a problem? Please describe. Integration of Itch.io games into the Junkstore plugin. First of all, I love all your work here!

I would love to do this but I am unfortunately busy on my own non-steam app :'( Just adding this here so you can correct me, and make it easier for others to jump in and smash it into reality. No rush in responding, take your time!

Draft Plan

### Operation Itch
- [ ] API - User Browser Third-Party Login
- [ ] API - Fetch my_games list
- [ ] API - Get game details
- [ ] API - Download images
- [ ] API - Install Game
- [ ] DB  - Persist to db GameSet
- [ ] UI  - Update UX for download progress
- [ ] UI  - Update UI (e.g. Itch Tab)

Next Steps

Current Observations [Epic Integration]

Login

How the third-party in-game-mode browser user login is handled for other stores (Epic)...

Uses tool to authenticate via CLI on Epic specific launcher Legendary https://github.com/ebenbruyns/junkstore/blob/e115250dad6dbf9d4a1201a8d92982087b32b980/defaults/scripts/Extensions/Epic/login.sh#L13

Arg info from Legendary documentation -v, --debug means Set loglevel to debug

Current Auth ID data in game logs, stored as appId.log e.g. cd1b8a6********************cf7b7f536.log which is 'Night in the Woods'.

-AUTH_LOGIN=unused: Login method not being used, userId is optional.
-AUTH_PASSWORD=8fe9*****************************422e: Hashed user password used for authentication.
-AUTH_TYPE=exchangecode: Specifies type of authentication being used, exchange code.
-epicapp=cd1b8a6********************cf7b7f536: Unique id for this Epic Games app.
-epicenv=Prod: Set to production.
-epicusername=Aeonitis: Your Epic Games account username.
-epicuserid=8d7********************************6c: Unique user ID for the Epic Games account.
-epiclocale=en: locale/language as English.
-epicsandboxid=abd***********************************3: Identifier for the sandbox environment being used.
Running: “/home/deck/.local/share/Steam/ubuntu12_32/reaper”: Path to executable being run.
“SteamLaunch” “AppId=4**********7” “–”: Steam launcher being used with specified app ID.
“/home/deck/.local/share/Steam/ubuntu12_32/steam-launch-wrapper”: Path to the Steam launch wrapper being used.

Client caching on deck is handled by sqlite 3:

https://github.com/ebenbruyns/junkstore/blob/befe4d055035c5dddcf933af21f077f8b442fe6d/defaults/scripts/shared/GamesDb.py#L2 & https://github.com/ebenbruyns/junkstore/blob/befe4d055035c5dddcf933af21f077f8b442fe6d/defaults/scripts/shared/GameSet.py#L10

Questions

Fundamental questions here to help make the process easier for whichever dev takes on some of this work, based on what I learned from skimming through the repo and especially this commit

Describe the solution we'd like

  1. API integration

    • Sample Shell Script:

      #!/bin/bash
      
      # Replace 'OUR_API_KEY' with our actual itch.io API key
      API_KEY="OUR_API_KEY"
      
      # Function to get user info
      login() {
      curl -H "Authorization: Bearer $API_KEY" "https://itch.io/api/1/key/me"
      }
      
      # Function to get list of our games
      get_my_games() {
      curl -H "Authorization: Bearer $API_KEY" "https://itch.io/api/1/key/my-games"
      }
      
      # Function to download a game by ID
      download_game() {
      game_id=$1
      curl -H "Authorization: Bearer $API_KEY" "https://itch.io/api/1/key/game/$game_id/download"
      }
      
      # Example usage
      echo "Logging in..."
      login
      
      echo "Fetching list of games..."
      get_my_games
      
      # Replace 'GAME_ID' with ID of game we want to download
      GAME_ID="GAME_ID"
      echo "Downloading game with ID: $GAME_ID"
      download_game $GAME_ID
    • Sample Python Code:

      import requests
      
      # Replace 'OUR_API_KEY' with our actual itch.io API key
      api_key = 'OUR_API_KEY'
      headers = {'Authorization': f'Bearer {api_key}'}
      
      # Function to log in and get our user info
      def login():
       response = requests.get('https://itch.io/api/1/key/me', headers=headers)
       response.raise_for_status()  # Will raise an exception for HTTP errors
       return response.json()
      
      # Function to get a list of our games
      def get_my_games():
       response = requests.get('https://itch.io/api/1/key/my-games', headers=headers)
       response.raise_for_status()
       return response.json()['games']
      
      # Function to download a game by ID
      def download_game(game_id):
       download_url = f'https://itch.io/api/1/key/game/{game_id}/download'
       response = requests.get(download_url, headers=headers)
       response.raise_for_status()
       return response.json()
      
      # Example usage
      user_info = login()
      print(f"Logged in as: {user_info['user']['username']}")
      
      my_games = get_my_games()
      print("My games:")
      for game in my_games:
       print(f"{game['id']}: {game['title']}")
      
      # Replace 'GAME_ID' with ID of game we want to download
      game_id = 'GAME_ID'
      game_download_info = download_game(game_id)
      print(f"Download URL for {game_id}: {game_download_info['url']}")
  2. Scripting and Configuration Adjustments

    • Add defaults/scripts/Extensions/Itch/store.sh:
      • Handle itch.io specific command-line operations, such as game list refreshes, downloads, and status checks.
      • Implement filtering and caching mechanisms appropriate for itch.io's data structure and update frequency.
    • Add defaults/scripts/itch-config.py:
      • Config handling script to incorporate itch.io as selectable store.
      • Add necessary args and environment variables to manage interactions for itch.io.
    • Backend Python Modifications

      • Add defaults/scripts/itch.py:
      • Develop or extend a Python class specifically for interacting with itch.io, handling API calls, data processing, and exception management.
      • Implement methods for fetching game lists, game details, downloading images, and other media associated with games.
      • Revise defaults/scripts/shared/GameSet.py and GamesDb.py:
      • Update shared base classes to include methods and attributes that support itch.io's integration.
      • Ensure that database schema accommodates any new itch.io-specific fields (e.g., DRM status (May not need), direct download links).
    • Database Schema Enhancement

      • Modify Database Tables (GameSet.py?):
      • Update existing tables or create new ones specific to itch.io if necessary, ensuring all game metadata and image links are correctly stored and indexed.
  3. Frontend Integration

    • Adjust src/Components/GameDetailsItem.tsx:
      • Update TypeScript component to handle display and interaction of itch.io game details within this plugin's UI.
      • Ensure that any new data elements specific to itch.io (like DRM-free indicators) are appropriately rendered.

Again, my two cents just to push us a little forward, Keep up the awesome work :D

snowkat commented 1 month ago

As I understand it, the itch.io server-side and OAuth APIs are for sellers to provide integration for games, and there isn't a way to use it for getting games you bought, generate download URLs, etc. The official itch app (https://github.com/itchio/itch) uses butler, but the APIs it uses require running it as a JSON-RPC daemon.

The butlerd docs are available at https://docs.itch.ovh/butlerd/master/, but as a general reference, getting authenticated works like such:

  1. Spawn a new butler process. For example:
    butler --json                        \
        --user-agent "test app (Linux)"  \
        daemon                           \
        --dbpath ".../path/to/butler.db"
  2. Connect to the TCP server by listening for the message on stdout, as described in https://docs.itch.ovh/butlerd/master/#/?id=making-requests. Notably, every TCP connection needs to call Meta.Authenticate with the secret provided over stdout.
  3. (Optional) Try getting a saved profile by calling Profile.List and Profile.UseSavedLogin. If you get a profile this way, you're done! Skip to step 7.
  4. Show the user a form with a username and password, and use them as parameters when calling Profile.LoginWithPassword.[^pw]
  5. Before Profile.LoginWithPassword returns, you may receive a couple server->client calls, Profile.RequestCaptcha and/or Profile.RequestTOTP.
    • Profile.RequestTOTP is simple: ask the user for the TOTP token, and respond.
    • Profile.RequestCaptcha is, bluntly, a mess. More on that in a moment.
  6. Once any requested challenges have been completed, Profile.LoginWithPassword returns with the logged in profile.
  7. Save the returned profile, or at least its profileId field, for use in later requests (e.g. Fetch.GameRecords).

For Profile.RequestCaptcha, you'll be provided a recaptchaUrl, most likely https://itch.io/captcha. This page needs to be embedded as a BrowserView or new window (not an iframe[^frame]) where you can inject JS to catch the CAPTCHA response. For example:

// function called by itch's reCAPTCHA on success.
// otherwise you could watch document.getElementById("g-recaptcha-response")
function onRecaptcha(response) {
    // "namespace" our message so we don't try to consume a stray Steam message
    window.opener.postMessage("__ITCH_CAPTCHA_" + response, "*");
}

Once solved, you can return the response string in the RPC call.

The other API calls are pretty straightforward, but handling two-way JSON-RPC in shell scripts would be a challenge. Logs are also sent through JSON-RPC calls, so you'd likely want at least one long-lived TCP connection to output them.

[^pw]: I was able to pull this off with a modal dialog. It doesn't feel great, but it works... [^frame]: https://itch.io/captcha sets X-Frame-Options: SAMEORIGIN, so we can't embed it in a frame.