├── .gitattributes ├── requirements.txt ├── readmeimages ├── example1.png ├── example2.png ├── example3.png ├── steamDB.png └── defaulticon.png ├── runningApps.py ├── LICENSE ├── flake.nix ├── uninstall.sh ├── exampleconfig.json ├── flake.lock ├── .gitignore ├── nix ├── pkgs │ └── steam-presence │ │ └── default.nix └── nixos-modules │ └── steam-presence.nix ├── installer.sh ├── steam-presence.service ├── README.md └── main.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-steamgriddb 2 | pypresence == 4.3.0 3 | beautifulsoup4 4 | requests 5 | psutil 6 | -------------------------------------------------------------------------------- /readmeimages/example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustTemmie/steam-presence/HEAD/readmeimages/example1.png -------------------------------------------------------------------------------- /readmeimages/example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustTemmie/steam-presence/HEAD/readmeimages/example2.png -------------------------------------------------------------------------------- /readmeimages/example3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustTemmie/steam-presence/HEAD/readmeimages/example3.png -------------------------------------------------------------------------------- /readmeimages/steamDB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustTemmie/steam-presence/HEAD/readmeimages/steamDB.png -------------------------------------------------------------------------------- /readmeimages/defaulticon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustTemmie/steam-presence/HEAD/readmeimages/defaulticon.png -------------------------------------------------------------------------------- /runningApps.py: -------------------------------------------------------------------------------- 1 | try: 2 | import psutil 3 | except Exception as e: 4 | print(f"doesn't seem like psutil is installed\n{e}") 5 | exit() 6 | 7 | print("press enter to print all applications open on the current device") 8 | input() 9 | 10 | for process in psutil.process_iter(): 11 | print(process.name()) 12 | 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-Curent GitHub user JustTemmie and Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A Nix flake for steam-presence"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | }; 8 | 9 | outputs = { 10 | self, 11 | nixpkgs, 12 | flake-utils, 13 | ... 14 | }: 15 | flake-utils.lib.eachDefaultSystem (system: let 16 | pkgs = import nixpkgs {inherit system;}; 17 | 18 | steam-presence-src = pkgs.lib.cleanSource ./.; 19 | 20 | steam-presence-pkg = pkgs.callPackage ./nix/pkgs/steam-presence { 21 | src = steam-presence-src; 22 | }; 23 | in { 24 | packages = { 25 | steam-presence = steam-presence-pkg; 26 | default = steam-presence-pkg; 27 | }; 28 | 29 | apps = { 30 | steam-presence = { 31 | type = "app"; 32 | program = "${steam-presence-pkg}/bin/steam-presence"; 33 | }; 34 | default = { 35 | type = "app"; 36 | program = "${steam-presence-pkg}/bin/steam-presence"; 37 | }; 38 | }; 39 | }) 40 | // { 41 | nixosModules = { 42 | steam-presence = import ./nix/nixos-modules/steam-presence.nix; 43 | }; 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Uninstalling steam-presence for Linux and macOS. Please confirm you want to continue." 4 | read -p "Press the enter key to continue..." 5 | echo "" 6 | 7 | # Detect OS and perform OS-specific uninstall tasks 8 | if [[ "$OSTYPE" == "linux-gnu"* ]]; then 9 | echo "Stopping and disabling service for Linux" 10 | systemctl --user stop steam-presence.service 11 | systemctl --user disable steam-presence.service 12 | rm "$HOME/.config/systemd/user/steam-presence.service" 13 | systemctl --user daemon-reload 14 | elif [[ "$OSTYPE" == "darwin"* ]]; then 15 | echo "Stopping and unloading service for macOS" 16 | launchctl unload "$HOME/Library/LaunchAgents/com.github.justtemmie.steam-presence.plist" 17 | rm "$HOME/Library/LaunchAgents/com.github.justtemmie.steam-presence.plist" 18 | else 19 | echo "Unsupported OS. This uninstall script is designed for Linux and macOS only." 20 | exit 1 21 | fi 22 | 23 | # Remove virtual environment 24 | echo "Removing virtual environment" 25 | rm -rf venv 26 | 27 | # Remove wrapper script if on macOS 28 | if [[ "$OSTYPE" == "darwin"* ]]; then 29 | echo "Removing wrapper script" 30 | rm steam-presence 31 | fi 32 | 33 | echo "Uninstallation completed. If you want to remove any other files created by steam-presence, please do so manually." 34 | -------------------------------------------------------------------------------- /exampleconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "STEAM_API_KEY": "STEAM_API_KEY", 3 | "USER_IDS": "USER_ID", 4 | 5 | "DISCORD_APPLICATION_ID": "869994714093465680", 6 | 7 | "FETCH_STEAM_RICH_PRESENCE": true, 8 | "FETCH_STEAM_REVIEWS": false, 9 | "ADD_STEAM_STORE_BUTTON": false, 10 | 11 | "WEB_SCRAPE": false, 12 | 13 | "COVER_ART": { 14 | "STEAM_GRID_DB": { 15 | "ENABLED": false, 16 | "STEAM_GRID_API_KEY": "STEAM_GRID_API_KEY" 17 | }, 18 | "USE_STEAM_STORE_FALLBACK": true 19 | }, 20 | 21 | "LOCAL_GAMES": { 22 | "ENABLED": false, 23 | "LOCAL_DISCORD_APPLICATION_ID": "1062648118375616594", 24 | "GAMES": [ 25 | "processName1", 26 | "processName2", 27 | "processName3", 28 | "so on" 29 | ] 30 | }, 31 | 32 | "GAME_OVERWRITE": { 33 | "ENABLED": false, 34 | "NAME": "Breath of the wild, now on steam!", 35 | "SECONDS_SINCE_START": 0 36 | }, 37 | 38 | "CUSTOM_ICON": { 39 | "ENABLED": false, 40 | "URL": "https://raw.githubusercontent.com/JustTemmie/steam-presence/main/readmeimages/defaulticon.png", 41 | "TEXT": "Steam Presence on Discord" 42 | }, 43 | 44 | "BLACKLIST": [ 45 | "game1", 46 | "game2", 47 | "game3" 48 | ] 49 | } 50 | 51 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1757745802, 24 | "narHash": "sha256-hLEO2TPj55KcUFUU1vgtHE9UEIOjRcH/4QbmfHNF820=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "c23193b943c6c689d70ee98ce3128239ed9e32d1", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # custom 2 | config.json 3 | steam-cookies.txt 4 | cookies.txt 5 | Development/ 6 | data/ 7 | BrowserTest/ 8 | result 9 | 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | MANIFEST 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .nox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *.cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | .python-version 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | -------------------------------------------------------------------------------- /nix/pkgs/steam-presence/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | stdenv, 4 | makeWrapper, 5 | src, 6 | python3, 7 | python3Packages, 8 | fetchPypi, 9 | bash, 10 | coreutils, 11 | }: let 12 | python-steamgriddb = python3Packages.buildPythonPackage rec { 13 | pname = "python-steamgriddb"; 14 | version = "1.0.5"; 15 | src = fetchPypi { 16 | inherit pname version; 17 | hash = "sha256-A223uwmGXac7QLaM8E+5Z1zRi0kIJ1CS2R83vxYkUGk="; 18 | }; 19 | pyproject = true; 20 | build-system = [python3Packages.setuptools]; 21 | propagatedBuildInputs = [ 22 | python3Packages.requests 23 | ]; 24 | doCheck = false; 25 | }; 26 | 27 | pythonEnv = python3.withPackages (ps: [ 28 | ps.requests 29 | ps.beautifulsoup4 30 | ps.pypresence 31 | ps.psutil 32 | python-steamgriddb 33 | ]); 34 | in 35 | stdenv.mkDerivation { 36 | pname = "steam-presence"; 37 | version = "1.12.2"; 38 | 39 | inherit src; 40 | nativeBuildInputs = [ 41 | makeWrapper 42 | ]; 43 | installPhase = '' 44 | runHook preInstall 45 | mkdir -p $out/share/steam-presence 46 | cp -r ./* $out/share/steam-presence 47 | 48 | mkdir -p $out/bin 49 | cat > $out/bin/steam-presence <<'EOF' 50 | #!${bash}/bin/bash 51 | set -eo pipefail 52 | 53 | # Determine runtime directory (env overrides default) 54 | RUNTIME_DIR="''${STEAM_PRESENCE_RUNTIME_DIR:-$HOME/.local/state/steam-presence}" 55 | 56 | # Store app dir (where upstream sources live) 57 | STORE_APP_DIR="$out/share/steam-presence" 58 | 59 | # Ensure runtime dir exists and seed it if main.py is missing 60 | if [ ! -f "$RUNTIME_DIR/main.py" ]; then 61 | ${coreutils}/bin/mkdir -p "$RUNTIME_DIR" 62 | ${coreutils}/bin/cp -r "$STORE_APP_DIR/." "$RUNTIME_DIR/" 63 | fi 64 | 65 | cd "$RUNTIME_DIR" 66 | exec ${pythonEnv}/bin/python "main.py" 67 | EOF 68 | chmod +x $out/bin/steam-presence 69 | 70 | runHook postInstall 71 | ''; 72 | meta = with lib; { 73 | description = "A simple script to fetch a Steam user's current game and related info, and display it as a Discord rich presence"; 74 | homepage = "https://github.com/JustTemmie/steam-presence"; 75 | license = licenses.mit; 76 | maintainers = with maintainers; []; 77 | mainProgram = "steam-presence"; 78 | platforms = platforms.linux; 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /installer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "This script is for Linux and macOS, and it's still in testing phases. If you encounter any bugs, please open an issue." 4 | read -p "Press the enter key to continue..." 5 | echo "" 6 | 7 | # Check if script is running as root 8 | if [ `whoami` == "root" ]; then 9 | echo "This script cannot be run as root. Please run as a regular user" 10 | exit 1 11 | fi 12 | 13 | # Check if config.json file exists 14 | if [ ! -f "config.json" ]; then 15 | echo "Error: config.json file not found. Please make sure it exists in the current directory" 16 | echo "files in current directory:" 17 | ls 18 | exit 1 19 | fi 20 | 21 | # Create necessary files if they don't exist 22 | for file in games.txt icons.txt customGameIDs.json; do 23 | if [ ! -e "$(pwd)/$file" ]; then 24 | echo "creating $file" 25 | [ "$file" == "customGameIDs.json" ] && echo "{}" > "$(pwd)/$file" || touch "$(pwd)/$file" 26 | fi 27 | done 28 | 29 | echo "Setting up virtual environment" 30 | mkdir venv 31 | cd venv 32 | python3 -m venv . 33 | ./bin/python -m pip install --upgrade pip wheel 34 | ./bin/python -m pip install -r ../requirements.txt 35 | cd .. 36 | 37 | echo "" 38 | echo "Testing if the script works" 39 | echo "" 40 | 41 | # Run the Python script in the background 42 | ./venv/bin/python ./main.py & 43 | # Get the background process's PID 44 | PYTHON_PID=$! 45 | # Sleep for 2.5 seconds 46 | sleep 2.5 47 | # Kill the background process 48 | kill $PYTHON_PID 49 | 50 | echo "Test might've worked, did it spit out any errors?" 51 | echo "Commands executed." 52 | 53 | # Create a shell script wrapper for macOS 54 | if [[ "$OSTYPE" == "darwin"* ]]; then 55 | echo '#!/bin/bash' > steam-presence 56 | echo "DIR=\"\$( cd \"\$( dirname \"\${BASH_SOURCE[0]}\" )\" && pwd )\"" >> steam-presence 57 | echo "exec \"\$DIR/venv/bin/python\" \"\$DIR/main.py\"" >> steam-presence 58 | chmod +x steam-presence 59 | fi 60 | 61 | # Detect OS and perform OS-specific tasks 62 | if [[ "$OSTYPE" == "linux-gnu"* || "$OSTYPE" == "linux" ]]; then 63 | echo "Setting up service file for Linux" 64 | mkdir -p "$HOME/.config/systemd/user" 65 | sed -e "s~steam-presence/bin/python~steam-presence/venv/bin/python~g" -e "s~/home/deck/steam-presence~$PWD~g" "$PWD/steam-presence.service" > $HOME/.config/systemd/user/steam-presence.service 66 | echo "Starting service" 67 | systemctl --user daemon-reload 68 | systemctl --user --now enable "steam-presence.service" 69 | elif [[ "$OSTYPE" == "darwin"* ]]; then 70 | echo "Setting up launchd plist file for macOS" 71 | PLIST="$HOME/Library/LaunchAgents/com.github.justtemmie.steam-presence.plist" 72 | cat < $PLIST 73 | 74 | 75 | 76 | 77 | Label 78 | Steam Presence 79 | ProgramArguments 80 | 81 | $(pwd)/steam-presence 82 | 83 | RunAtLoad 84 | 85 | StandardOutPath 86 | $(pwd)/steam-presence.log 87 | StandardErrorPath 88 | $(pwd)/steam-presence-error.log 89 | 90 | 91 | EOL 92 | echo "Starting service" 93 | launchctl load $PLIST 94 | else 95 | echo "Unsupported OS. This script is designed for Linux and macOS only." 96 | exit 1 97 | fi 98 | 99 | echo "If you encountered any errors with this script, please create an issue on the GitHub page" 100 | echo "" 101 | echo "Script completed." 102 | -------------------------------------------------------------------------------- /steam-presence.service: -------------------------------------------------------------------------------- 1 | # This is a Systemd service file for steam-presence! 2 | # Using this, you can have steam-presence automatically start on Steam Deck, 3 | # even if you're using the Steam Deck in Gaming mode. 4 | 5 | # Systemd is a "system manager". After the low-level OS starts up, Systemd is 6 | # responsible for running the programs that do things like connect you to the 7 | # network, start Bluetooth, and launch Steam. It's also possible to have 8 | # Systemd handle other things, like automatically launching steam-presence! 9 | 10 | # This file contains the information that Systemd needs to know how to 11 | # launch steam-presence. HOWEVER, Systemd is not smart enough to automatically 12 | # configure steam-presence. So, before you can use this, you have to get 13 | # steam-presence working first. 14 | 15 | # WARNING: This is for advanced users only! We can't really help you if things 16 | # go weird, because it is really hard to provide remote support. Happily, none 17 | # of the things done here will modify the OS, so you're not going to damage your 18 | # Deck. 19 | 20 | # Here is what to do. Remember, get Part 1 working before going to Part 2, etc. 21 | # 22 | # PART 1: Get and configure Discord 23 | # * On your Deck, go to Desktop mode. 24 | # * Launch the Discover app, search for, and install Discord. 25 | # * Launch Discord, and sign in. Leave Discord running in the background. 26 | # * In the bottom-right of the screen, you'll see an icon for Steam. Right- 27 | # click it, and select "Library". 28 | # * Add a non-Steam game to your Library, and select Discord from the list of 29 | # things you can add. You'll now see Discord as a 'game'. There won't be 30 | # any logo in the Steam library, but that's OK. 31 | # * Minimize both Discord and Steam. Don't exit them, just minimize them. 32 | # 33 | # PART 2: Get and configure steam-presence 34 | # * Launch the Konsole app to get a terminal. 35 | # * You'll be in your home directory. Clone steam-presence from GitHub: 36 | # > git clone https://github.com/JustTemmie/steam-presence.git 37 | # * Go into the recently downloaded directory 38 | # > cd steam-presence 39 | # * Turn this Git clone into a Python venv: 40 | # > python3.10 -m venv . 41 | # * Update the venv and install steam-presence required software 42 | # > ./bin/python -m pip install --upgrade pip wheel 43 | # > ./bin/python -m pip install -r requirements.txt 44 | # * Copy exampleconfig.json to config.json and edit it, as per the README. 45 | # * Run steam-presence. 46 | # > ./bin/python ./main.py 47 | # If everything is configured correctly, you should eventually be told 48 | # "Everything is ready". 49 | # Press Control-C to exit steam-presence. 50 | # 51 | # PART 3: Enable auto-start 52 | # * In the Konsole (the terminal), copy this file to the place where Systemd 53 | # looks for service files: 54 | # > cp steam-presence.service ~/.config/systemd/user/steam-presence.service 55 | # * Reload Systemd, so that is becomes aware of steam-presence: 56 | # > systemctl --user daemon-reload 57 | # * Enable steam-presence, so that it will start automatically when the Steam 58 | # Deck is restarted, the --now argument makes it also start now: 59 | # > systemctl --user enable --now steam-presence 60 | # * Check on the status of steam-presence, to see if it started OK: 61 | # > systemctl --user status steam-presence 62 | # Systemd should tell you that steam-presence is running, and at the end 63 | # of the output, you should see the "everything is ready" message. 64 | # If the text goes off the right side of the screen, make the Konsole window 65 | # larger and run the command again. 66 | # 67 | # PART 4: Test! 68 | # * Exit the terminal sesson: 69 | # > exit 70 | # * On the Desktop, double-click the icon to "Return to Gaming Mode". 71 | # * Once in gaming mode, restart the Steam Deck. 72 | # * Once Steam Deck is rebooted, go to the Library, and launch Discord. 73 | # * Once Discord is running, press the STEAM button, go to the Library, 74 | # and lanch an actual game. 75 | # NOTE: It's important that you leave Discord running! steam-presence 76 | # uses Discord (which runs in the background) to relay status updates. 77 | # * Wait about a minute, and then go to Discord on another device (like your 78 | # phone). You should see that Discord is now showing the game you are 79 | # playing! 80 | 81 | # Hopefully all of that worked! Just remember: 82 | # * steam-presence will run in the background any time the Deck starts. 83 | # If you want to reduce battery consumption, you can disable it by going to Desktop mode, 84 | # launching the Konsole app, and running these two commands: 85 | # > systemctl --user stop steam-presence 86 | # > systemctl --user disable steam-presence 87 | # That will stop steam-presence, *and* keep it from starting again. 88 | # 89 | # * steam-presence needs Discord running in the background. 90 | # 91 | # * Always launch Discord *before* you launch a game. That gives 92 | # steam-presence time to wake up; also, when you've got multiple "games" 93 | # running, Steam only says that you're "playing" the most recently-launched 94 | # game. So, if you launch Discord second, then Steam will tell everyone 95 | # that you are "playing Discord". 96 | 97 | # Good luck! 98 | # The rest of this file is the actual contents of the Systemd service file. 99 | 100 | [Unit] 101 | # This is just basic information about what steam-presence is. 102 | Description=Discord rich presence from Steam on Linux 103 | Documentation=https://github.com/JustTemmie/steam-presence 104 | 105 | [Service] 106 | # CHANGEME: This is the full command that is run to launch steam-presence. 107 | # If you installed steam-presence somewhere else, then you need to change this. 108 | ExecStart=/home/deck/steam-presence/bin/python -u /home/deck/steam-presence/main.py 109 | 110 | # This tells Systemd that steam-presence is a simple service: 111 | # When launched, the Python process stays running until the service exits. 112 | Type=simple 113 | 114 | # This tells the OS to treat steam-presence as a lowest-priority program. 115 | Nice=19 116 | 117 | # When steam-presence is told to exit, it throws a KeyboardInterrupt Python 118 | # exception. This tells Systemd that it's OK, and that should count as a 119 | # normal program exit. 120 | SuccessExitStatus=130 121 | 122 | # This locks down what steam-presence is able to do. It is not able to 123 | # get any more privileges than it already has, and almost the entire 124 | # filesystem is made read-only to it. The only thing made read/write is 125 | # the directory where steam-presence lives. That is so the icons.txt file 126 | # can be updated with new game icons (only when you use SteamGridDB). 127 | NoNewPrivileges=true 128 | ProtectSystem=strict 129 | ReadWritePaths=/home/deck/steam-presence 130 | 131 | # This section is used when you run `systemctl --user enable steam-presence`. 132 | [Install] 133 | # WantedBy tells Systemd "when this service is enabled, it should be an 134 | # optional requirement of X". 135 | # 136 | # Normally, this is set to "multi-user.target". In Systemd 'system mode', 137 | # where Systemd is managing services for the OS, "multi-user.target" is the 138 | # correct thing to use. But because we are operating Systemd in 'user mode', 139 | # there is no "multi-user.target". Instead, there is "default.target". 140 | # 141 | # Once the OS is up, Systemd starts up a user-mode instance for us, and looks 142 | # for any services that are "wanted by" the "default.target". That is what 143 | # actually causes us to be launched at startup! 144 | WantedBy=default.target 145 | -------------------------------------------------------------------------------- /nix/nixos-modules/steam-presence.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | ... 6 | }: 7 | with lib; let 8 | cfg = config.programs.steam.presence; 9 | # Write a base config that does not include secret values. 10 | # Secret file values are injected at service start into config.json. 11 | configBaseFile = pkgs.writeText "steam-presence-config.base.json" (builtins.toJSON { 12 | STEAM_API_KEY = 13 | if cfg.steamApiKeyFile != null 14 | then null 15 | else cfg.steamApiKey; 16 | USER_IDS = cfg.userIds; 17 | DISCORD_APPLICATION_ID = cfg.discordApplicationId; 18 | FETCH_STEAM_RICH_PRESENCE = cfg.fetchSteamRichPresence; 19 | FETCH_STEAM_REVIEWS = cfg.fetchSteamReviews; 20 | ADD_STEAM_STORE_BUTTON = cfg.addSteamStoreButton; 21 | WEB_SCRAPE = cfg.webScrape; 22 | COVER_ART = { 23 | STEAM_GRID_DB = { 24 | ENABLED = cfg.coverArt.steamGridDB.enable; 25 | STEAM_GRID_API_KEY = 26 | if cfg.coverArt.steamGridDB.apiKeyFile != null 27 | then null 28 | else cfg.coverArt.steamGridDB.apiKey; 29 | }; 30 | USE_STEAM_STORE_FALLBACK = cfg.coverArt.useSteamStoreFallback; 31 | }; 32 | LOCAL_GAMES = { 33 | ENABLED = cfg.localGames.enable; 34 | LOCAL_DISCORD_APPLICATION_ID = cfg.localGames.discordApplicationId; 35 | GAMES = cfg.localGames.games; 36 | }; 37 | GAME_OVERWRITE = { 38 | ENABLED = cfg.gameOverwrite.enable; 39 | NAME = cfg.gameOverwrite.name; 40 | SECONDS_SINCE_START = cfg.gameOverwrite.secondsSinceStart; 41 | }; 42 | CUSTOM_ICON = { 43 | ENABLED = cfg.customIcon.enable; 44 | URL = cfg.customIcon.url; 45 | TEXT = cfg.customIcon.text; 46 | }; 47 | BLACKLIST = cfg.blacklist; 48 | WHITELIST = cfg.whitelist; 49 | }); 50 | in { 51 | options.programs.steam.presence = { 52 | enable = mkEnableOption "steam-presence"; 53 | package = mkOption { 54 | type = types.package; 55 | default = 56 | if pkgs ? steam-presence 57 | then pkgs.steam-presence 58 | else (pkgs.callPackage (builtins.toString ../pkgs/steam-presence) {src = pkgs.lib.cleanSource ../..;}); 59 | defaultText = literalExpression "pkgs.steam-presence (via overlay)"; 60 | description = "The steam-presence package to use."; 61 | }; 62 | steamApiKey = mkOption { 63 | type = types.nullOr types.str; 64 | default = null; 65 | description = "Your Steam Web API key."; 66 | }; 67 | 68 | steamApiKeyFile = mkOption { 69 | type = types.nullOr types.path; 70 | default = null; 71 | description = "Path to a file containing your Steam Web API key (e.g., agenix secret)."; 72 | }; 73 | userIds = mkOption { 74 | type = types.listOf types.str; 75 | default = []; 76 | description = "A list of Steam user IDs (SteamID64) to track."; 77 | }; 78 | discordApplicationId = mkOption { 79 | type = types.str; 80 | default = "869994714093465680"; 81 | description = "The Discord Application ID to use for the rich presence."; 82 | }; 83 | fetchSteamRichPresence = mkOption { 84 | type = types.bool; 85 | default = true; 86 | description = "Fetch 'enhanced rich presence' information from Steam."; 87 | }; 88 | fetchSteamReviews = mkOption { 89 | type = types.bool; 90 | default = false; 91 | description = "Fetch the review scores of Steam games."; 92 | }; 93 | addSteamStoreButton = mkOption { 94 | type = types.bool; 95 | default = false; 96 | description = "Add a button to the Steam store page in the rich presence."; 97 | }; 98 | webScrape = mkOption { 99 | type = types.bool; 100 | default = false; 101 | description = "Enable web scraping to detect non-Steam games."; 102 | }; 103 | coverArt = { 104 | steamGridDB = { 105 | enable = mkOption { 106 | type = types.bool; 107 | default = false; 108 | description = "Enable fetching cover art from SteamGridDB."; 109 | }; 110 | apiKey = mkOption { 111 | type = types.str; 112 | default = "STEAM_GRID_API_KEY"; 113 | description = "Your SteamGridDB API key."; 114 | }; 115 | 116 | apiKeyFile = mkOption { 117 | type = types.nullOr types.path; 118 | default = null; 119 | description = "Path to a file containing your SteamGridDB API key (e.g., agenix secret)."; 120 | }; 121 | }; 122 | useSteamStoreFallback = mkOption { 123 | type = types.bool; 124 | default = true; 125 | description = "Use the Steam store page for cover art as a fallback."; 126 | }; 127 | }; 128 | localGames = { 129 | enable = mkOption { 130 | type = types.bool; 131 | default = false; 132 | description = "Enable detection of locally running games."; 133 | }; 134 | discordApplicationId = mkOption { 135 | type = types.str; 136 | default = "1062648118375616594"; 137 | description = "The Discord Application ID to use for locally detected games."; 138 | }; 139 | games = mkOption { 140 | type = types.listOf types.str; 141 | default = []; 142 | description = "A list of process names for locally detected games."; 143 | }; 144 | }; 145 | gameOverwrite = { 146 | enable = mkOption { 147 | type = types.bool; 148 | default = false; 149 | description = "Enable overwriting the currently playing game."; 150 | }; 151 | name = mkOption { 152 | type = types.str; 153 | default = "Breath of the wild, now on steam!"; 154 | description = "The name of the game to display when overwriting."; 155 | }; 156 | 157 | secondsSinceStart = mkOption { 158 | type = types.int; 159 | default = 0; 160 | description = "The number of seconds to offset the start time when overwriting."; 161 | }; 162 | }; 163 | customIcon = { 164 | enable = mkOption { 165 | type = types.bool; 166 | default = false; 167 | description = "Enable a custom icon in the rich presence."; 168 | }; 169 | url = mkOption { 170 | type = types.str; 171 | default = "https://raw.githubusercontent.com/JustTemmie/steam-presence/main/readmeimages/defaulticon.png"; 172 | description = "The URL of the custom icon."; 173 | }; 174 | 175 | text = mkOption { 176 | type = types.str; 177 | default = "Steam Presence on Discord"; 178 | description = "The text to display when hovering over the custom icon."; 179 | }; 180 | }; 181 | cookiesFile = mkOption { 182 | type = types.nullOr types.path; 183 | default = null; 184 | description = "Path to cookies.txt for non-Steam game detection (web scraping)."; 185 | }; 186 | gamesFile = mkOption { 187 | type = types.nullOr types.path; 188 | default = null; 189 | description = "Path to games.txt mapping process names to display names."; 190 | }; 191 | iconsFile = mkOption { 192 | type = types.nullOr types.path; 193 | default = null; 194 | description = "Path to icons.txt mapping game names to icon URLs."; 195 | }; 196 | customGameIDsFile = mkOption { 197 | type = types.nullOr types.path; 198 | default = null; 199 | description = "Path to customGameIDs.json mapping game names to Discord application IDs."; 200 | }; 201 | blacklist = mkOption { 202 | type = types.listOf types.str; 203 | default = []; 204 | description = "A list of games to blacklist from the rich presence."; 205 | }; 206 | whitelist = mkOption { 207 | type = types.listOf types.str; 208 | default = []; 209 | description = "A list of games to whitelist for the rich presence."; 210 | }; 211 | }; 212 | config = mkIf cfg.enable { 213 | assertions = [ 214 | { 215 | assertion = (cfg.steamApiKey != null) || (cfg.steamApiKeyFile != null); 216 | message = "You must set either programs.steam.presence.steamApiKey or steamApiKeyFile"; 217 | } 218 | { 219 | assertion = cfg.userIds != []; 220 | message = "You must set programs.steam.presence.userIds"; 221 | } 222 | ]; 223 | systemd.user.services.steam-presence = { 224 | description = "Discord rich presence for Steam"; 225 | after = ["network-online.target"]; 226 | wantedBy = ["default.target"]; 227 | 228 | serviceConfig = { 229 | Environment = 230 | [ 231 | "STEAM_PRESENCE_RUNTIME_DIR=%h/.local/state/steam-presence" 232 | ] 233 | ++ (optional (cfg.steamApiKeyFile != null) "STEAM_API_KEY_FILE=${toString cfg.steamApiKeyFile}") 234 | ++ (optional (cfg.coverArt.steamGridDB.apiKeyFile != null) "STEAM_GRID_API_KEY_FILE=${toString cfg.coverArt.steamGridDB.apiKeyFile}"); 235 | WorkingDirectory = "%h/.local/state/steam-presence"; 236 | 237 | ExecStart = "${cfg.package}/bin/steam-presence"; 238 | 239 | Restart = "on-failure"; 240 | RestartSec = "10s"; 241 | }; 242 | preStart = '' 243 | set -euo pipefail 244 | RUNTIME_DIR="''${STEAM_PRESENCE_RUNTIME_DIR:-$HOME/.local/state/steam-presence}" 245 | ${pkgs.coreutils}/bin/mkdir -p "$RUNTIME_DIR" 246 | if [ ! -e "$RUNTIME_DIR/main.py" ]; then 247 | ${pkgs.coreutils}/bin/cp -r ${cfg.package}/share/steam-presence/. "$RUNTIME_DIR/" 248 | fi 249 | ${pkgs.coreutils}/bin/cp -Lf ${toString configBaseFile} "$RUNTIME_DIR/config.base.json" 250 | ${optionalString (cfg.cookiesFile != null) "${pkgs.coreutils}/bin/cp -Lf ${toString cfg.cookiesFile} \"$RUNTIME_DIR/cookies.txt\"\n"} 251 | ${optionalString (cfg.gamesFile != null) "${pkgs.coreutils}/bin/cp -Lf ${toString cfg.gamesFile} \"$RUNTIME_DIR/games.txt\"\n"} 252 | ${optionalString (cfg.iconsFile != null) "${pkgs.coreutils}/bin/cp -Lf ${toString cfg.iconsFile} \"$RUNTIME_DIR/icons.txt\"\n"} 253 | ${optionalString (cfg.customGameIDsFile != null) "${pkgs.coreutils}/bin/cp -Lf ${toString cfg.customGameIDsFile} \"$RUNTIME_DIR/customGameIDs.json\"\n"} 254 | 255 | cd "$RUNTIME_DIR" 256 | in_base="config.base.json" 257 | out_tmp="config.json.tmp" 258 | out_final="config.json" 259 | ${pkgs.python3}/bin/python - "$in_base" "$out_tmp" <<'PY' 260 | import json, os, sys 261 | base_path, out_path = sys.argv[1], sys.argv[2] 262 | with open(base_path, "r") as f: 263 | data = json.load(f) 264 | 265 | def ensure_path(d, keys): 266 | cur = d 267 | for k in keys: 268 | if k not in cur or not isinstance(cur[k], dict): 269 | cur[k] = {} 270 | cur = cur[k] 271 | return cur 272 | 273 | steam_key_file = os.environ.get("STEAM_API_KEY_FILE") 274 | if steam_key_file and os.path.isfile(steam_key_file): 275 | try: 276 | with open(steam_key_file, "r") as f: 277 | data["STEAM_API_KEY"] = f.read().strip() 278 | except Exception: 279 | pass 280 | 281 | sgdb_key_file = os.environ.get("STEAM_GRID_API_KEY_FILE") 282 | if sgdb_key_file and os.path.isfile(sgdb_key_file): 283 | try: 284 | ensure_path(data, ["COVER_ART", "STEAM_GRID_DB"]) 285 | with open(sgdb_key_file, "r") as f: 286 | data["COVER_ART"]["STEAM_GRID_DB"]["STEAM_GRID_API_KEY"] = f.read().strip() 287 | except Exception: 288 | pass 289 | 290 | with open(out_path, "w") as f: 291 | json.dump(data, f, indent=2) 292 | PY 293 | ${pkgs.coreutils}/bin/mv -f "$out_tmp" "$out_final" 294 | ''; 295 | }; 296 | }; 297 | } 298 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![image](https://user-images.githubusercontent.com/47639983/212717243-8325d457-5eb3-4948-ab45-865a763445c3.png) 2 | 3 | # Steam Presence 4 | 5 | a simple script to fetch a Steam user's current game and related info, and display it as a Discord rich presence 6 | 7 | ![GitHub last commit (by committer)](https://img.shields.io/github/last-commit/justtemmie/steam-presence) 8 | ![stars](https://img.shields.io/github/stars/justTemmie/steam-presence) 9 | ![visitors](https://visitor-badge.laobi.icu/badge?page_id=justtemmie.steam-presence) 10 | 11 | Made with ❤ by myself and our contributors 12 | 13 | If you have any issues with the script and you can't solve it yourself, don't be ashamed to open an issue! I will typically get back to you within 0-1 days 14 | 15 | 16 | ### Showcase 17 | 18 | ![Playing BTD6 with the script running](readmeimages/example1.png) 19 | 20 | playing "BTD6" with the script running, discord still finds some of the executables so it appears twice 21 | 22 | ![Playing Snake vs Snake with the script running](readmeimages/example2.png) 23 | 24 | playing "Snake vs Snake" with the script running 25 | 26 | this is pretty much the worst case scenario for the script as it's so niche 27 | 28 | * that fetching the game ID thru Discord isn't possible, so it ends up as "a game on steam" 29 | * and there's no art found in any public databases, so it just uses the cover art from steam, which sometimes ends up being a bit cropped 30 | 31 | ![Playing a local "game", known as calculator](readmeimages/example3.png) 32 | 33 | playing a local game i've set up, in this case "calculator", it's not running thru steam, so it's not "a game on steam", but rather just "a game" 34 | 35 | ### Features 36 | 37 | * Set Discord Rich Presence based on information fetched from steam. 38 | * Supports locally running applications, both games and other programs. 39 | * Dynamic config file reloading. 40 | * Simple to set up the core functionality. 41 | 42 | if you want a more thorough list, look at some of the [features listed here](#setup) 43 | 44 | ### Why?? 45 | well, why did i make this? Discord already detects the games you're playing so isn't this just pointless?? 46 | 47 | see, no. 48 | 49 | Discord has severe limitations when it comes to Linux as most games running thru a compatability layer, and most of these displayed as pr-wrap or something similar. 50 | 51 | in addition to this, there's the Steam Deck, a handheld linux game "console". 52 | 53 | having discord constantly run in the background is a terrible idea considering how that's gonna lose you at least half an hour of battery life, in addition to the previous issues with linux. 54 | 55 | so this script is a way of circumventing these issues by either having this run on something like a server 24/7, or setting this script to start on bootup on your gaming machine. 56 | 57 | DO NOTE that if you do intend to run this on a steam deck itself, discord will have to be open in the background 58 | 59 | but what this script CAN do is run on as mentioned, a server, or another form of desktop computer (with discord open in the background on that device) 60 | 61 | here's a step by step explanation of what happens. Let's say I launch Deep Rock Galactic (rock & stone) on my Steam Deck. Here's what happens: 62 | 63 | 1) Steam (on my Steam Deck) lets Steam HQ know that I'm running DRG. 64 | 65 | 2) Steam HQ updates, so my Steam Friends can see that I'm playing DRG. 66 | 67 | 3) Within a minute, steam-presence (running on my Mac) queries Steam HQ and sees that I'm playing DRG. 68 | 69 | 4) steam-presence (still on my Mac) pushes the rich presence information to the Discord client (also running on my Mac). 70 | 71 | 5) The Discord client will now display your current game on your profile, for your friends to see 72 | 73 | 74 | ## Requirements 75 | 76 | - Python 3.8 or higher installed 77 | - pip installed 78 | - a verified steam account (requires your account to have spent 5 USD on games) 79 | - your steam account must be online in order to detect your current game 80 | 81 | ## Installation 82 | 83 | clone the repo: 84 | 85 | ```sh 86 | git clone https://github.com/JustTemmie/steam-presence 87 | ``` 88 | 89 | follow the **setup** guide 90 | 91 | and for linux users, run the [Installer](#automatic-installer) 92 | 93 | for Nix/NixOS users, see the [Nix/NixOS](#nixnixos) section at the bottom. 94 | 95 | ## Setup 96 | ### Minimal 97 | create a file named `config.json` in the same directory as main.py and fill in the required data: 98 | 99 | ```json 100 | { 101 | "STEAM_API_KEY": "STEAM_API_KEY", 102 | "USER_IDS": "USER_ID" 103 | } 104 | ``` 105 | 106 | instructions for the `STEAM_API_KEY` can be found [here](#steam-web-api) 107 | and the `USER_IDS` field can be found [here](#user-ids) 108 | 109 | 110 | this will lead to some of the features not being able to work, but it does an alright job, 111 | the script will find: 112 | - Games running thru steam 113 | - Art of said games taken from the steam store 114 | - The rich presence will have the correct name for most games, whilst some nicher titles will be listed as "a game on steam" 115 | - Fetch rich presence information thru steam, for example in Hades you can be "Battling out of Elysium" or in BTD6 you can be "Browsing Menus" 116 | 117 | 118 | ### Full Featureset 119 | if you want more of the features provided by this script, you may fill in whatever parts of this config file seem interesting to you. 120 | this will allow the script functionality such as: 121 | - [Customizing the default game name to display when the script can't find the correct name (I.E the correct discord ID)](#discord-application-id) 122 | - [Detection of non-steam games running thru steam (done thru scuffed webscraping)](#non-steam-games) 123 | - [Usually better art (thru steam-grid-DB)](#steam-grid-db-(sgdb)) 124 | - [The detection of games running locally, such as minecraft (actually not scuffed)](#local-games) 125 | - [Overwriting it with whatever you want, even when not playing anything](#game-overwrite) 126 | - [A custom icon in the rich presence](custom-icon) 127 | - [Manually adding the correct names for any game you'd like](#custom-game-ids) 128 | - [Disabling fetching steam's rich presence](#fetch-steam-rich-presence) 129 | - [Enabling steam reviews](#fetch-steam-reviews) 130 | - [Add a button to the steam store page](#add-steam-store-button) 131 | - [Ability to blacklist games from showing up on discord rich presence](#blacklist) 132 | 133 | you will still need to fill out the `STEAM_API_KEY` found [here](#steam-web-api) and the `USER_IDS` found [here](#user-ids) 134 | 135 | this is what a full config file looks like; you only need to fill in the parts that you want to change for this default 136 | ```json 137 | { 138 | "STEAM_API_KEY": "STEAM_API_KEY", 139 | "USER_IDS": "USER_ID", 140 | 141 | "DISCORD_APPLICATION_ID": "869994714093465680", 142 | 143 | "FETCH_STEAM_RICH_PRESENCE": true, 144 | "FETCH_STEAM_REVIEWS": false, 145 | "ADD_STEAM_STORE_BUTTON": false, 146 | 147 | "WEB_SCRAPE": false, 148 | 149 | "COVER_ART": { 150 | "STEAM_GRID_DB": { 151 | "ENABLED": false, 152 | "STEAM_GRID_API_KEY": "STEAM_GRID_API_KEY" 153 | }, 154 | "USE_STEAM_STORE_FALLBACK": true 155 | }, 156 | 157 | "LOCAL_GAMES": { 158 | "ENABLED": false, 159 | "LOCAL_DISCORD_APPLICATION_ID": "1062648118375616594", 160 | "GAMES": [ 161 | "processName1", 162 | "processName2", 163 | "processName3", 164 | "so on" 165 | ] 166 | }, 167 | 168 | "GAME_OVERWRITE": { 169 | "ENABLED": false, 170 | "NAME": "Breath of the wild, now on steam!", 171 | "SECONDS_SINCE_START": 0 172 | }, 173 | 174 | "CUSTOM_ICON": { 175 | "ENABLED": false, 176 | "URL": "https://raw.githubusercontent.com/JustTemmie/steam-presence/main/readmeimages/defaulticon.png", 177 | "TEXT": "Steam Presence on Discord" 178 | }, 179 | 180 | "BLACKLIST": [ 181 | "game1", 182 | "game2", 183 | "game3" 184 | ], 185 | 186 | "WHITELIST": [] 187 | } 188 | ``` 189 | # Steam web API 190 | the `STEAM_API_KEY` in this case is regarding to the Steam web API. 191 | 192 | this you can obtain by registering here https://steamcommunity.com/dev/apikey while logged in 193 | 194 | the `domain` asked for does not matter in the slightest, just write something like 127.0.0.1 or github.com 195 | 196 | # User IDs 197 | the `USER_IDS` is the steam user id of the user you want to track. 198 | 199 | **NOTE** this is not the same as the display URL of the user. 200 | 201 | the easiest way i've found to get the ID is by throwing your url into the steamDB calculator https://steamdb.info/calculator/ 202 | 203 | and then taking the ID from that url 204 | 205 | ![ExampleImage](readmeimages/steamDB.png) 206 | 207 | **NOTE 2** the script accepts multiple steam users if you format it as "userid1,userid2" or ["userid1", "userid2"] 208 | 209 | # Discord Application ID 210 | the `DISCORD_APPLICATION_ID` is the discord application ID of the app you want to use. 211 | 212 | please generate one here https://discordapp.com/developers/applications/ or use mine "869994714093465680" 213 | 214 | the only thing you need to fill out on their site is the application name itself. 215 | 216 | for example i named mine "a game on steam" as shown in the screenshot above. 217 | 218 | # Cover Art 219 | and then we have the `COVER_ART` section. 220 | 221 | first is the STEAM_GRID_DB subsection 222 | ## Steam Grid DB (SGDB) 223 | 224 | this will download an icon from steamGridDB and use it as the cover art for the discord presence. 225 | 226 | change the ENABLED field to true and fill in the api key to enable this. 227 | 228 | you can get your API key here https://www.steamgriddb.com/profile/preferences/api 229 | 230 | ### icons.txt 231 | additionally, images from SGDB will be cached to a file named icons.txt, so if you don't like an icon it found you can open the file, find the game, and replace it with any image you want. 232 | 233 | basic example: 234 | ``` 235 | hades=https://cdn2.steamgriddb.com/file/sgdb-cdn/icon/fe50ae64d08d4f8245aaabc55d1baf79/32/80x80.png||Art by DIGGRID on SteamGrid DB 236 | deep rock galactic=https://cdn2.steamgriddb.com/file/sgdb-cdn/icon/fb508ef074ee78a0e58c68be06d8a2eb/32/256x256.png||Art by darklinkpower on SteamGrid DB 237 | a short hike=https://cdn2.steamgriddb.com/file/sgdb-cdn/icon/6a30e32e56fce5cf381895dfe6ca7b6f.png||Art by Julia on SteamGrid DB 238 | risk of rain 2=https://cdn2.steamgriddb.com/file/sgdb-cdn/icon/c4492cbe90fbdbf88a5aec486aa81ed5/32/256x256.png||Art by darklinkpower on SteamGrid DB 239 | ``` 240 | name of game=link to image||optional text to appear when hovering over 241 | 242 | 243 | ## Use Steam Store Fallback 244 | this will only have an effect if 1) the script fails to fetch an icon from SGDB, or 2) SGDB is simply disabled 245 | 246 | what this does is navigate to the store page of your game, and copy the link directly from the header image there, this image will end up getting cropped so it's not ideal but it works fairly well, at least in my opinion. 247 | 248 | # Local Games 249 | this will make the script scan for games running locally 250 | 251 | ## Local Discord App ID 252 | 253 | this is the application ID of the app you want to show up whenever you're playing a game that was detected locally. if you want to, this can be the same as the other default app ID 254 | 255 | please generate one here https://discordapp.com/developers/applications/ or use mine "1062648118375616594" 256 | 257 | the only thing you need to fill out on their site is the application name itself. 258 | 259 | for example i simply named mine "a game", rather generic but you can call it whatever 260 | 261 | ## Games 262 | 263 | please fill in the games field according to the names of the tasks, these are not case sensitive 264 | 265 | example for unix users: 266 | 267 | ``` 268 | "GAMES": [ 269 | "minesweeper", 270 | "firefox-bin" 271 | ] 272 | ``` 273 | 274 | whilst on windows you need to write something similar to this: 275 | 276 | ``` 277 | "GAMES": [ 278 | "osu!.exe", 279 | "firefox.exe" 280 | ] 281 | ``` 282 | 283 | to find the task names, you can check all your local apps by running `runningApps.py` 284 | 285 | ### games.txt 286 | 287 | you may also set a proper display name inside games.txt 288 | 289 | example: 290 | 291 | ``` 292 | firefox-bin=Firefox Web Browser 293 | steam=Steam Store 294 | ``` 295 | 296 | if you want to find out what's running locally, you can run the runningApps.py script, it will simply print out every single application it detects locally, ctrl + f is your best friend. This script is likely going to get improved in the future 297 | 298 | 299 | # Fetch Steam Rich Presence 300 | 301 | ![image](https://user-images.githubusercontent.com/47639983/237048939-d08c1b01-f85a-40d5-928e-df3e88116063.png) 302 | 303 | the "enhanced rich presence" info here is talking about the "Browsing Menus" part 304 | 305 | some steam games have what steam calls "enhanced rich presence", it's pretty much the same rich presence system discord has, but on steam! 306 | 307 | not a ton of games have this implementet, as i can speak from personal experience as a game dev, it's pretty hard and un-intuitive 308 | 309 | fetching this "enhanced rich presence" is enabled by default 310 | 311 | but it can be disabled if you like by adding `"FETCH_STEAM_RICH_PRESENCE": false,` to the config file 312 | 313 | # Fetch Steam Reviews 314 | 315 | the script can also fetch the reviews of any steam game - it's very epic 316 | 317 | if you'd like to enable this, add `"FETCH_STEAM_REVIEWS": true,` to the config file 318 | 319 | # Add Steam Store Button 320 | 321 | if you want, you can add a button to the steam page, price included (USD) 322 | 323 | add `"ADD_STEAM_STORE_BUTTON": true,` to the config file to enable this 324 | 325 | # Non Steam Games 326 | 327 | okay i'll be frank, this functionality is VERY scuffed, but - it does work. 328 | 329 | so for a bit of background to why this was thrown together in such a scuffed manner, the main reason being that Steam's actual API does not report non steam games in ANY capacity. 330 | 331 | so the best solution i could come up with to solve this; web scraping - loading a website in the background, reading the data off it, and closing it - redoing this every 20 seconds. 332 | 333 | performance wise it's actually fine, being reasonably light weight and on anything reasonably modern this should be fine. 334 | 335 | to do this, download your cookies for steam these are needed because the script needs to login as you in order to see what games you're playing (yes it's scuffed). 336 | 337 | from my own experience steam's cookies tend to stay valid for about a week, meaning you have to redo this step EVERY week. 338 | 339 | download this addon for firefox, i couldn't find any extensions for chrome that i'm certain aren't viruses (sorry, just go download firefox for this). 340 | 341 | https://addons.mozilla.org/en-US/firefox/addon/cookies-txt/ 342 | 343 | navigate to your profile on steam, and download the steam cookie file, naming it "cookies.txt" and save it in the same folder as main.py 344 | 345 | then change WEB_SCRAPE in the config file to true 346 | 347 | **Note**: due to the names of non steam games being set by yourself, steam grid DB might have problems finding icons for the game, but if it's in their database, this script will fetch it 348 | 349 | **Note 2**: if you're using multiple steam users, steam presence will be able to fetch every user who is a friend of the cookie's user via this 350 | 351 | # Game Overwrite 352 | 353 | if you want to display soemthing else, you can use the `GAME_OVERWRITE` section. 354 | 355 | set enabled to true and fill in the name of whatever you want to display. 356 | 357 | this will still try to grab an icon from steamGridDB, but if it can't find one you can try giving the game an icon yourself, [see icons.txt](#iconstxt). 358 | 359 | this field can be *anything* if you want to be seen playing "eirasdtloawbd", or "Hollow knight: Silksong" you can do so. 360 | 361 | (note to self, remove the silksong joke when the game actually releases) 362 | 363 | ## Seconds Since Start 364 | 365 | this should be set to 0 most of the time 366 | 367 | but if you want to display that you've been playing since 2 hours ago, you can set this 7200 (3600 seconds in an hour * 2 hours) 368 | 369 | this sadly won't help you break Discord's cap of 24 hours on rich presences 370 | 371 | if you set the game to start in -3600 seconds (or 1 hour) it just display 0:00:00 372 | 373 | # Custom Icon 374 | 375 | this is a small icon that appears in the bottom right, enable it or disable it. 376 | 377 | set the URL for an image you'd like to use, and some text to appear when anyone hovers over the icon. 378 | 379 | # Custom Game IDs 380 | 381 | if you wish to, you can create a file named "customGameIDs.json", this file will allow the script to properly display the game name for everything the script can't find on it's own. 382 | 383 | you need to create a game thru discord's dashboard https://discord.com/developers/applications/ the only thing you need so set is the application name, everything else is irrelevant. 384 | 385 | then pair this up with the game's name, be careful with the end of line commas. 386 | 387 | note, you will have to make an application thru discord for every single game you add here. 388 | 389 | this is compatible with games both fetched thru steam and detected running locally. 390 | 391 | ![image](https://user-images.githubusercontent.com/47639983/213175355-a082bfc3-3083-4e76-9747-d2cda4f18790.png) 392 | 393 | template: 394 | 395 | ``` 396 | { 397 | "Firefox": 1065236154477203456, 398 | "Godot Engine": 1065241036932268073, 399 | "Beaver Clicker": 1065249144853254245 400 | } 401 | ``` 402 | 403 | # Blacklist 404 | 405 | Here you can add games that are not supposed to show up in Discord's rich presence. please fill in the games field according to the full name of the games, these are not case sensitive. 406 | 407 | Example: 408 | ``` 409 | "BLACKLIST" : [ 410 | "Hades", 411 | "Deep Rock Galactic", 412 | "Risk of Rain 2" 413 | ] 414 | ``` 415 | 416 | # Whitelist 417 | 418 | Here you can add games that should only show up in Discord's rich presence, all games not included here will not create a rich presence object. Leave empty to have no whitelist. Please fill in the games field according to the full name of the games, these are not case sensitive. 419 | 420 | Example: 421 | ``` 422 | "WHITELIST" : [ 423 | "Hades", 424 | "Deep Rock Galactic", 425 | "Risk of Rain 2" 426 | ] 427 | ``` 428 | 429 | # Python 430 | only tested on python3.8 and higher. 431 | 432 | run `python3 -m pip install -r requirements.txt` to install all the dependencies 433 | 434 | then run `python3 main.py` 435 | 436 | (these should hopefully be platform independent, if they're not please open an issue or pull request) 437 | 438 | # Run On Startup 439 | this script doesn't have any inherent way to run when your device starts up. 440 | 441 | if you're running either Windows or MacOS i cannot really give you any help with this. 442 | 443 | (if you do know a way to run this on startup on any of the mentioned systems, *please* create a pull request with an updated readme) 444 | 445 | # Installation to Automatically Start on Bootupt 446 | 447 | ## Automatic Installer 448 | 449 | Steam presence currently only supports automatically starting up on `Linux` and `MacOS`, if you know how to make it start on boot within windows, please make a PR, thanks! 450 | 451 | to install steam presence, simply run the `installer.sh` file 452 | 453 | to do this, open konsole or another terminal and run this command: 454 | 455 | ``` 456 | ./installer.sh 457 | ``` 458 | 459 | ## Manual Installation 460 | 461 | The file `steam-presence.service` has more information and instructions on how to install it on linux. 462 | 463 | ## Linux (not using Systemd) 464 | 465 | for those of you not running systemd, you might have cron installed! 466 | 467 | if you have cron setup, you can also install the `screen` application, and then create a file named `startup.sh` and paste in the code below, changing the path so it finds steam presence's main.py file. 468 | 469 | ``` 470 | screen -dmS steamPresence bash -c 'python3 /home/USER/steam-presence/main.py' 471 | ``` 472 | 473 | make this script executable using `chmod +x startup.sh` 474 | 475 | then run `crontab -e` and add `@reboot /home/USER/startup.sh` to the end of the crontab file. 476 | 477 | if you've done these steps the script should launch itself after your computer turns on. 478 | 479 | ## Nix/NixOS 480 | 481 | If you use Nix flakes, this repository provides a package and a NixOS module. 482 | 483 | - Use the flake: 484 | ```nix 485 | { 486 | inputs = { 487 | steam-presence = { 488 | url = "github:JustTemmie/steam-presence"; 489 | inputs.nixpkgs.follows = "nixpkgs"; 490 | }; 491 | }; 492 | } 493 | ``` 494 | 495 | - Enable the NixOS user service module: 496 | ```nix 497 | { 498 | imports = [inputs.steam-presence.nixosModules.steam-presence]; 499 | 500 | programs.steam = { 501 | # ... 502 | presence = { 503 | enable = true; 504 | # Either set the key directly (not recommended) or via file/secret 505 | # steamApiKey = "YOUR_STEAM_WEB_API_KEY"; 506 | steamApiKeyFile = "/run/secrets/steam_api_key"; # e.g. from agenix/sops 507 | userIds = [ "YOUR_STEAMID" ]; 508 | # Other optional settings 509 | }; 510 | }; 511 | } 512 | ``` 513 | All configuration options are in [steam-presence.nix](nix/nixos-modules/steam-presence.nix) 514 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # creating rich presences for discord 2 | from time import sleep, time 3 | 4 | # for errors 5 | from datetime import datetime 6 | 7 | # for loading the config file 8 | import json 9 | from os.path import exists, dirname, abspath 10 | 11 | # for restarting the script on a failed run 12 | import sys 13 | import os 14 | 15 | # for general programing 16 | import copy 17 | 18 | try: 19 | # requesting data from steam's API 20 | import requests 21 | 22 | # creating rich presences for discord 23 | from pypresence import Presence 24 | 25 | # used to get the game's cover art 26 | from steamgrid import SteamGridDB 27 | 28 | # used as a backup when cover art 29 | from bs4 import BeautifulSoup 30 | 31 | # used to check applications that are open locally 32 | import psutil 33 | 34 | # used to load cookies for non-steam games 35 | import http.cookiejar as cookielib 36 | 37 | except: 38 | answer = input("looks like either requests, pypresence, steamgrid, psutil, or beautifulSoup is not installed, do you want to install them? (y/n) ") 39 | if answer.lower() == "y": 40 | from os import system 41 | print("installing required packages...") 42 | system(f"python3 -m pip install -r {dirname(abspath(__file__))}/requirements.txt") 43 | 44 | from pypresence import Presence 45 | from steamgrid import SteamGridDB 46 | from bs4 import BeautifulSoup 47 | import psutil 48 | import requests 49 | import http.cookiejar as cookielib 50 | 51 | print("\npackages installed and imported successfully!") 52 | 53 | # just shorthand for logs and errors - easier to write in script 54 | def log(log): 55 | print(f"[{datetime.now().strftime('%b %d %Y - %H:%M:%S')}] {log}") 56 | 57 | def error(error): 58 | print(f" ERROR: [{datetime.now().strftime('%b %d %Y - %H:%M:%S')}] {error}") 59 | 60 | # i've gotten the error `requests.exceptions.ConnectionError: ('Connection aborted.', ConnectionResetError(104, 'Connection reset by peer'))` a lot; 61 | # this just seems to sometimes happens if your network conection is a bit wack, this function is a replacement for requests.get() and basically just does error handling and stuff 62 | def makeWebRequest(URL, loops=0): 63 | try: 64 | r = requests.get(URL) 65 | return r 66 | except Exception as e: 67 | if loops > 10: 68 | error(f"falling back... the script got caught in a loop while fetching data from `{URL}`") 69 | return "error" 70 | elif "104 'Connection reset by peer'" in str(e): 71 | return makeWebRequest(URL, loops+1) 72 | else: 73 | # error(f"falling back... exception met whilst trying to fetch data from `{URL}`\nfull error: {e}") 74 | return "error" 75 | 76 | def getMetaFile(): 77 | if exists(f"{dirname(__file__)}/data/meta.json"): 78 | with open(f"{dirname(__file__)}/data/meta.json", "r") as f: 79 | metaFile = json.load(f) 80 | 81 | elif exists(f"{dirname(__file__)}/meta.json"): 82 | with open(f"{dirname(__file__)}/meta.json", "r") as f: 83 | metaFile = json.load(f) 84 | 85 | else: 86 | # remove in 1.12? maybe 1.13 - whenever i do anything else with the meta file - just make this throw an error instead 87 | log("couldn't find the the meta file, creating new one") 88 | with open(f"{dirname(__file__)}/meta.json", "w") as f: 89 | metaFile = json.dump({"structure-version": "0"}, f) 90 | 91 | return getMetaFile() 92 | 93 | return metaFile 94 | 95 | def writeToMetaFile(keys: list, value): 96 | metaFile = getMetaFile() 97 | 98 | for i in range(len(keys) - 1): 99 | metaFile = metaFile[keys[i]] 100 | 101 | metaFile[keys[-1]] = value 102 | 103 | with open(f"{dirname(__file__)}/data/meta.json", "w") as f: 104 | json.dump(metaFile, f) 105 | 106 | 107 | 108 | # opens the config file and loads the data 109 | def getConfigFile(): 110 | # the default settings, don't use exampleConfig.json as people might change that 111 | defaultSettings = { 112 | "STEAM_API_KEY": "STEAM_API_KEY", 113 | "USER_IDS": "USER_ID", 114 | 115 | "DISCORD_APPLICATION_ID": "869994714093465680", 116 | 117 | "FETCH_STEAM_RICH_PRESENCE": True, 118 | "FETCH_STEAM_REVIEWS": False, 119 | "ADD_STEAM_STORE_BUTTON": False, 120 | 121 | "WEB_SCRAPE": False, 122 | 123 | "COVER_ART": { 124 | "STEAM_GRID_DB": { 125 | "ENABLED": False, 126 | "STEAM_GRID_API_KEY": "STEAM_GRID_API_KEY" 127 | }, 128 | "USE_STEAM_STORE_FALLBACK": True 129 | }, 130 | 131 | "LOCAL_GAMES": { 132 | "ENABLED": False, 133 | "LOCAL_DISCORD_APPLICATION_ID": "1062648118375616594", 134 | "GAMES": [ 135 | "processName1", 136 | "processName2", 137 | "processName3" 138 | ] 139 | }, 140 | 141 | "GAME_OVERWRITE": { 142 | "ENABLED": False, 143 | "NAME": "NAME", 144 | "SECONDS_SINCE_START": 0 145 | }, 146 | 147 | "CUSTOM_ICON": { 148 | "ENABLED": False, 149 | "URL": "https://raw.githubusercontent.com/JustTemmie/steam-presence/main/readmeimages/defaulticon.png", 150 | "TEXT": "Steam Presence on Discord" 151 | }, 152 | 153 | "BLACKLIST" : [ 154 | "game1", 155 | "game2", 156 | "game3" 157 | ], 158 | 159 | "WHITELIST" : [] 160 | } 161 | 162 | if exists(f"{dirname(abspath(__file__))}/config.json"): 163 | with open(f"{dirname(abspath(__file__))}/config.json", "r") as f: 164 | userSettings = json.load(f) 165 | 166 | elif exists(f"{dirname(abspath(__file__))}/exampleconfig.json"): 167 | with open(f"{dirname(abspath(__file__))}/exampleconfig.json", "r") as f: 168 | userSettings = json.load(f) 169 | 170 | else: 171 | error("Config file not found. Please read the readme and create a config file.") 172 | exit() 173 | 174 | 175 | # if something isn't speficied in the user's config file, fill it in with data from the default settings 176 | settings = {**defaultSettings, **userSettings} 177 | for key, value in defaultSettings.items(): 178 | if key in settings and isinstance(value, dict): 179 | settings[key] = {**value, **settings[key]} 180 | 181 | 182 | return settings 183 | 184 | def removeChars(inputString: str, ignoredChars: str) -> str: 185 | # removes all characters in the ingoredChars string from the inputString 186 | for ignoredChar in ignoredChars: 187 | if ignoredChar in inputString: 188 | for j in range(len(inputString) - 1, 0, -1): 189 | if inputString[j] in ignoredChar: 190 | inputString = inputString[:j] + inputString[j+1:] 191 | 192 | return inputString 193 | 194 | 195 | def getImageFromSGDB(loops=0): 196 | global coverImage 197 | global coverImageText 198 | 199 | log("searching for an icon using the SGDB") 200 | # searches SGDB for the game you're playing 201 | results = sgdb.search_game(gameName) 202 | 203 | if len(results) == 0: 204 | log(f"could not find anything on SGDB") 205 | return 206 | 207 | 208 | log(f"found the game {results[0]} on SGDB") 209 | gridAppID = results[0].id 210 | 211 | # searches for icons 212 | gridIcons = sgdb.get_icons_by_gameid(game_ids=[gridAppID]) 213 | 214 | # makes sure anything was returned at all 215 | if gridIcons != None: 216 | 217 | # throws the icons into a dictionary with the required information, then sorts them using the icon height 218 | gridIconsDict = {} 219 | for i, gridIcon in enumerate(gridIcons): 220 | gridIconsDict[i] = [gridIcon.height, gridIcon._nsfw, gridIcon.url, gridIcon.mime, gridIcon.author.name, gridIcon.id] 221 | 222 | gridIconsDict = (sorted(gridIconsDict.items(), key=lambda x:x[1], reverse=True)) 223 | 224 | 225 | # does a couple checks before making it the cover image 226 | for i in range(0, len(gridIconsDict)): 227 | entry = gridIconsDict[i][1] 228 | # makes sure image is not NSFW 229 | if entry[1] == False: 230 | # makes sure it's not an .ico file - discord cannot display these 231 | if entry[3] == "image/png": 232 | # sets the link, and gives credit to the artist if anyone hovers over the icon 233 | coverImage = entry[2] 234 | coverImageText = f"Art by {entry[4]} on SteamGrid DB" 235 | log("successfully retrived icon from SGDB") 236 | # saves this data to disk 237 | with open(f'{dirname(abspath(__file__))}/data/icons.txt', 'a') as icons: 238 | icons.write(f"{gameName.lower()}={coverImage}||{coverImageText}\n") 239 | icons.close() 240 | return 241 | 242 | log("failed, trying to load from the website directly") 243 | # if the game doesn't have any .png files for the game, try to web scrape them from the site 244 | for i in range(0, len(gridIconsDict)): 245 | entry = gridIconsDict[i][1] 246 | # makes sure image is not NSFW 247 | if entry[1] == False: 248 | URL = f"https://www.steamgriddb.com/icon/{entry[5]}" 249 | page = makeWebRequest(URL) 250 | if page == "error": 251 | return 252 | 253 | if page.status_code != 200: 254 | error(f"status code {page.status_code} recieved when trying to web scrape SGDB, ignoring") 255 | return 256 | 257 | # web scraping, this code is messy 258 | soup = BeautifulSoup(page.content, "html.parser") 259 | 260 | img = soup.find("meta", property="og:image") 261 | 262 | coverImage = img["content"] 263 | coverImageText = f"Art by {entry[4]} on SteamGrid DB" 264 | 265 | log("successfully retrived icon from SGDB") 266 | 267 | # saves data to disk 268 | with open(f'{dirname(abspath(__file__))}/data/icons.txt', 'a') as icons: 269 | icons.write(f"{gameName.lower()}={coverImage}||{coverImageText}\n") 270 | icons.close() 271 | return 272 | 273 | log("failed to fetch icon from SGDB") 274 | 275 | else: 276 | log(f"SGDB doesn't seem to have any entries for {gameName}") 277 | 278 | def getGameSteamID(): 279 | # fetches a list of ALL games on steam 280 | r = makeWebRequest(f"https://api.steampowered.com/ISteamApps/GetAppList/v0002/?key={steamAPIKey}&format=json") 281 | if r == "error": 282 | return 283 | 284 | # sleep for 0.2 seconds, this is done after every steam request, to avoid getting perma banned (yes steam is scuffed) 285 | sleep(0.2) 286 | 287 | if r.status_code == 403: 288 | error("Forbidden, Access to the steam API has been denied, please verify your steam API key") 289 | exit() 290 | 291 | if r.status_code != 200: 292 | error(f"error code {r.status_code} met when requesting list of games in order to obtain an icon for {gameName}, ignoring") 293 | return 294 | 295 | respone = r.json() 296 | 297 | global gameSteamID 298 | 299 | # loops thru every game until it finds one matching your game's name 300 | for i in respone["applist"]["apps"]: 301 | if gameName.lower() == i["name"].lower(): 302 | 303 | if gameSteamID == 0: 304 | log(f"steam app ID {i['appid']} found for {gameName}") 305 | 306 | gameSteamID = i["appid"] 307 | return 308 | 309 | 310 | # for handling game demos 311 | if " demo" in gameName.lower(): 312 | tempGameName = copy(gameName.lower()) 313 | tempGameName.replace(" demo", "") 314 | for i in respone["applist"]["apps"]: 315 | if tempGameName.lower() == i["name"].lower(): 316 | 317 | if gameSteamID == 0: 318 | log(f"steam app ID {i['appid']} found for {gameName}") 319 | 320 | gameSteamID = i["appid"] 321 | return 322 | 323 | 324 | 325 | # if we didn't find the game at all on steam, 326 | log(f"could not find the steam app ID for {gameName}") 327 | gameSteamID = 0 328 | 329 | 330 | def getImageFromStorepage(): 331 | global coverImage 332 | global coverImageText 333 | 334 | # if the steam game ID is known to be invalid, just return immediately 335 | if gameSteamID == 0: 336 | coverImage = None 337 | coverImageText = None 338 | return 339 | 340 | log("getting icon from the steam store") 341 | try: 342 | r = makeWebRequest(f"https://store.steampowered.com/api/appdetails?appids={gameSteamID}") 343 | if r == "error": 344 | return 345 | 346 | # sleep for 0.2 seconds, this is done after every steam request, to avoid getting perma banned (yes steam is scuffed) 347 | sleep(0.2) 348 | 349 | if r.status_code != 200: 350 | error(f"error code {r.status_code} met when requesting list of games in order to obtain an icon for {gameName}, ignoring") 351 | coverImage = None 352 | coverImageText = None 353 | return 354 | 355 | respone = r.json() 356 | 357 | coverImage = respone[str(gameSteamID)]["data"]["header_image"] 358 | coverImageText = f"{gameName} on Steam" 359 | # do note this is NOT saved to disk, just in case someone ever adds an entry to the SGDB later on 360 | 361 | log(f"successfully found steam's icon for {gameName}") 362 | 363 | except Exception as e: 364 | error(f"Exception {e} raised when trying to fetch {gameName}'s icon thru steam, ignoring") 365 | coverImage = None 366 | coverImageText = None 367 | 368 | 369 | def getGameReviews(): 370 | # get the review data for the steam game 371 | r = makeWebRequest(f"https://store.steampowered.com/appreviews/{gameSteamID}?json=1") 372 | if r == "error": 373 | return 374 | 375 | # sleep for 0.2 seconds, this is done after every steam request, to avoid getting perma banned (yes steam is scuffed) 376 | sleep(0.2) 377 | 378 | if r.status_code != 200: 379 | error(f"error code {r.status_code} met when requesting the review score for {gameName}, ignoring") 380 | return 381 | 382 | # convert it to a dictionary 383 | respone = r.json() 384 | 385 | # sometimes instead of returning the disered dicationary steam just decides to be quirky 386 | # and it returns the dictionary `{'success': 2}` - something which isn't really useful. If this happens we try again :) 387 | if respone["success"] != 1: 388 | getGameReviews() 389 | return 390 | 391 | response = respone["query_summary"] 392 | 393 | if response["total_positive"] == 0: 394 | return 395 | 396 | global gameReviewScore 397 | global gameReviewString 398 | 399 | gameReviewScore = round( 400 | (response["total_positive"] / response["total_reviews"]) * 100 401 | , 2) 402 | gameReviewString = response["review_score_desc"] 403 | 404 | 405 | # searches the steam grid DB or the official steam store to get cover images for games 406 | def getGameImage(): 407 | global coverImage 408 | global coverImageText 409 | 410 | coverImage = "" 411 | 412 | log(f"fetching icon for {gameName}") 413 | 414 | # checks if there's already an existing icon saved to disk for the game 415 | with open(f'{dirname(abspath(__file__))}/data/icons.txt', 'r') as icons: 416 | for i in icons: 417 | # cut off the new line character 418 | game = i.split("\n") 419 | game = game[0].split("=") 420 | if gameName.lower() == game[0]: 421 | coverData = game[1].split("||") 422 | coverImage = coverData[0] 423 | 424 | # if the script doesn't find text saved for the image, it won't set any 425 | if len(coverData) >= 2: 426 | coverImageText = coverData[1] 427 | # write over it and set it to None, just in case 428 | else: 429 | coverImageText = None 430 | 431 | log(f"found icon for {gameName} in cache") 432 | return 433 | 434 | log("no image found in cache") 435 | 436 | if gridEnabled and coverImage == "": 437 | getImageFromSGDB() 438 | 439 | if appID != defaultAppID and appID != defaultLocalAppID and coverImage == "": 440 | getImageFromDiscord() 441 | 442 | if steamStoreCoverartBackup and coverImage == "": 443 | getImageFromStorepage() 444 | 445 | 446 | def getGamePrice(): 447 | r = makeWebRequest(f"https://store.steampowered.com/api/appdetails?appids={gameSteamID}&cc=us") 448 | if r == "error": 449 | return 450 | 451 | # sleep for 0.2 seconds, this is done after every steam request, to avoid getting perma banned (yes steam is scuffed) 452 | sleep(0.2) 453 | 454 | if r.status_code != 200: 455 | error(f"error code {r.status_code} met when requesting list of games in order to obtain an icon for {gameName}, ignoring") 456 | return 457 | 458 | respone = r.json() 459 | 460 | if "price_overview" not in respone[str(gameSteamID)]["data"]: 461 | return 462 | 463 | return respone[str(gameSteamID)]["data"]["price_overview"]["final_formatted"] 464 | 465 | 466 | # web scrapes the user's web page, sending the needed cookies along with the request 467 | def getWebScrapePresence(): 468 | if not exists(f"{dirname(abspath(__file__))}/cookies.txt"): 469 | print("cookie.txt not found, this is because `WEB_SCRAPE` is enabled in the config") 470 | return 471 | 472 | cj = cookielib.MozillaCookieJar(f"{dirname(abspath(__file__))}/cookies.txt") 473 | cj.load() 474 | 475 | # split on ',' in case of multiple userIDs 476 | for i in userID.split(","): 477 | URL = f"https://steamcommunity.com/profiles/{i}/" 478 | 479 | # sleep for 0.2 seconds, this is done after every steam request, to avoid getting perma banned (yes steam is scuffed) 480 | sleep(0.2) 481 | 482 | try: 483 | page = requests.post(URL, cookies=cj) 484 | except requests.exceptions.RetryError as e: 485 | log(f"failed connecting to {URL}, perhaps steam is down for maintenance?\n error:{e}") 486 | return 487 | except Exception as e: 488 | error(f"error caught while web scraping data from {URL}, ignoring\n error:{e}") 489 | return 490 | 491 | if page.status_code == 403: 492 | error("Forbidden, Access to Steam has been denied, please verify that your cookies are up to date") 493 | 494 | elif page.status_code != 200: 495 | error(f"error code {page.status_code} met when trying to fetch game thru webscraping, ignoring") 496 | 497 | else: 498 | soup = BeautifulSoup(page.content, "html.parser") 499 | 500 | for element in soup.find_all("div", class_="profile_in_game_name"): 501 | result = element.text.strip() 502 | 503 | # the "last online x min ago" field is the same div as the game name 504 | if "Last Online" not in result: 505 | 506 | global isPlayingSteamGame 507 | global gameName 508 | 509 | isPlayingSteamGame = False 510 | gameName = result 511 | 512 | # checks what game the user is currently playing 513 | def getSteamPresence(): 514 | r = makeWebRequest(f"https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key={steamAPIKey}&format=json&steamids={userID}") 515 | 516 | # sleep for 0.2 seconds, this is done after every steam request, to avoid getting perma banned (yes steam is scuffed) 517 | sleep(0.2) 518 | 519 | # if it errors out, just return the already asigned gamename 520 | if r == "error": 521 | return gameName 522 | 523 | 524 | if r.status_code == 403: 525 | error("Forbidden, Access to the steam API has been denied, please verify your steam API key") 526 | exit() 527 | 528 | if r.status_code != 200: 529 | error(f"error code {r.status_code} met when trying to fetch game, ignoring") 530 | return gameName 531 | 532 | 533 | response = r.json() 534 | 535 | # counts how many users you're supposed to get back, and checks if you got that many back 536 | if len(response["response"]["players"]) != userID.count(",") + 1: 537 | error("There seems to be an incorrect account ID given, please verify that your user ID(s) are correct") 538 | 539 | 540 | global isPlayingSteamGame 541 | 542 | # sort the players based on position in the config file 543 | sorted_response = [] 544 | for steam_id in userID.split(","): 545 | for player in response["response"]["players"]: 546 | if player["steamid"] == steam_id: 547 | sorted_response.append(player) 548 | break 549 | 550 | 551 | # loop thru every user in the response, if they're playing a game, save it 552 | for i in range(0, len(sorted_response)): 553 | if "gameextrainfo" in sorted_response[i]: 554 | game_title = sorted_response[i]["gameextrainfo"] 555 | if game_title != gameName: 556 | log(f"found game {game_title} played by {sorted_response[i]['personaname']}") 557 | isPlayingSteamGame = True 558 | return game_title 559 | 560 | return "" 561 | 562 | # webscrape the "enhanced" rich presence information for your profile 563 | # if you're confused this uses your , it's your (which is what you put into the config file) minus 76561197960265728 564 | # aka = - 76561197960265728 565 | # why steam does this is beyond me but it's fine 566 | # thank you so much to `wuddih` in this post for being the reason i found out about this https://steamcommunity.com/discussions/forum/1/5940851794736009972/ lmao 567 | def getSteamRichPresence(): 568 | for i in userID.split(","): 569 | # userID type 3. = - 76561197960265728 570 | URL = f"https://steamcommunity.com/miniprofile/{int(i) - 76561197960265728}" 571 | try: 572 | pageRequest = makeWebRequest(URL) 573 | except requests.exceptions.RetryError as e: 574 | log(f"failed connecting to {URL}, perhaps steam is down for maintenance?\n error:{e}") 575 | return 576 | except Exception as e: 577 | error(f"error caught while fetching enhanced RPC data from {URL}, ignoring\n error:{e}") 578 | return 579 | 580 | if pageRequest == "error": 581 | return 582 | 583 | # sleep for 0.2 seconds, this is done after every steam request, to avoid getting perma banned (yes steam is scuffed) 584 | sleep(0.2) 585 | 586 | if pageRequest.status_code != 200: 587 | error(f"status code {pageRequest.status_code} returned whilst trying to fetch the enhanced rich presence info for steam user ID `{i}`, ignoring function") 588 | return 589 | 590 | # turn the page into proper html formating 591 | soup = BeautifulSoup(pageRequest.content, "html.parser") 592 | 593 | global gameRichPresence 594 | 595 | # double check if it's the correct game, yea i know we're basically fetching the game twice 596 | # once thru here, and once thru the API... BUT OH WELL - the api is used for other things so people would still need a steam api key 597 | # doesn't really change it that much, might change things around later 598 | miniGameName = soup.find("span", class_="miniprofile_game_name") 599 | if miniGameName != None: 600 | # this usually has a length of 0 when the user is playing a non steam game 601 | if len(miniGameName.contents) != 0: 602 | if gameName != miniGameName.contents[0]: 603 | # print(f"{gameName} doesn't match", soup.find("span", class_="miniprofile_game_name").contents[0]) 604 | break 605 | 606 | 607 | # find the correct entry where the rich presence is located 608 | rich_presence = soup.find("span", class_="rich_presence") 609 | 610 | # save rich presence if it exists 611 | if rich_presence != None: 612 | gameRichPresence = rich_presence.contents[0] 613 | 614 | # set the "enhanced rich presence" information back to nothing 615 | if rich_presence == None: 616 | gameRichPresence = "" 617 | 618 | 619 | 620 | # requests a list of all games recognized internally by discord, if any of the names matches 621 | # the detected game, save the discord game ID associated with said title to RAM, this is used to report to discord as that game 622 | def getGameDiscordID(loops=0): 623 | log(f"fetching the Discord game ID for {gameName}") 624 | r = makeWebRequest("https://discordapp.com/api/v8/applications/detectable") 625 | 626 | if r.status_code != 200: 627 | error(f"status code {r.status_code} returned whilst trying to find the game's ID from discord") 628 | 629 | response = [] 630 | if r == "error": 631 | if loops > 3: 632 | error("failed to fetch the list of discord's game IDs") 633 | 634 | else: 635 | getGameDiscordID(loops + 1) 636 | return 637 | 638 | else: 639 | response = r.json() 640 | 641 | ignoredChars = "®©™℠" 642 | 643 | # check if the "customGameIDs.json" file exists, if so, open it 644 | if exists(f"{dirname(abspath(__file__))}/data/customGameIDs.json"): 645 | with open(f"{dirname(abspath(__file__))}/data/customGameIDs.json", "r") as f: 646 | # load the values of the file 647 | gameIDsFile = json.load(f) 648 | 649 | log(f"loaded {len(gameIDsFile)} custom discord game IDs from disk") 650 | 651 | # add the values from the file directly to the list returned by discord 652 | for i in gameIDsFile: 653 | response.append({ 654 | "name": i, 655 | "id": gameIDsFile[i] 656 | }) 657 | 658 | global appID 659 | 660 | # loop thru all games 661 | for i in response: 662 | gameNames = [] 663 | gameNames.append(i["name"]) 664 | # for handling demos of games, adding it as a valid discord name because it's easier 665 | gameNames.append(i["name"] + " demo") 666 | 667 | # make a list containing all the names of said game 668 | if "aliases" in i: 669 | aliases = i["aliases"] 670 | for alias in aliases: 671 | gameNames.append(alias) 672 | 673 | for j in range(len(gameNames)): 674 | gameNames[j] = removeChars( 675 | gameNames[j].lower(), 676 | ignoredChars) 677 | 678 | # if it's the same, we successfully found the discord game ID 679 | if removeChars(gameName.lower(), ignoredChars) in gameNames: 680 | log(f"found the discord game ID for {removeChars(gameName.lower(), ignoredChars)}") 681 | appID = i["id"] 682 | return 683 | 684 | # if the game was fetched using the local checker, instead of thru steam 685 | if isPlayingLocalGame: 686 | log(f"could not find the discord game ID for {gameName}, defaulting to the secondary, local game ID") 687 | appID = defaultLocalAppID 688 | return 689 | 690 | log(f"could not find the discord game ID for {gameName}, defaulting to well, the default game ID") 691 | appID = defaultAppID 692 | return 693 | 694 | # get game's icon straight from Discord's CDN 695 | def getImageFromDiscord(): 696 | global coverImage 697 | global coverImageText 698 | 699 | log("getting icon from Discord") 700 | 701 | try: 702 | r = makeWebRequest(f"https://discordapp.com/api/v8/applications/{appID}/rpc") 703 | if r == "error": 704 | return 705 | 706 | if r.status_code != 200: 707 | error(f"status code {r.status_code} returned when requesting more info about {gameName} from Discord, ignoring") 708 | coverImage = None 709 | coverImageText = None 710 | return 711 | 712 | respone = r.json() 713 | 714 | coverImage = f"https://cdn.discordapp.com/app-icons/{appID}/{respone['icon']}.webp" 715 | coverImageText = f"{respone['name']}" 716 | 717 | log(f"successfully found Discord icon for {gameName}") 718 | 719 | except Exception as e: 720 | error(f"Exception {e} raised when trying to fetch {gameName}'s icon from Discord, ignoring") 721 | coverImage = None 722 | coverImageText = None 723 | 724 | # checks if any local games are running 725 | def getLocalPresence(): 726 | config = getConfigFile() 727 | # load the custom games, all lower case 728 | localGames = list(map(str.lower, config["LOCAL_GAMES"]["GAMES"])) 729 | 730 | 731 | gameFound = False 732 | # process = None 733 | 734 | try: 735 | # get a list of all open applications, make a list of their creation times, and their names 736 | processCreationTimes = [] 737 | processNames = [] 738 | 739 | for proc in psutil.process_iter(): 740 | try: 741 | processCreationTimes.append(proc.create_time()) 742 | processNames.append(proc.name().lower()) 743 | except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): 744 | # Continue instead of return when processes have updated since they were grabbed 745 | continue 746 | except: 747 | return 748 | 749 | # loop thru all games we're supposed to look for 750 | for game in localGames: 751 | # check if that game is running locally 752 | if game in processNames: 753 | # write down the process name and it's creation time 754 | processCreationTime = processCreationTimes[processNames.index(game)] 755 | processName = game 756 | 757 | if not isPlaying: 758 | log(f"found {processName} running locally") 759 | 760 | gameFound = True 761 | break 762 | 763 | # don't continue if it didn't find a game 764 | if not gameFound: 765 | return 766 | 767 | global gameName 768 | global startTime 769 | global isPlayingLocalGame 770 | global isPlayingSteamGame 771 | 772 | 773 | if exists(f"{dirname(abspath(__file__))}/data/games.txt"): 774 | with open(f'{dirname(abspath(__file__))}/data/games.txt', 'r+') as gamesFile: 775 | for i in gamesFile: 776 | # remove the new line 777 | game = i.split("\n") 778 | # split first and second part of the string 779 | game = game[0].split("=") 780 | 781 | # if there's a match 782 | if game[0].lower() == processName.lower(): 783 | gameName = game[1] 784 | startTime = processCreationTime 785 | isPlayingLocalGame = True 786 | isPlayingSteamGame = False 787 | 788 | if not isPlaying: 789 | log(f"found name for {gameName} on disk") 790 | 791 | gamesFile.close() 792 | return 793 | 794 | # if there wasn't a local entry for the game 795 | log(f"could not find a name for {processName}, adding an entry to games.txt") 796 | gamesFile.write(f"{processName}={processName.title()}\n") 797 | gamesFile.close() 798 | 799 | # if games.txt doesn't exist at all 800 | else: 801 | log("games.txt does not exist, creating one") 802 | with open(f'{dirname(abspath(__file__))}/data/games.txt', 'a') as gamesFile: 803 | gamesFile.write(f"{processName}={processName.title()}\n") 804 | gamesFile.close() 805 | 806 | 807 | isPlayingLocalGame = True 808 | isPlayingSteamGame = False 809 | gameName = processName.title() 810 | startTime = processCreationTime 811 | 812 | 813 | 814 | def setPresenceDetails(): 815 | global activeRichPresence 816 | global startTime 817 | global currentGameBlacklisted 818 | 819 | details = None 820 | state = None 821 | buttons = None 822 | 823 | # ignore game if it is in blacklist, case insensitive check 824 | if gameName.casefold() in map(str.casefold, blacklist): 825 | if not currentGameBlacklisted: 826 | log(f"{gameName} is in blacklist, not creating RPC object.") 827 | currentGameBlacklisted = True 828 | return 829 | 830 | currentGameBlacklisted = False 831 | 832 | # ignore game if it is not whitelisted, case insensitive check 833 | if whitelist and gameName.casefold() not in map(str.casefold, whitelist): 834 | log(f"{gameName} is not in whitelist, not creating RPC object.") 835 | return 836 | 837 | # if the game ID is corresponding to "a game on steam" - set the details field to be the real game name 838 | if appID == defaultAppID or appID == defaultLocalAppID: 839 | details = gameName 840 | 841 | if activeRichPresence != gameRichPresence: 842 | if gameRichPresence != "": 843 | if details == None: 844 | log(f"setting the details for {gameName} to `{gameRichPresence}`") 845 | details = gameRichPresence 846 | elif state == None: 847 | log(f"setting the state for {gameName} to `{gameRichPresence}`") 848 | state = gameRichPresence 849 | 850 | 851 | activeRichPresence = gameRichPresence 852 | 853 | if state == None and gameReviewScore != 0: 854 | state = f"{gameReviewString} - {gameReviewScore}%" 855 | 856 | 857 | if addSteamStoreButton and gameSteamID != 0: 858 | price = getGamePrice() 859 | if price == None: 860 | price = "Free" 861 | else: 862 | price += " USD" 863 | label = f"{gameName} on steam - {price}" 864 | if len(label) > 32: 865 | label = f"{gameName} - {price}" 866 | if len(label) > 32: 867 | label = f"get it on steam! - {price}" 868 | if len(label) > 32: 869 | label = f"on steam! - {price}" 870 | 871 | buttons = [{"label": label, "url": f"https://store.steampowered.com/app/{gameSteamID}"}] 872 | 873 | 874 | log("pushing presence to Discord") 875 | 876 | # sometimes startTime is 0 when it reaches this point, which results in a crash 877 | # i do *NOT* know how or why it does this, adding these 2 lines of code seems to fix it 878 | if startTime == 0: 879 | startTime = round((time())) 880 | 881 | try: 882 | RPC.update( 883 | details = details, state = state, 884 | start = startTime, 885 | large_image = coverImage, large_text = coverImageText, 886 | small_image = customIconURL, small_text = customIconText, 887 | buttons=buttons 888 | ) 889 | 890 | except Exception as e: 891 | error(f"pushing presence failed...\nError encountered: {e}") 892 | 893 | def verifyProjectVersion(): 894 | metaFile = getMetaFile() 895 | if metaFile["structure-version"] == "0": 896 | print("----------------------------------------------------------") 897 | log("updating meta.json's structure-version to `1`") 898 | log("importing libraries for meta update") 899 | try: 900 | import shutil 901 | except ImportError: 902 | error("import error whilst importing `shutil`, exiting") 903 | exit() 904 | 905 | if not os.path.exists(f"{dirname(__file__)}/data"): 906 | log(f"creating {dirname(__file__)}/data/") 907 | os.makedirs(f"{dirname(__file__)}/data") 908 | 909 | expectedFiles = { 910 | "icons.txt": "", 911 | "games.txt": "", 912 | "customGameIDs.json": "{}" 913 | } 914 | 915 | for i in expectedFiles: 916 | if not os.path.exists(i): 917 | log(f"creating file `{i}` with content `{expectedFiles[i]}`") 918 | with open(f"{dirname(__file__)}/{i}", "w") as f: 919 | f.write(expectedFiles[i]) 920 | 921 | try: 922 | log(f"moving {dirname(__file__)}/icons.txt") 923 | shutil.move(f"{dirname(__file__)}/icons.txt", f"{dirname(__file__)}/data/icons.txt") 924 | log(f"moving {dirname(__file__)}/games.txt") 925 | shutil.move(f"{dirname(__file__)}/games.txt", f"{dirname(__file__)}/data/games.txt") 926 | log(f"moving {dirname(__file__)}/customGameIDs.json") 927 | shutil.move(f"{dirname(__file__)}/customGameIDs.json", f"{dirname(__file__)}/data/customGameIDs.json") 928 | log(f"moving {dirname(__file__)}/meta.json") 929 | shutil.move(f"{dirname(__file__)}/meta.json", f"{dirname(__file__)}/data/meta.json") 930 | 931 | writeToMetaFile(["structure-version"], "1") 932 | except Exception as e: 933 | error(f"error encountered whilst trying to update the config-version to version 1, exiting\nError encountered: {e}") 934 | exit() 935 | print("----------------------------------------------------------") 936 | elif metaFile["structure-version"] == "1": 937 | print("----------------------------------------------------------") 938 | log("progam's current folder structure version is up to date...") 939 | print("----------------------------------------------------------") 940 | else: 941 | error("invalid structure-version found in meta.json, exiting") 942 | exit() 943 | 944 | # checks if the program has any updates 945 | def checkForUpdate(): 946 | URL = f"https://api.github.com/repos/JustTemmie/steam-presence/releases/latest" 947 | try: 948 | r = requests.get(URL) 949 | except Exception as e: 950 | error(f"failed to check if a newer version is available, falling back...\nfull error: {e}") 951 | return 952 | 953 | if r.status_code != 200: 954 | error(f"status code {r.status_code} recieved when trying to find latest version of steam presence, ignoring") 955 | return 956 | 957 | # the newest current release tag name 958 | newestVersion = r.json()["tag_name"] 959 | 960 | # make the version numbers easier to parse 961 | parsableCurrentVersion = currentVersion.replace("v", "") # the `currentVersion` variable is set in the main() function so i'm less likely to forget, lol 962 | parsableNewestVersion = newestVersion.replace("v", "") 963 | 964 | parsableCurrentVersion = parsableCurrentVersion.split(".") 965 | parsableNewestVersion = parsableNewestVersion.split(".") 966 | 967 | 968 | # make sure both ot the version lists have 4 entries so that the zip() function below works properly 969 | for i in [parsableNewestVersion, parsableCurrentVersion]: 970 | while len(i) < 4: 971 | i.append(0) 972 | 973 | # loop thru both of the version lists, 974 | for new, old in zip(parsableNewestVersion, parsableCurrentVersion): 975 | if int(new) > int(old): 976 | print("----------------------------------------------------------") 977 | print("there's a newer update available!") 978 | print(f"if you wish to upload from `{currentVersion}` to `{newestVersion}` simply run `git pull` from the terminal/cmd in the same folder as main.py") 979 | print(f"commits made in this time frame: https://github.com/JustTemmie/steam-presence/compare/{currentVersion}...{newestVersion}") 980 | print("----------------------------------------------------------") 981 | # untested, but it should work 982 | if int(parsableNewestVersion[0]) == 2: 983 | print("DO NOTE, UPDATING TO VERSION 2 OF STEAM PRESENCE WILL REQUIRE YOU TO COMPLETELY REDO YOUR CONFIG FILE") 984 | print("----------------------------------------------------------") 985 | return 986 | # if the current version is newer than the "newest one", just return to make sure it doesn't falsly report anything 987 | # this shouldn't ever come up for most people - but it's probably a good idea to include this if statement; just in case 988 | if int(old) > int(new): 989 | return 990 | 991 | def main(): 992 | global currentVersion 993 | # this always has to match the newest release tag 994 | currentVersion = "v1.12.2" 995 | 996 | # check if there's any updates for the program 997 | checkForUpdate() 998 | # does various things, such as verifying that certain files are in certain locations 999 | # well it does 1 thing at the time of writing, but i'll probably forget to update this comment when i add more lol 1000 | verifyProjectVersion() 1001 | 1002 | global userID 1003 | global steamAPIKey 1004 | global localGames 1005 | global defaultAppID 1006 | global defaultLocalAppID 1007 | global blacklist 1008 | global whitelist 1009 | global currentGameBlacklisted 1010 | currentGameBlacklisted = False 1011 | 1012 | global appID 1013 | global startTime 1014 | global gameName 1015 | global gameRichPresence 1016 | global activeRichPresence 1017 | global gameSteamID 1018 | global gameReviewScore 1019 | global gameReviewString 1020 | global isPlaying 1021 | global isPlayingLocalGame 1022 | global isPlayingSteamGame 1023 | 1024 | global coverImage 1025 | global coverImageText 1026 | 1027 | global RPC 1028 | global sgdb 1029 | 1030 | global gridEnabled 1031 | global steamStoreCoverartBackup 1032 | global customIconURL 1033 | global customIconText 1034 | 1035 | global addSteamStoreButton 1036 | 1037 | 1038 | log("loading config file") 1039 | config = getConfigFile() 1040 | 1041 | steamAPIKey = config["STEAM_API_KEY"] 1042 | defaultAppID = config["DISCORD_APPLICATION_ID"] 1043 | defaultLocalAppID = config["LOCAL_GAMES"]["LOCAL_DISCORD_APPLICATION_ID"] 1044 | doLocalGames = config["LOCAL_GAMES"]["ENABLED"] 1045 | localGames = config["LOCAL_GAMES"]["GAMES"] 1046 | blacklist = config["BLACKLIST"] 1047 | whitelist = config["WHITELIST"] 1048 | 1049 | steamStoreCoverartBackup = config["COVER_ART"]["USE_STEAM_STORE_FALLBACK"] 1050 | gridEnabled = config["COVER_ART"]["STEAM_GRID_DB"]["ENABLED"] 1051 | gridKey = config["COVER_ART"]["STEAM_GRID_DB"]["STEAM_GRID_API_KEY"] 1052 | 1053 | doCustomIcon = config["CUSTOM_ICON"]["ENABLED"] 1054 | 1055 | doWebScraping = config["WEB_SCRAPE"] 1056 | 1057 | doCustomGame = config["GAME_OVERWRITE"]["ENABLED"] 1058 | customGameName = config["GAME_OVERWRITE"]["NAME"] 1059 | customGameStartOffset = config["GAME_OVERWRITE"]["SECONDS_SINCE_START"] 1060 | 1061 | doSteamRichPresence = config["FETCH_STEAM_RICH_PRESENCE"] 1062 | fetchSteamReviews = config["FETCH_STEAM_REVIEWS"] 1063 | addSteamStoreButton = config["ADD_STEAM_STORE_BUTTON"] 1064 | 1065 | # load these later on 1066 | customIconURL = None 1067 | customIconText = None 1068 | 1069 | 1070 | # loads the steam user id 1071 | userID = "" 1072 | if type(config["USER_IDS"]) == str: 1073 | userID = config["USER_IDS"] 1074 | elif type(config["USER_IDS"]) == list: 1075 | for i in config["USER_IDS"]: 1076 | userID += f"{i}," 1077 | # remove the last comma 1078 | userID = userID[:-1] 1079 | else: 1080 | error( 1081 | "type error whilst reading the USER_IDS field, please make sure the formating is correct\n", 1082 | "it should be something like `\"USER_IDS\": \"76561198845672697\",`", 1083 | ) 1084 | 1085 | # declare variables 1086 | isPlaying = False 1087 | isPlayingLocalGame = False 1088 | isPlayingSteamGame = False 1089 | startTime = 0 1090 | coverImage = None 1091 | coverImageText = None 1092 | gameSteamID = 0 1093 | gameReviewScore = 0 1094 | gameReviewString = "" 1095 | gameName = "" 1096 | previousGameName = "" 1097 | gameRichPresence = "" 1098 | # the rich presence text that's actually in the current discord presence, set to beaver cause it can't start empty 1099 | activeRichPresence = "beaver" 1100 | 1101 | 1102 | if doCustomIcon: 1103 | log("loading custom icon") 1104 | customIconURL = config["CUSTOM_ICON"]["URL"] 1105 | customIconText = config["CUSTOM_ICON"]["TEXT"] 1106 | 1107 | # initialize the steam grid database object 1108 | if gridEnabled: 1109 | log("intializing the SteamGrid database...") 1110 | sgdb = SteamGridDB(gridKey) 1111 | 1112 | # everything ready! 1113 | log("everything is ready!") 1114 | print("----------------------------------------------------------") 1115 | 1116 | while True: 1117 | # these values are taken from the config file every cycle, so the user can change these whilst the script is running 1118 | config = getConfigFile() 1119 | 1120 | steamAPIKey = config["STEAM_API_KEY"] 1121 | defaultAppID = config["DISCORD_APPLICATION_ID"] 1122 | defaultLocalAppID = config["LOCAL_GAMES"]["LOCAL_DISCORD_APPLICATION_ID"] 1123 | doLocalGames = config["LOCAL_GAMES"]["ENABLED"] 1124 | localGames = config["LOCAL_GAMES"]["GAMES"] 1125 | blacklist = config["BLACKLIST"] 1126 | whitelist = config["WHITELIST"] 1127 | 1128 | steamStoreCoverartBackup = config["COVER_ART"]["USE_STEAM_STORE_FALLBACK"] 1129 | gridEnabled = config["COVER_ART"]["STEAM_GRID_DB"]["ENABLED"] 1130 | gridKey = config["COVER_ART"]["STEAM_GRID_DB"]["STEAM_GRID_API_KEY"] 1131 | 1132 | doCustomIcon = config["CUSTOM_ICON"]["ENABLED"] 1133 | 1134 | doWebScraping = config["WEB_SCRAPE"] 1135 | 1136 | doCustomGame = config["GAME_OVERWRITE"]["ENABLED"] 1137 | customGameName = config["GAME_OVERWRITE"]["NAME"] 1138 | customGameStartOffset = config["GAME_OVERWRITE"]["SECONDS_SINCE_START"] 1139 | 1140 | 1141 | # set the custom game 1142 | if doCustomGame: 1143 | gameName = customGameName 1144 | 1145 | else: 1146 | gameName = getSteamPresence() 1147 | 1148 | if gameName == "" and doLocalGames: 1149 | getLocalPresence() 1150 | 1151 | if gameName == "" and doWebScraping: 1152 | getWebScrapePresence() 1153 | 1154 | if doSteamRichPresence and isPlayingSteamGame: 1155 | getSteamRichPresence() 1156 | 1157 | 1158 | # if the game has changed 1159 | if previousGameName != gameName: 1160 | # try finding the game on steam, and saving it's ID to `gameSteamID` 1161 | getGameSteamID() 1162 | 1163 | # fetch the steam reviews if enabled 1164 | if fetchSteamReviews: 1165 | if gameName != "" and gameSteamID != 0: 1166 | getGameReviews() 1167 | else: 1168 | gameReviewScore = 0 1169 | 1170 | # if the game has been closed 1171 | if gameName == "": 1172 | # only close once 1173 | if isPlaying: 1174 | log(f"closing previous rich presence object, no longer playing {previousGameName}") 1175 | print("----------------------------------------------------------") 1176 | RPC.close() 1177 | 1178 | # set previous game name to "", this is used to check if the game has changed 1179 | # if we don't use this and the user opens a game, closes it, and then relaunches it - the script won't detect that 1180 | previousGameName = "" 1181 | startTime = 0 1182 | isPlaying = False 1183 | 1184 | # if the game has changed or a new game has been opened 1185 | else: 1186 | # save the time as the time we started playing 1187 | # if we're playing a localgame the time has already been set 1188 | if not isPlayingLocalGame: 1189 | startTime = round(time()) 1190 | 1191 | if doCustomGame: 1192 | log(f"using custom game '{customGameName}'") 1193 | # set the start time to the custom game start time 1194 | if customGameStartOffset != 0: 1195 | startTime = round(time() - customGameStartOffset) 1196 | 1197 | log(f"game changed, updating to '{gameName}'") 1198 | 1199 | # fetch the new app ID 1200 | getGameDiscordID() 1201 | 1202 | # get cover image 1203 | getGameImage() 1204 | 1205 | # checks to make sure the old RPC has been closed 1206 | if isPlaying: 1207 | log(f"RPC for {previousGameName} still open, closing it") 1208 | RPC.close() 1209 | 1210 | # redefine and reconnect to the RPC object 1211 | log(f"creating new rich presence object for {gameName}") 1212 | RPC = Presence(client_id=appID) 1213 | RPC.connect() 1214 | 1215 | # push info to RPC 1216 | setPresenceDetails() 1217 | 1218 | isPlaying = True 1219 | previousGameName = gameName 1220 | 1221 | print("----------------------------------------------------------") 1222 | 1223 | if activeRichPresence != gameRichPresence and isPlaying and not currentGameBlacklisted: 1224 | setPresenceDetails() 1225 | print("----------------------------------------------------------") 1226 | 1227 | # sleep for a 30 seconds for every user we query, to avoid getting banned from the steam API 1228 | sleep(30 * (userID.count(",") + 1)) 1229 | 1230 | 1231 | if __name__ == "__main__": 1232 | main() 1233 | # try: 1234 | # pass 1235 | # except Exception as e: 1236 | # error(f"{e}\nautomatically restarting script in 60 seconds\n") 1237 | # sleep(60) 1238 | # python = sys.executable 1239 | # log("restarting...") 1240 | # os.execl(python, python, *sys.argv) 1241 | --------------------------------------------------------------------------------