├── .flake8 ├── .github ├── dependabot.yml ├── label-actions.yml ├── semantic.yml └── workflows │ ├── CI.yml │ ├── codeql.yml │ ├── common-lint.yml │ ├── issues.yml │ ├── release-notifier-moonlight.yml │ ├── release-notifier.yml │ └── update-changelog.yml ├── .gitignore ├── LICENSE ├── README.rst ├── codecov.yml ├── gsms ├── __init__.py └── gsms.py ├── requirements-dev.txt ├── requirements.txt ├── scripts └── build.py ├── sunshine.ico └── tests ├── box.png ├── conftest.py └── unit ├── __init__.py └── test_gsms.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | filename = 3 | *.py 4 | max-line-length = 120 5 | extend-exclude = 6 | venv/ 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This file is centrally managed in https://github.com//.github/ 3 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in 4 | # the above-mentioned repo. 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | time: "07:30" 13 | open-pull-requests-limit: 10 14 | 15 | - package-ecosystem: "docker" 16 | directory: "/" 17 | schedule: 18 | interval: "daily" 19 | time: "08:00" 20 | open-pull-requests-limit: 10 21 | 22 | - package-ecosystem: "github-actions" 23 | directory: "/" 24 | schedule: 25 | interval: "daily" 26 | time: "08:30" 27 | open-pull-requests-limit: 10 28 | groups: 29 | docker-actions: 30 | applies-to: version-updates 31 | patterns: 32 | - "docker/*" 33 | github-actions: 34 | applies-to: version-updates 35 | patterns: 36 | - "actions/*" 37 | - "github/*" 38 | lizardbyte-actions: 39 | applies-to: version-updates 40 | patterns: 41 | - "LizardByte/*" 42 | 43 | - package-ecosystem: "npm" 44 | directory: "/" 45 | schedule: 46 | interval: "daily" 47 | time: "09:00" 48 | open-pull-requests-limit: 10 49 | groups: 50 | dev-dependencies: 51 | applies-to: version-updates 52 | dependency-type: "development" 53 | 54 | - package-ecosystem: "nuget" 55 | directory: "/" 56 | schedule: 57 | interval: "daily" 58 | time: "09:30" 59 | open-pull-requests-limit: 10 60 | 61 | - package-ecosystem: "pip" 62 | directory: "/" 63 | schedule: 64 | interval: "daily" 65 | time: "10:00" 66 | open-pull-requests-limit: 10 67 | groups: 68 | pytest-dependencies: 69 | applies-to: version-updates 70 | patterns: 71 | - "pytest*" 72 | 73 | - package-ecosystem: "gitsubmodule" 74 | directory: "/" 75 | schedule: 76 | interval: "daily" 77 | time: "10:30" 78 | open-pull-requests-limit: 10 79 | -------------------------------------------------------------------------------- /.github/label-actions.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This file is centrally managed in https://github.com//.github/ 3 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in 4 | # the above-mentioned repo. 5 | 6 | # Configuration for Label Actions - https://github.com/dessant/label-actions 7 | 8 | added: 9 | comment: > 10 | This feature has been added and will be available in the next release. 11 | fixed: 12 | comment: > 13 | This issue has been fixed and will be available in the next release. 14 | invalid:duplicate: 15 | comment: > 16 | :wave: @{issue-author}, this appears to be a duplicate of a pre-existing issue. 17 | close: true 18 | lock: true 19 | unlabel: 'status:awaiting-triage' 20 | 21 | -invalid:duplicate: 22 | reopen: true 23 | unlock: true 24 | 25 | invalid:support: 26 | comment: > 27 | :wave: @{issue-author}, we use the issue tracker exclusively for bug reports. 28 | However, this issue appears to be a support request. Please use our 29 | [Support Center](https://app.lizardbyte.dev/support) for support issues. Thanks. 30 | close: true 31 | lock: true 32 | lock-reason: 'off-topic' 33 | unlabel: 'status:awaiting-triage' 34 | 35 | -invalid:support: 36 | reopen: true 37 | unlock: true 38 | 39 | invalid:template-incomplete: 40 | issues: 41 | comment: > 42 | :wave: @{issue-author}, please edit your issue to complete the template with 43 | all the required info. Your issue will be automatically closed in 5 days if 44 | the template is not completed. Thanks. 45 | prs: 46 | comment: > 47 | :wave: @{issue-author}, please edit your PR to complete the template with 48 | all the required info. Your PR will be automatically closed in 5 days if 49 | the template is not completed. Thanks. 50 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This file is centrally managed in https://github.com//.github/ 3 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in 4 | # the above-mentioned repo. 5 | 6 | # This is the configuration file for https://github.com/Ezard/semantic-prs 7 | 8 | enabled: true 9 | titleOnly: true # We only use the PR title as we squash and merge 10 | commitsOnly: false 11 | titleAndCommits: false 12 | anyCommit: false 13 | allowMergeCommits: false 14 | allowRevertCommits: false 15 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | pull_request: 6 | branches: [master] 7 | types: [opened, synchronize, reopened] 8 | push: 9 | branches: [master] 10 | workflow_dispatch: 11 | 12 | concurrency: 13 | group: "${{ github.workflow }}-${{ github.ref }}" 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | setup_release: 18 | name: Setup Release 19 | outputs: 20 | publish_release: ${{ steps.setup_release.outputs.publish_release }} 21 | release_body: ${{ steps.setup_release.outputs.release_body }} 22 | release_commit: ${{ steps.setup_release.outputs.release_commit }} 23 | release_generate_release_notes: ${{ steps.setup_release.outputs.release_generate_release_notes }} 24 | release_tag: ${{ steps.setup_release.outputs.release_tag }} 25 | release_version: ${{ steps.setup_release.outputs.release_version }} 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | 31 | - name: Setup Release 32 | id: setup_release 33 | uses: LizardByte/setup-release-action@v2024.919.143601 34 | with: 35 | github_token: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | build: 38 | runs-on: windows-2019 39 | needs: 40 | - setup_release 41 | 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@v4 45 | 46 | - name: Install Python 47 | uses: actions/setup-python@v5 48 | with: 49 | python-version: '3.11' 50 | architecture: 'x64' 51 | 52 | - name: Set up Python Dependencies 53 | run: | 54 | python -m pip install --upgrade pip setuptools wheel 55 | python -m pip install -r requirements-dev.txt 56 | 57 | - name: Build pyinstaller package 58 | run: | 59 | python -u ./scripts/build.py 60 | 61 | - name: Test with pytest 62 | id: test 63 | shell: bash 64 | run: | 65 | python -m pytest \ 66 | -rxXs \ 67 | --tb=native \ 68 | --verbose \ 69 | --color=yes \ 70 | --cov=gsms \ 71 | tests 72 | 73 | - name: Upload coverage 74 | # any except cancelled or skipped 75 | if: always() && (steps.test.outcome == 'success' || steps.test.outcome == 'failure') 76 | uses: codecov/codecov-action@v4 77 | with: 78 | fail_ci_if_error: true 79 | token: ${{ secrets.CODECOV_TOKEN }} 80 | verbose: true 81 | 82 | - name: Upload Artifacts 83 | if: ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} 84 | uses: actions/upload-artifact@v4 85 | with: 86 | name: GSMS 87 | if-no-files-found: error # 'warn' or 'ignore' are also available, defaults to `warn` 88 | path: | 89 | ${{ github.workspace }}/dist/gsms.exe 90 | 91 | - name: Create/Update GitHub Release 92 | if: ${{ needs.setup_release.outputs.publish_release == 'true' }} 93 | uses: LizardByte/create-release-action@v2024.919.143026 94 | with: 95 | allowUpdates: false 96 | artifacts: "${{ github.workspace }}/dist/gsms.exe" 97 | body: ${{ needs.setup_release.outputs.release_body }} 98 | generateReleaseNotes: ${{ needs.setup_release.outputs.release_generate_release_notes }} 99 | name: ${{ needs.setup_release.outputs.release_tag }} 100 | prerelease: true 101 | tag: ${{ needs.setup_release.outputs.release_tag }} 102 | token: ${{ secrets.GH_BOT_TOKEN }} 103 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This action is centrally managed in https://github.com//.github/ 3 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in 4 | # the above-mentioned repo. 5 | 6 | # This workflow will analyze all supported languages in the repository using CodeQL Analysis. 7 | 8 | name: "CodeQL" 9 | 10 | on: 11 | push: 12 | branches: ["master"] 13 | pull_request: 14 | branches: ["master"] 15 | schedule: 16 | - cron: '00 12 * * 0' # every Sunday at 12:00 UTC 17 | 18 | concurrency: 19 | group: "${{ github.workflow }}-${{ github.ref }}" 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | languages: 24 | name: Get language matrix 25 | runs-on: ubuntu-latest 26 | outputs: 27 | matrix: ${{ steps.lang.outputs.result }} 28 | continue: ${{ steps.continue.outputs.result }} 29 | steps: 30 | - name: Get repo languages 31 | uses: actions/github-script@v7 32 | id: lang 33 | with: 34 | script: | 35 | // CodeQL supports ['cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift'] 36 | // Use only 'java' to analyze code written in Java, Kotlin or both 37 | // Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 38 | // Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 39 | const supported_languages = ['cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift'] 40 | 41 | const remap_languages = { 42 | 'c++': 'cpp', 43 | 'c#': 'csharp', 44 | 'kotlin': 'java', 45 | 'typescript': 'javascript', 46 | } 47 | 48 | const repo = context.repo 49 | const response = await github.rest.repos.listLanguages(repo) 50 | let matrix = { 51 | "include": [] 52 | } 53 | 54 | for (let [key, value] of Object.entries(response.data)) { 55 | // remap language 56 | if (remap_languages[key.toLowerCase()]) { 57 | console.log(`Remapping language: ${key} to ${remap_languages[key.toLowerCase()]}`) 58 | key = remap_languages[key.toLowerCase()] 59 | } 60 | if (supported_languages.includes(key.toLowerCase())) { 61 | console.log(`Found supported language: ${key}`) 62 | let osList = ['ubuntu-latest']; 63 | if (key.toLowerCase() === 'swift') { 64 | osList = ['macos-latest']; 65 | } else if (key.toLowerCase() === 'cpp') { 66 | // TODO: update macos to latest after the below issue is resolved 67 | // https://github.com/github/codeql-action/issues/2266 68 | osList = ['macos-13', 'ubuntu-latest', 'windows-latest']; 69 | } 70 | for (let os of osList) { 71 | // set name for matrix 72 | if (osList.length == 1) { 73 | name = key.toLowerCase() 74 | } else { 75 | name = `${key.toLowerCase()}, ${os}` 76 | } 77 | 78 | // add to matrix 79 | matrix['include'].push({"language": key.toLowerCase(), "os": os, "name": name}) 80 | } 81 | } 82 | } 83 | 84 | // print languages 85 | console.log(`matrix: ${JSON.stringify(matrix)}`) 86 | 87 | return matrix 88 | 89 | - name: Continue 90 | uses: actions/github-script@v7 91 | id: continue 92 | with: 93 | script: | 94 | // if matrix['include'] is an empty list return false, otherwise true 95 | const matrix = ${{ steps.lang.outputs.result }} // this is already json encoded 96 | 97 | if (matrix['include'].length == 0) { 98 | return false 99 | } else { 100 | return true 101 | } 102 | 103 | analyze: 104 | name: Analyze (${{ matrix.name }}) 105 | if: ${{ needs.languages.outputs.continue == 'true' }} 106 | defaults: 107 | run: 108 | shell: ${{ matrix.os == 'windows-latest' && 'msys2 {0}' || 'bash' }} 109 | env: 110 | GITHUB_CODEQL_BUILD: true 111 | needs: [languages] 112 | runs-on: ${{ matrix.os || 'ubuntu-latest' }} 113 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 114 | permissions: 115 | actions: read 116 | contents: read 117 | security-events: write 118 | 119 | strategy: 120 | fail-fast: false 121 | matrix: ${{ fromJson(needs.languages.outputs.matrix) }} 122 | 123 | steps: 124 | - name: Maximize build space 125 | if: >- 126 | runner.os == 'Linux' && 127 | matrix.language == 'cpp' 128 | uses: easimon/maximize-build-space@v10 129 | with: 130 | root-reserve-mb: 30720 131 | remove-dotnet: ${{ (matrix.language == 'csharp' && 'false') || 'true' }} 132 | remove-android: 'true' 133 | remove-haskell: 'true' 134 | remove-codeql: 'false' 135 | remove-docker-images: 'true' 136 | 137 | - name: Checkout repository 138 | uses: actions/checkout@v4 139 | with: 140 | submodules: recursive 141 | 142 | - name: Setup msys2 143 | if: >- 144 | runner.os == 'Windows' && 145 | matrix.language == 'cpp' 146 | uses: msys2/setup-msys2@v2 147 | with: 148 | msystem: ucrt64 149 | update: true 150 | 151 | # Initializes the CodeQL tools for scanning. 152 | - name: Initialize CodeQL 153 | uses: github/codeql-action/init@v3 154 | with: 155 | languages: ${{ matrix.language }} 156 | # If you wish to specify custom queries, you can do so here or in a config file. 157 | # By default, queries listed here will override any specified in a config file. 158 | # Prefix the list here with "+" to use these queries and those in the config file. 159 | 160 | # yamllint disable-line rule:line-length 161 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 162 | # queries: security-extended,security-and-quality 163 | config: | 164 | paths-ignore: 165 | - build 166 | - node_modules 167 | - third-party 168 | 169 | # Pre autobuild 170 | # create a file named .codeql-prebuild-${{ matrix.language }}.sh in the root of your repository 171 | # create a file named .codeql-build-${{ matrix.language }}.sh in the root of your repository 172 | - name: Prebuild 173 | id: prebuild 174 | run: | 175 | # check if prebuild script exists 176 | filename=".codeql-prebuild-${{ matrix.language }}-${{ runner.os }}.sh" 177 | if [ -f "./${filename}" ]; then 178 | echo "Running prebuild script: ${filename}" 179 | ./${filename} 180 | fi 181 | 182 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). 183 | - name: Autobuild 184 | if: steps.prebuild.outputs.skip_autobuild != 'true' 185 | uses: github/codeql-action/autobuild@v3 186 | 187 | - name: Perform CodeQL Analysis 188 | uses: github/codeql-action/analyze@v3 189 | with: 190 | category: "/language:${{matrix.language}}" 191 | output: sarif-results 192 | upload: failure-only 193 | 194 | - name: filter-sarif 195 | uses: advanced-security/filter-sarif@v1 196 | with: 197 | input: sarif-results/${{ matrix.language }}.sarif 198 | output: sarif-results/${{ matrix.language }}.sarif 199 | patterns: | 200 | -build/** 201 | -node_modules/** 202 | -third\-party/** 203 | 204 | - name: Upload SARIF 205 | uses: github/codeql-action/upload-sarif@v3 206 | with: 207 | sarif_file: sarif-results/${{ matrix.language }}.sarif 208 | 209 | - name: Upload loc as a Build Artifact 210 | uses: actions/upload-artifact@v4 211 | with: 212 | name: sarif-results-${{ matrix.language }}-${{ runner.os }} 213 | path: sarif-results 214 | retention-days: 1 215 | -------------------------------------------------------------------------------- /.github/workflows/common-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This action is centrally managed in https://github.com//.github/ 3 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in 4 | # the above-mentioned repo. 5 | 6 | # Common linting. 7 | 8 | name: common lint 9 | 10 | on: 11 | pull_request: 12 | branches: [master] 13 | types: [opened, synchronize, reopened] 14 | 15 | concurrency: 16 | group: "${{ github.workflow }}-${{ github.ref }}" 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | lint: 21 | name: Common Lint 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | 27 | - name: Set up Python 28 | uses: actions/setup-python@v5 29 | with: 30 | python-version: '3.12' 31 | 32 | - name: Install dependencies 33 | run: | 34 | python -m pip install --upgrade \ 35 | pip \ 36 | setuptools \ 37 | wheel \ 38 | cmakelang \ 39 | flake8 \ 40 | nb-clean \ 41 | nbqa[toolchain] 42 | 43 | - name: C++ - find files 44 | id: cpp_files 45 | run: | 46 | # find files 47 | found_files=$(find . -type f \ 48 | -iname "*.c" -o \ 49 | -iname "*.cpp" -o \ 50 | -iname "*.h" -o \ 51 | -iname "*.hpp" -o \ 52 | -iname "*.m" -o \ 53 | -iname "*.mm" \ 54 | ) 55 | ignore_files=$(find . -type f -iname ".clang-format-ignore") 56 | 57 | # Loop through each C++ file 58 | for file in $found_files; do 59 | for ignore_file in $ignore_files; do 60 | ignore_directory=$(dirname "$ignore_file") 61 | # if directory of ignore_file is beginning of file 62 | if [[ "$file" == "$ignore_directory"* ]]; then 63 | echo "ignoring file: ${file}" 64 | found_files="${found_files//${file}/}" 65 | break 1 66 | fi 67 | done 68 | done 69 | 70 | # remove empty lines 71 | found_files=$(echo "$found_files" | sed '/^\s*$/d') 72 | 73 | echo "found cpp files: ${found_files}" 74 | 75 | # do not quote to keep this as a single line 76 | echo found_files=${found_files} >> $GITHUB_OUTPUT 77 | 78 | - name: C++ - Clang format lint 79 | if: always() && steps.cpp_files.outputs.found_files 80 | uses: DoozyX/clang-format-lint-action@v0.18 81 | with: 82 | source: ${{ steps.cpp_files.outputs.found_files }} 83 | extensions: 'c,cpp,h,hpp,m,mm' 84 | style: file 85 | inplace: false 86 | 87 | - name: CMake - find files 88 | id: cmake_files 89 | if: always() 90 | run: | 91 | # find files 92 | found_files=$(find . -type f -iname "CMakeLists.txt" -o -iname "*.cmake") 93 | ignore_files=$(find . -type f -iname ".cmake-lint-ignore") 94 | 95 | # Loop through each C++ file 96 | for file in $found_files; do 97 | for ignore_file in $ignore_files; do 98 | ignore_directory=$(dirname "$ignore_file") 99 | # if directory of ignore_file is beginning of file 100 | if [[ "$file" == "$ignore_directory"* ]]; then 101 | echo "ignoring file: ${file}" 102 | found_files="${found_files//${file}/}" 103 | break 1 104 | fi 105 | done 106 | done 107 | 108 | # remove empty lines 109 | found_files=$(echo "$found_files" | sed '/^\s*$/d') 110 | 111 | echo "found cmake files: ${found_files}" 112 | 113 | # do not quote to keep this as a single line 114 | echo found_files=${found_files} >> $GITHUB_OUTPUT 115 | 116 | - name: CMake - cmake-lint 117 | if: always() && steps.cmake_files.outputs.found_files 118 | run: | 119 | cmake-lint --line-width 120 --tab-size 4 ${{ steps.cmake_files.outputs.found_files }} 120 | 121 | - name: Docker - find files 122 | id: dokcer_files 123 | if: always() 124 | run: | 125 | found_files=$(find . -type f -iname "Dockerfile" -o -iname "*.dockerfile") 126 | 127 | echo "found_files: ${found_files}" 128 | 129 | # do not quote to keep this as a single line 130 | echo found_files=${found_files} >> $GITHUB_OUTPUT 131 | 132 | - name: Docker - hadolint 133 | if: always() && steps.dokcer_files.outputs.found_files 134 | run: | 135 | docker pull hadolint/hadolint 136 | 137 | # create hadolint config file 138 | cat < .hadolint.yaml 139 | --- 140 | ignored: 141 | - DL3008 142 | - DL3013 143 | - DL3016 144 | - DL3018 145 | - DL3028 146 | - DL3059 147 | EOF 148 | 149 | failed=0 150 | failed_files="" 151 | 152 | for file in ${{ steps.dokcer_files.outputs.found_files }}; do 153 | echo "::group::${file}" 154 | docker run --rm -i \ 155 | -e "NO_COLOR=0" \ 156 | -e "HADOLINT_VERBOSE=1" \ 157 | -v $(pwd)/.hadolint.yaml:/.config/hadolint.yaml \ 158 | hadolint/hadolint < $file || { 159 | failed=1 160 | failed_files="$failed_files $file" 161 | } 162 | echo "::endgroup::" 163 | done 164 | 165 | if [ $failed -ne 0 ]; then 166 | echo "::error:: hadolint failed for the following files: $failed_files" 167 | exit 1 168 | fi 169 | 170 | - name: Python - flake8 171 | if: always() 172 | run: | 173 | python -m flake8 \ 174 | --color=always \ 175 | --verbose 176 | 177 | - name: Python - nbqa flake8 178 | if: always() 179 | run: | 180 | python -m nbqa flake8 \ 181 | --color=always \ 182 | --verbose \ 183 | . 184 | 185 | - name: Python - nb-clean 186 | if: always() 187 | run: | 188 | output=$(find . -name '*.ipynb' -exec nb-clean check {} \;) 189 | 190 | # fail if there are any issues 191 | if [ -n "$output" ]; then 192 | echo "$output" 193 | exit 1 194 | fi 195 | 196 | - name: Rust - find Cargo.toml 197 | id: run_cargo 198 | if: always() 199 | run: | 200 | # check if Cargo.toml exists 201 | if [ -f "Cargo.toml" ]; then 202 | echo "found_cargo=true" >> $GITHUB_OUTPUT 203 | else 204 | echo "found_cargo=false" >> $GITHUB_OUTPUT 205 | fi 206 | 207 | - name: Rust - setup toolchain 208 | if: always() && steps.run_cargo.outputs.found_cargo == 'true' 209 | uses: dtolnay/rust-toolchain@stable 210 | with: 211 | components: rustfmt 212 | 213 | - name: Rust - cargo fmt 214 | if: always() && steps.run_cargo.outputs.found_cargo == 'true' 215 | run: | 216 | cargo fmt -- --check 217 | 218 | - name: YAML - find files 219 | id: yaml_files 220 | if: always() 221 | run: | 222 | # space separated list of files 223 | FILES=.clang-format 224 | 225 | # empty placeholder 226 | found_files="" 227 | 228 | for FILE in ${FILES}; do 229 | if [ -f "$FILE" ] 230 | then 231 | found_files="$found_files $FILE" 232 | fi 233 | done 234 | 235 | echo "found_files=${found_files}" >> $GITHUB_OUTPUT 236 | 237 | - name: YAML - yamllint 238 | id: yamllint 239 | if: always() 240 | uses: ibiqlik/action-yamllint@v3 241 | with: 242 | # https://yamllint.readthedocs.io/en/stable/configuration.html#default-configuration 243 | config_data: | 244 | extends: default 245 | rules: 246 | comments: 247 | level: error 248 | document-start: 249 | level: error 250 | line-length: 251 | max: 120 252 | new-line-at-end-of-file: 253 | level: error 254 | new-lines: 255 | type: unix 256 | truthy: 257 | # GitHub uses "on" for workflow event triggers 258 | # .clang-format file has options of "Yes" "No" that will be caught by this, so changed to "warning" 259 | allowed-values: ['true', 'false', 'on'] 260 | check-keys: true 261 | level: warning 262 | file_or_dir: . ${{ steps.yaml_files.outputs.found_files }} 263 | 264 | - name: YAML - log 265 | if: always() && steps.yamllint.outcome == 'failure' 266 | run: | 267 | cat "${{ steps.yamllint.outputs.logfile }}" >> $GITHUB_STEP_SUMMARY 268 | -------------------------------------------------------------------------------- /.github/workflows/issues.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This action is centrally managed in https://github.com//.github/ 3 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in 4 | # the above-mentioned repo. 5 | 6 | # Label and un-label actions using `../label-actions.yml`. 7 | 8 | name: Issues 9 | 10 | on: 11 | issues: 12 | types: [labeled, unlabeled] 13 | discussion: 14 | types: [labeled, unlabeled] 15 | 16 | jobs: 17 | label: 18 | name: Label Actions 19 | if: startsWith(github.repository, 'LizardByte/') 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Label Actions 23 | uses: dessant/label-actions@v4 24 | with: 25 | github-token: ${{ secrets.GH_BOT_TOKEN }} 26 | -------------------------------------------------------------------------------- /.github/workflows/release-notifier-moonlight.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release Notifications (Moonlight) 3 | 4 | on: 5 | release: 6 | types: 7 | - released # this triggers when a release is published, but does not include prereleases or drafts 8 | 9 | jobs: 10 | discord: 11 | if: >- 12 | startsWith(github.repository, 'LizardByte/') && 13 | !github.event.release.prerelease && 14 | !github.event.release.draft 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: discord 18 | uses: sarisia/actions-status-discord@v1 # https://github.com/sarisia/actions-status-discord 19 | with: 20 | webhook: ${{ secrets.DISCORD_RELEASE_WEBHOOK_MOONLIGHT }} 21 | nodetail: true 22 | nofail: false 23 | username: ${{ secrets.DISCORD_USERNAME }} 24 | avatar_url: ${{ secrets.ORG_LOGO_URL }} 25 | title: ${{ github.event.repository.name }} ${{ github.ref_name }} Released 26 | description: ${{ github.event.release.body }} 27 | color: 0xFF4500 28 | -------------------------------------------------------------------------------- /.github/workflows/release-notifier.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This action is centrally managed in https://github.com//.github/ 3 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in 4 | # the above-mentioned repo. 5 | 6 | # Send release notification to various platforms. 7 | 8 | name: Release Notifications 9 | 10 | on: 11 | release: 12 | types: 13 | - released # this triggers when a release is published, but does not include pre-releases or drafts 14 | 15 | jobs: 16 | simplified_changelog: 17 | if: >- 18 | startsWith(github.repository, 'LizardByte/') && 19 | !github.event.release.prerelease && 20 | !github.event.release.draft 21 | outputs: 22 | SIMPLIFIED_BODY: ${{ steps.output.outputs.SIMPLIFIED_BODY }} 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: remove contributors section 26 | env: 27 | RELEASE_BODY: ${{ github.event.release.body }} 28 | id: output 29 | run: | 30 | echo "${RELEASE_BODY}" > ./release_body.md 31 | modified_body=$(sed '/^---/,$d' ./release_body.md) 32 | echo "modified_body: ${modified_body}" 33 | 34 | # use a heredoc to ensure the output is multiline 35 | echo "SIMPLIFIED_BODY<> $GITHUB_OUTPUT 36 | echo "${modified_body}" >> $GITHUB_OUTPUT 37 | echo "EOF" >> $GITHUB_OUTPUT 38 | 39 | discord: 40 | if: >- 41 | startsWith(github.repository, 'LizardByte/') && 42 | !github.event.release.prerelease && 43 | !github.event.release.draft 44 | needs: simplified_changelog 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: discord 48 | uses: sarisia/actions-status-discord@v1 49 | with: 50 | avatar_url: ${{ secrets.ORG_LOGO_URL }} 51 | color: 0x00ff00 52 | description: ${{ needs.simplified_changelog.outputs.SIMPLIFIED_BODY }} 53 | nodetail: true 54 | nofail: false 55 | title: ${{ github.event.repository.name }} ${{ github.ref_name }} Released 56 | url: ${{ github.event.release.html_url }} 57 | username: ${{ secrets.DISCORD_USERNAME }} 58 | webhook: ${{ secrets.DISCORD_RELEASE_WEBHOOK }} 59 | 60 | facebook_group: 61 | if: >- 62 | startsWith(github.repository, 'LizardByte/') && 63 | !github.event.release.prerelease && 64 | !github.event.release.draft 65 | runs-on: ubuntu-latest 66 | steps: 67 | - name: facebook-post-action 68 | uses: ReenigneArcher/facebook-post-action@v1 69 | with: 70 | page_id: ${{ secrets.FACEBOOK_GROUP_ID }} 71 | access_token: ${{ secrets.FACEBOOK_ACCESS_TOKEN }} 72 | message: | 73 | ${{ github.event.repository.name }} ${{ github.ref_name }} Released 74 | url: ${{ github.event.release.html_url }} 75 | 76 | facebook_page: 77 | if: >- 78 | startsWith(github.repository, 'LizardByte/') && 79 | !github.event.release.prerelease && 80 | !github.event.release.draft 81 | runs-on: ubuntu-latest 82 | steps: 83 | - name: facebook-post-action 84 | uses: ReenigneArcher/facebook-post-action@v1 85 | with: 86 | page_id: ${{ secrets.FACEBOOK_PAGE_ID }} 87 | access_token: ${{ secrets.FACEBOOK_ACCESS_TOKEN }} 88 | message: | 89 | ${{ github.event.repository.name }} ${{ github.ref_name }} Released 90 | url: ${{ github.event.release.html_url }} 91 | 92 | reddit: 93 | if: >- 94 | startsWith(github.repository, 'LizardByte/') && 95 | !github.event.release.prerelease && 96 | !github.event.release.draft 97 | needs: simplified_changelog 98 | runs-on: ubuntu-latest 99 | steps: 100 | - name: reddit 101 | uses: bluwy/release-for-reddit-action@v2 102 | with: 103 | username: ${{ secrets.REDDIT_USERNAME }} 104 | password: ${{ secrets.REDDIT_PASSWORD }} 105 | app-id: ${{ secrets.REDDIT_CLIENT_ID }} 106 | app-secret: ${{ secrets.REDDIT_CLIENT_SECRET }} 107 | subreddit: ${{ secrets.REDDIT_SUBREDDIT }} 108 | title: ${{ github.event.repository.name }} ${{ github.ref_name }} Released 109 | url: ${{ github.event.release.html_url }} 110 | flair-id: ${{ secrets.REDDIT_FLAIR_ID }} # https://www.reddit.com/r/>/api/link_flair.json 111 | comment: ${{ needs.simplified_changelog.outputs.SIMPLIFIED_BODY }} 112 | 113 | x: 114 | if: >- 115 | startsWith(github.repository, 'LizardByte/') && 116 | !github.event.release.prerelease && 117 | !github.event.release.draft 118 | runs-on: ubuntu-latest 119 | steps: 120 | - name: x 121 | uses: nearform-actions/github-action-notify-twitter@v1 122 | with: 123 | message: ${{ github.event.release.html_url }} 124 | twitter-app-key: ${{ secrets.X_APP_KEY }} 125 | twitter-app-secret: ${{ secrets.X_APP_SECRET }} 126 | twitter-access-token: ${{ secrets.X_ACCESS_TOKEN }} 127 | twitter-access-token-secret: ${{ secrets.X_ACCESS_TOKEN_SECRET }} 128 | -------------------------------------------------------------------------------- /.github/workflows/update-changelog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This action is centrally managed in https://github.com//.github/ 3 | # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in 4 | # the above-mentioned repo. 5 | 6 | # Update changelog on release events. 7 | 8 | name: Update changelog 9 | 10 | on: 11 | release: 12 | types: [created, edited, deleted] 13 | workflow_dispatch: 14 | 15 | concurrency: 16 | group: "${{ github.workflow }}" 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | update-changelog: 21 | if: >- 22 | github.event_name == 'workflow_dispatch' || 23 | (!github.event.release.prerelease && !github.event.release.draft) 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Update Changelog 27 | uses: LizardByte/update-changelog-action@v2024.919.152649 28 | with: 29 | changelogBranch: changelog 30 | changelogFile: CHANGELOG.md 31 | token: ${{ secrets.GH_BOT_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | .idea/ 153 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Overview 2 | ======== 3 | 4 | About 5 | ----- 6 | 7 | .. attention:: This project is archived since NVIDIA GeForce Experience is now end-of-life. GSMS is not planning to 8 | support the new NVIDIA app. 9 | 10 | GSMS is a migration tool that allows users to import their custom apps and games from Nvidia GameStream to 11 | `Sunshine `_. Note that Nvidia GameStream support has ended in February 2023, 12 | so users who relied on GameStream may want to consider migrating their library to Sunshine. This program updates the 13 | `apps.json` file for Sunshine and copies the corresponding box art images to a specified directory. It reads shortcut 14 | files (.lnk) from the default GameStream installation location. Additionally it can add games that were automatically 15 | detected by Nvidia GameStream. The found games and applications are added to Sunshine as new apps. If an app with 16 | the same name already exists in Sunshine, it will be skipped. 17 | 18 | This program is intended for users who want to migrate their GameStream library to Sunshine and have their custom apps 19 | and games available in Sunshine. It can save users the time and effort of manually adding each app to Sunshine and 20 | finding and copying the corresponding box art images. The GameStream library and box-art images will not be modified. 21 | 22 | To use this script, users will need to have both GameStream and Sunshine installed on their system and have a basic 23 | understanding of using the command line. The script can be run with a variety of options to customize its behavior, 24 | such as specifying the `apps.json` file to update, the directory where to copy box art images, and whether to preview 25 | the changes without actually updating the `apps.json` file. 26 | 27 | As an alternative option to migrating custom GameStream apps, you may also migrate any directory containing 28 | ``.lnk`` (shortcut) files. Below is the preferred directory structure of a custom directory. Cover images 29 | (``box-art.png``) is optional. 30 | 31 | .. code-block:: 32 | 33 | . 34 | ├── A Game.lnk 35 | ├── Another Game.lnk 36 | └── StreamingAssets 37 | ├── A Game 38 | │ └── box-art.png 39 | └── Another Game 40 | └── box-art.png 41 | 42 | Usage 43 | ----- 44 | #. Download the latest version from our `latest release `_. 45 | #. Double click the program to run with default arguments OR... 46 | #. Open command prompt/terminal and execute the following command to see available options. 47 | 48 | .. Tip:: This code requires no modification if you download the program to your Downloads directory, otherwise 49 | be sure to change the directory accordingly. 50 | 51 | .. code-block:: batch 52 | 53 | cd /d "%userprofile%/Downloads" 54 | gsms.exe --help 55 | 56 | Command Line 57 | ^^^^^^^^^^^^ 58 | 59 | To run the script, use the following command: 60 | 61 | .. code-block:: batch 62 | 63 | gsms.exe [OPTIONS] 64 | 65 | OPTIONS 66 | 67 | ``--apps, -a`` 68 | Specify the sunshine ``apps.json`` file to update, otherwise we will attempt to use the ``apps.json`` file from the 69 | default Sunshine installation location. 70 | 71 | ``--image_path, -i`` 72 | Specify the full directory where to copy box art to. If not specified, box art will be copied to 73 | ``%USERPROFILE%/Pictures/Sunshine`` 74 | 75 | ``--shortcut_dir, -s`` 76 | Specify a custom shortcut directory. If not specified, ``%localappdata%/NVIDIA Corporation/Shield Apps`` 77 | will be used. 78 | 79 | ``--dry_run, -d`` 80 | If set, the `apps.json` file will not be overwritten and box-art images won't be copied. Use this flag to preview 81 | the changes that would be made without committing them. 82 | 83 | ``--no_sleep`` 84 | If set, the script will not pause for 10 seconds at the end of the import. 85 | 86 | ``--nv_add_autodetect, -n`` 87 | If set, the script will add all streamable apps from Nvidia GameStream's automatically detected applications. 88 | 89 | Examples 90 | ^^^^^^^^ 91 | 92 | To migrate all GameStream apps to Sunshine and copy box art images to the default directory: 93 | 94 | .. code-block:: batch 95 | 96 | gsms.exe 97 | 98 | To migrate all GameStream apps to Sunshine and copy box art images to a custom directory: 99 | 100 | .. code-block:: batch 101 | 102 | gsms.exe --image_path C:\\Users\MyUser\\Photos\\Sunshine 103 | 104 | To preview the changes that would be made without actually updating the `apps.json` file: 105 | 106 | .. code-block:: batch 107 | 108 | gsms.exe --dry_run 109 | 110 | Integrations 111 | ------------ 112 | 113 | .. image:: https://img.shields.io/github/actions/workflow/status/lizardbyte/gsms/CI.yml?branch=master&label=CI%20build&logo=github&style=for-the-badge 114 | :alt: GitHub Workflow Status (CI) 115 | :target: https://github.com/LizardByte/GSMS/actions/workflows/CI.yml?query=branch%3Amaster 116 | 117 | Support 118 | ------- 119 | 120 | Our support methods are listed in our 121 | `LizardByte Docs `_. 122 | 123 | Downloads 124 | --------- 125 | 126 | .. image:: https://img.shields.io/github/downloads/lizardbyte/gsms/total?style=for-the-badge&logo=github 127 | :alt: GitHub Releases 128 | :target: https://github.com/LizardByte/GSMS/releases/latest 129 | 130 | Stats 131 | ----- 132 | .. image:: https://img.shields.io/github/stars/lizardbyte/gsms?logo=github&style=for-the-badge 133 | :alt: GitHub stars 134 | :target: https://github.com/LizardByte/GSMS 135 | 136 | .. image:: https://img.shields.io/codecov/c/gh/LizardByte/GSMS?token=IC678AQFBI&style=for-the-badge&logo=codecov&label=codecov 137 | :alt: Codecov 138 | :target: https://codecov.io/gh/LizardByte/GSMS 139 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | --- 2 | codecov: 3 | branch: master 4 | 5 | coverage: 6 | status: 7 | project: 8 | default: 9 | target: auto 10 | threshold: 10% 11 | 12 | comment: 13 | layout: "diff, flags, files" 14 | behavior: default 15 | require_changes: false # if true: only post the comment if coverage changes 16 | -------------------------------------------------------------------------------- /gsms/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LizardByte/GSMS/671c37808b51d86b12593686a6d24650ae7390b5/gsms/__init__.py -------------------------------------------------------------------------------- /gsms/gsms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | gsms.py 4 | 5 | A migration tool for Nvidia GameStream to Sunshine. 6 | 7 | This script updates the `apps.json` file for Sunshine and copies box art images to a specified directory. It reads 8 | shortcut files (.lnk) from a specified directory and adds them to Sunshine as new apps. If an 9 | app with the same name already exists in Sunshine, it will be skipped. 10 | 11 | Usage 12 | ----- 13 | 14 | To run the script, use the following command: 15 | 16 | $ python gsms.py [OPTIONS] 17 | 18 | OPTIONS 19 | 20 | --apps, -a 21 | Specify the sunshine `apps.json` file to update, otherwise we will attempt to use the `apps.json` file from the 22 | default Sunshine installation location. 23 | 24 | --image_path, -i 25 | Specify the full directory where to copy box art to. If not specified, box art will be copied to 26 | `%USERPROFILE%/Pictures/Sunshine` 27 | 28 | --shortcut_dir, -s 29 | Specify a custom shortcut directory. If not specified, `%localappdata%/NVIDIA Corporation/Shield Apps` will be used. 30 | 31 | --dry_run, -d 32 | If set, the `apps.json` file will not be overwritten and box-art images won't be copied. Use this flag to preview 33 | the changes that would be made without committing them. 34 | 35 | --no_sleep 36 | If set, the script will not pause for 10 seconds at the end of the import. 37 | 38 | --nv_add_autodetect, -n 39 | If set, the script will add all streamable apps from Nvidia GameStream's automatically detected applications. 40 | 41 | Examples 42 | -------- 43 | 44 | To migrate all GameStream apps to Sunshine and copy box art images to the default directory: 45 | 46 | $ python gsms.py 47 | 48 | To migrate all GameStream apps to Sunshine and copy box art images to a custom directory: 49 | 50 | $ python gsms.py --image_path /path/to/custom/dir 51 | 52 | To preview the changes that would be made without actually updating the `apps.json` file: 53 | 54 | $ python gsms.py --dry_run 55 | """ 56 | 57 | # standard imports 58 | import argparse 59 | import ctypes 60 | from ctypes import wintypes 61 | import json 62 | import os 63 | import re 64 | import shutil 65 | import time 66 | from uuid import UUID 67 | import xml.etree.ElementTree 68 | 69 | # lib imports 70 | import pylnk3 71 | 72 | 73 | # Code from here and simplified to work with GSMS 74 | # https://gist.github.com/mkropat/7550097 75 | class WindowsGUIDWrapper(ctypes.Structure): 76 | """ 77 | Create a GUID compliant object for use in Windows libraries. 78 | 79 | Parameters 80 | ---------- 81 | unique_id : UUID 82 | The UUID to parse into a GUID object. 83 | 84 | Examples 85 | -------- 86 | >>> WindowsGUIDWrapper(unique_id=UUID("{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}")) 87 | ... 88 | """ 89 | _fields_ = [ 90 | ("Data1", wintypes.DWORD), 91 | ("Data2", wintypes.WORD), 92 | ("Data3", wintypes.WORD), 93 | ("Data4", wintypes.BYTE * 8) 94 | ] 95 | 96 | def __init__(self, unique_id: UUID) -> None: 97 | ctypes.Structure.__init__(self) 98 | self.Data1, self.Data2, self.Data3, self.Data4[0], self.Data4[1], rest = unique_id.fields 99 | for i in range(2, 8): 100 | self.Data4[i] = rest >> (8 - i - 1)*8 & 0xff 101 | 102 | 103 | # https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cotaskmemfree 104 | # Define function to free pointer memory 105 | _CoTaskMemFree = ctypes.windll.ole32.CoTaskMemFree 106 | # Add response type to function call 107 | _CoTaskMemFree.restype = None 108 | # Add argument types to C function call 109 | _CoTaskMemFree.argtypes = [ctypes.c_void_p] 110 | 111 | # https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath 112 | # Define function call for resolving the GUID path 113 | _SHGetKnownFolderPath = ctypes.windll.shell32.SHGetKnownFolderPath 114 | # Add argument types to the C function call 115 | _SHGetKnownFolderPath.argtypes = [ 116 | ctypes.POINTER(WindowsGUIDWrapper), wintypes.DWORD, wintypes.HANDLE, ctypes.POINTER(ctypes.c_wchar_p) 117 | ] 118 | 119 | 120 | def get_win_path(folder_id: str) -> str: 121 | """ 122 | Resolve Windows UUID folders into their absolute path. 123 | 124 | Parameters 125 | ---------- 126 | folder_id : str 127 | The folder UUID to convert into the absolute path. 128 | 129 | Returns 130 | ------- 131 | str 132 | Resolved Windows path as string. 133 | 134 | Raises 135 | ------ 136 | NotADirectoryError 137 | When a UUID can not be resolved to a path as it is not a Windows UUID path this will be raised. 138 | 139 | Examples 140 | -------- 141 | >>> get_win_path(folder_id="{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}") 142 | C:\\Users\\...\\Desktop 143 | >>> get_win_path(folder_id="B4BFCC3A-DB2C-424C-B029-7FE99A87C641") 144 | C:\\Users\\...\\Desktop 145 | """ 146 | # Get actual Windows folder id (fid) 147 | fid = WindowsGUIDWrapper(unique_id=UUID(folder_id)) 148 | 149 | # Prepare pointer to store the path 150 | path_pointer = ctypes.c_wchar_p() 151 | 152 | # Execute function (which stores the path in our pointer) and check return value for success (0 = OK) 153 | if _SHGetKnownFolderPath(ctypes.byref(fid), 0, wintypes.HANDLE(0), ctypes.byref(path_pointer)) != 0: 154 | raise NotADirectoryError(f"The specified UUID '{folder_id}' could not be resolved to a path") 155 | 156 | # Get value from pointer 157 | path = path_pointer.value 158 | 159 | # Free memory used by pointer 160 | _CoTaskMemFree(path_pointer) 161 | 162 | return path 163 | 164 | 165 | def copy_image(src_image: str, dst_image: str) -> None: 166 | """ 167 | Copy an image from `src_image` to `dst_image` if the destination image does not exist. 168 | 169 | Parameters 170 | ---------- 171 | src_image : str 172 | Source path of the image. 173 | dst_image : str 174 | Destination path of the image. 175 | 176 | Examples 177 | -------- 178 | >>> copy_image(src_image="C:\\Image1.png", dst_image="D:\\Image1.png") 179 | """ 180 | # if src_image exists and dst_image does not exist 181 | if os.path.isfile(src_image) and not os.path.isfile(dst_image): 182 | shutil.copy2(src=src_image, dst=dst_image) # copy2 preserves metadata 183 | print(f'Copied box-art image to: {dst_image}') 184 | else: 185 | print(f'No box-art image found at: {src_image}') 186 | 187 | 188 | def has_app(sunshine_apps: dict, name: str) -> bool: 189 | """ 190 | Checks if a given app name is in the sunshine_apps object. 191 | 192 | Parameters 193 | ---------- 194 | sunshine_apps : dict 195 | Dictionary object of the sunshine `apps.json`. 196 | name : str 197 | Name to check for. 198 | 199 | Returns 200 | ------- 201 | bool 202 | True if the app is in the sunshine_apps object, otherwise False. 203 | 204 | Examples 205 | -------- 206 | >>> has_app(sunshine_apps={}, name="Game Name") 207 | False 208 | """ 209 | app_exists = False 210 | 211 | for existing_app in sunshine_apps['apps']: 212 | if name == existing_app['name']: 213 | app_exists = True 214 | print(f'{name} app already exist in Sunshine apps.json, skipping.') 215 | break 216 | 217 | return app_exists 218 | 219 | 220 | def add_game(sunshine_apps: dict, name: str, logfile: str, cmd: str, working_dir: str, image_path: str) -> None: 221 | """ 222 | Add an app to the sunshine_apps object. 223 | 224 | Parameters 225 | ---------- 226 | sunshine_apps : dict 227 | Dictionary object of the sunshine `apps.json`. 228 | name : str 229 | Name of the app. 230 | logfile : str 231 | Logfile name for the app. 232 | cmd : str 233 | Commandline to start the app. 234 | working_dir : str 235 | Working directory for the app. 236 | image_path : str 237 | Path to an image file to display as box-art. 238 | 239 | Examples 240 | -------- 241 | >>> add_game(sunshine_apps={}, name="Game Name", logfile="game.log", cmd="game.exe", 242 | >>> working_dir="C:\\game_dir", image_path="C:\\game_dir\\image.png") 243 | """ 244 | working_dir = known_path_to_absolute(path=working_dir) 245 | cmd = known_path_to_absolute(path=cmd) 246 | 247 | # remove final path separator but only if it exists 248 | while working_dir.endswith(os.sep): 249 | working_dir = working_dir[:-1] 250 | 251 | # remove preceding separator on the command if it exists 252 | while cmd.startswith(os.sep): 253 | cmd = cmd[1:] 254 | 255 | # we don't need ot keep quotes around the path or working directory 256 | working_dir = working_dir.replace('"', "") 257 | cmd = cmd.replace('"', '') 258 | 259 | detached = False 260 | 261 | # cmd begins with "start", this must be a detached command 262 | if cmd.lower().startswith("start"): 263 | detached = True 264 | cmd = cmd[5:].strip() 265 | 266 | # command is a URI, this must be a detached command 267 | if '://' in cmd: 268 | detached = True 269 | 270 | # if the URI uses the steam protocol we prepend the steam executable 271 | if 'steam://' in cmd: 272 | cmd = f"steam {cmd}" 273 | # if it's not a URI we check if the command includes the working_directory 274 | elif not cmd.startswith(working_dir): 275 | cmd = os.path.join(working_dir, cmd) 276 | 277 | data = { 278 | 'name': name, 279 | 'output': logfile, 280 | 'working-dir': working_dir, 281 | 'image-path': image_path 282 | } 283 | 284 | # we add the command to the appropriate field 285 | if detached: 286 | data['detached'] = [cmd] 287 | else: 288 | data['cmd'] = cmd 289 | 290 | sunshine_apps['apps'].append(data) 291 | 292 | 293 | def known_path_to_absolute(path: str) -> str: 294 | """ 295 | Convert paths containing Windows known path UUID to absolute path. 296 | 297 | Parameters 298 | ---------- 299 | path : str 300 | Path that may contain Windows UUID. 301 | 302 | Returns 303 | ------- 304 | str 305 | The absolute path. 306 | 307 | Examples 308 | -------- 309 | >>> known_path_to_absolute(path='::{62AB5D82-FDC1-4DC3-A9DD-070D1D495D97}') 310 | C:\\ProgramData 311 | """ 312 | # prepare regex to get folder UUIDs which can only be at the start exactly 1 time with 2 preceding colons 313 | regex = re.compile( 314 | r"^::(\{[\da-fA-F]{8}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{12}})" 315 | ) 316 | 317 | path_result = regex.findall(path) 318 | 319 | if len(path_result) == 1: 320 | path = path.replace( 321 | f"::{path_result[0]}", 322 | get_win_path(folder_id=path_result[0]) 323 | ) 324 | 325 | return path 326 | 327 | 328 | def stopwatch(message: str, sec: int) -> None: 329 | """ 330 | Countdown function that updates the console with a message and the remaining time in minutes and seconds. 331 | 332 | Parameters 333 | ---------- 334 | message : str 335 | Prefix message to display before the countdown timer. 336 | sec : int 337 | Time, in seconds, to countdown from. 338 | 339 | Examples 340 | -------- 341 | >>> stopwatch(message="Exiting in: ", sec=10) 342 | Exiting in: 00:10 343 | Exiting in: 00:09 344 | ... 345 | Exiting in: 00:00 346 | """ 347 | while sec: 348 | minute, second = divmod(sec, 60) 349 | time_format = '{}{:02d}:{:02d}'.format(message, minute, second) 350 | print(time_format, end='\r') 351 | time.sleep(1) 352 | sec -= 1 353 | 354 | 355 | def main() -> None: 356 | """ 357 | Main application entrypoint. Migrates Nvidia Gamestream apps to Sunshine by updating the `apps.json` file and 358 | copying box art images to a specified directory. 359 | 360 | Raises 361 | ------ 362 | FileNotFoundError 363 | When the ``apps.json`` file specified or the default ``apps.json`` file is not found. 364 | 365 | Examples 366 | -------- 367 | >>> main() 368 | ... 369 | """ 370 | # Set up and gather command line arguments 371 | parser = argparse.ArgumentParser(description='GSMS is a migration tool for Nvidia GameStream to Sunshine.') 372 | 373 | parser.add_argument('--apps', '-a', 374 | help='Specify the sunshine `apps.json` file to update, otherwise we will attempt to use the ' 375 | '`apps.json` file from the default Sunshine installation location.', 376 | default=os.path.join(os.environ['programfiles'], 'Sunshine', 'config', 'apps.json') 377 | ) 378 | parser.add_argument('--image_path', '-i', 379 | help='Specify the full directory where to copy box art to. If not specified, box art will be ' 380 | 'copied to `%%USERPROFILE%%/Pictures/Sunshine`', 381 | default=os.path.join(os.environ['userprofile'], 'Pictures', 'Sunshine') 382 | ) 383 | parser.add_argument('--shortcut_dir', '-s', 384 | help='Specify a custom shortcut directory. If not specified,' 385 | '`%%localappdata%%/NVIDIA Corporation/Shield Apps` will be used.', 386 | default=os.path.join(os.environ['localappdata'], 'NVIDIA Corporation', 'Shield Apps') 387 | ) 388 | parser.add_argument('--dry_run', '-d', action='store_true', 389 | help='If set, the `apps.json` file will not be overwritten and box-art images won\'t be copied.' 390 | 'Use this flag to preview the changes that would be made without committing them.') 391 | parser.add_argument('--no_sleep', action='store_true', 392 | help='If set, the script will not pause for 10 seconds at the end of the import.') 393 | parser.add_argument('--nv_add_autodetect', '-n', action='store_true', 394 | help='If set, GSMS will import the automatically detected apps from Nvidia GameStream.') 395 | 396 | args = parser.parse_args() 397 | 398 | # create the image destination if it doesn't exist 399 | os.makedirs(name=args.image_path, exist_ok=True) 400 | 401 | # create some helper path variables for later usage 402 | nvidia_base_dir = os.path.join(os.environ['localappdata'], "NVIDIA", "NvBackend") 403 | # Path for the main application xml Nvidia GFE uses 404 | nvidia_autodetect_dir = os.path.join(nvidia_base_dir, "journalBS.main.xml") 405 | # Base folder for the box-art 406 | nvidia_images_base_dir = os.path.join(nvidia_base_dir, "VisualOPSData") 407 | 408 | count = 0 409 | if os.path.isfile(args.apps): 410 | # file exists 411 | with open(file=args.apps, mode="r") as f: 412 | sunshine_apps = json.load(f) 413 | print('Found apps.json file.') 414 | print(json.dumps(obj=sunshine_apps, indent=4)) 415 | print('----') 416 | 417 | gs_apps = os.listdir(args.shortcut_dir) 418 | 419 | for gs_app in gs_apps: 420 | if gs_app.lower().endswith('.lnk'): 421 | name = gs_app.rsplit('.', 1)[0] # split the lnk name by the extension separator 422 | 423 | if has_app(sunshine_apps=sunshine_apps, name=name): 424 | continue 425 | 426 | count += 1 427 | 428 | shortcut = pylnk3.parse(lnk=os.path.join(args.shortcut_dir, gs_app)) 429 | shortcut.work_dir = "" if shortcut.work_dir is None else shortcut.work_dir 430 | print(f'Found GameStream app: {name}') 431 | print(f'working-dir: {shortcut.work_dir}') 432 | print(f'path: {shortcut.path}') 433 | 434 | # GFE converts jpg to png, so no reason to handle anything except PNG files 435 | src_image = os.path.join(args.shortcut_dir, 'StreamingAssets', name, 'box-art.png') 436 | dst_image = os.path.join(args.image_path, f'{name}.png') 437 | 438 | if not args.dry_run: 439 | copy_image(src_image=src_image, dst_image=dst_image) 440 | 441 | add_game( 442 | sunshine_apps=sunshine_apps, 443 | name=name, 444 | logfile=f"{name.lower().replace(' ', '_')}.log", 445 | cmd=shortcut.path, 446 | working_dir=shortcut.work_dir, 447 | image_path=dst_image 448 | ) 449 | 450 | if args.nv_add_autodetect: 451 | # Use ElementTree lib to build XML tree 452 | tree = xml.etree.ElementTree.parse(nvidia_autodetect_dir) 453 | # Get root so we can loop through children 454 | root = tree.getroot() 455 | applications_root = root.find("Application") 456 | 457 | # Prepare JSON object to fetch version numbers for use in getting the box-art image 458 | with open(file=os.path.join(nvidia_images_base_dir, "ApplicationData.json"), mode="r") as f: 459 | gfe_apps = json.load(f) 460 | 461 | # Loop through all applications in the 'Application' parent element 462 | for application in applications_root: 463 | 464 | name = application.find("DisplayName").text 465 | 466 | # We skip the GFE GS steam application 467 | if name == "Steam": 468 | continue 469 | 470 | # If we already have an App with the EXACT same name we skip it 471 | if has_app(sunshine_apps=sunshine_apps, name=name): 472 | continue 473 | 474 | cmd = application.find("StreamingCommandLine").text 475 | working_dir = application.find("InstallDirectory").text 476 | # Nvidia's short_name is a pre-shortened and filesystem safe name for the game 477 | short_name = application.find("ShortName").text 478 | 479 | if not cmd or short_name not in gfe_apps["metadata"]: 480 | continue 481 | 482 | print(f'Found GameStream app: {name}') 483 | print(f'working-dir: {working_dir}') 484 | print(f'path: {cmd}') 485 | 486 | src_image = os.path.join( 487 | nvidia_images_base_dir, 488 | short_name, 489 | gfe_apps["metadata"][short_name]["c"], 490 | f"{short_name}-box-art.png" 491 | ) 492 | dst_image = os.path.join(args.image_path, f'{short_name}.png') 493 | 494 | if not args.dry_run: 495 | copy_image(src_image=src_image, dst_image=dst_image) 496 | 497 | add_game( 498 | sunshine_apps=sunshine_apps, 499 | name=name, 500 | logfile=f"{short_name}.log", 501 | cmd=cmd, 502 | working_dir=working_dir, 503 | image_path=dst_image 504 | ) 505 | 506 | count += 1 507 | 508 | if not args.dry_run: 509 | with open(file=args.apps, mode="w") as f: 510 | json.dump(obj=sunshine_apps, indent=4, fp=f) 511 | print(json.dumps(obj=sunshine_apps, indent=4)) 512 | print('Completed importing Nvidia GameStream games.') 513 | print(f'Added {count} apps to Sunshine.') 514 | if not args.no_sleep: 515 | stopwatch(message='Exiting in: ', sec=10) 516 | else: 517 | raise FileNotFoundError('Specified apps.json does not exist. ' 518 | 'If you used the Sunshine Windows installer version, be sure to run Sunshine first ' 519 | 'and we will automatically detect it IF you use the default installation directory. ' 520 | 'Use the `--apps` arg to specify the full path of the file if you\'d like to use a ' 521 | 'custom location.') 522 | 523 | 524 | if __name__ == '__main__': 525 | main() 526 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | flake8==7.1.1 3 | pyinstaller==6.11.1 4 | pytest==8.3.3 5 | pytest-cov==6.0.0 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pylnk3==0.4.2 2 | -------------------------------------------------------------------------------- /scripts/build.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. 3 | build.py 4 | 5 | Creates spec and builds binaries for GSMS. 6 | """ 7 | # standard imports 8 | import sys 9 | 10 | # lib imports 11 | import PyInstaller.__main__ 12 | 13 | 14 | def build(): 15 | """Sets arguments for pyinstaller, creates spec, and builds binaries.""" 16 | pyinstaller_args = [ 17 | 'gsms/gsms.py', 18 | '--onefile', 19 | '--noconfirm', 20 | '--paths=./', 21 | '--icon=./sunshine.ico' 22 | ] 23 | 24 | if sys.platform.lower() == 'win32': # windows 25 | pyinstaller_args.append('--console') 26 | 27 | # fix args for windows 28 | for index, arg in enumerate(pyinstaller_args): 29 | pyinstaller_args[index] = arg.replace(':', ';') 30 | 31 | # no point in having macos/linux versions for this project 32 | 33 | PyInstaller.__main__.run(pyinstaller_args) 34 | 35 | 36 | if __name__ == '__main__': 37 | build() 38 | -------------------------------------------------------------------------------- /sunshine.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LizardByte/GSMS/671c37808b51d86b12593686a6d24650ae7390b5/sunshine.ico -------------------------------------------------------------------------------- /tests/box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LizardByte/GSMS/671c37808b51d86b12593686a6d24650ae7390b5/tests/box.png -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # lib imports 2 | import pytest 3 | 4 | 5 | @pytest.fixture 6 | def sunshine_default_apps(): 7 | apps_json = { 8 | "env": { 9 | "PATH": "$(PATH);$(ProgramFiles(x86))\\Steam" 10 | }, 11 | "apps": [ 12 | { 13 | "name": "Desktop", 14 | "image-path": "desktop.png" 15 | }, 16 | { 17 | "name": "Steam Big Picture", 18 | "detached": [ 19 | "steam steam://open/bigpicture" 20 | ], 21 | "image-path": "steam.png" 22 | } 23 | ] 24 | } 25 | 26 | return apps_json 27 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LizardByte/GSMS/671c37808b51d86b12593686a6d24650ae7390b5/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/test_gsms.py: -------------------------------------------------------------------------------- 1 | # standard imports 2 | import os 3 | import time 4 | import uuid 5 | 6 | # lib imports 7 | import pytest 8 | 9 | # local imports 10 | from gsms import gsms 11 | 12 | 13 | def test_windows_guid_wrapper(): 14 | guid = gsms.WindowsGUIDWrapper(unique_id=uuid.uuid4()) 15 | assert isinstance(guid, gsms.WindowsGUIDWrapper) 16 | 17 | 18 | def test_get_win_path(): 19 | with pytest.raises(ValueError): 20 | gsms.get_win_path(folder_id="not_a_valid_uuid") 21 | with pytest.raises(NotADirectoryError): 22 | gsms.get_win_path(folder_id=str(uuid.uuid4())) # what are the chances this will resolve to a valid path? 23 | 24 | folder_ids = [ 25 | # uuid list 26 | # more known UUIDs: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid 27 | "B4BFCC3A-DB2C-424C-B029-7FE99A87C641", # user "Desktop" folder 28 | "F42EE2D3-909F-4907-8871-4C22FC0BF756", # user "Documents" folder 29 | "FDD39AD0-238F-46AF-ADB4-6C85480369C7", # alternate user "Documents" folder 30 | "F38BF404-1D43-42F2-9305-67DE0B28FC23", # "C:\\WINDOWS" folder 31 | "374DE290-123F-4565-9164-39C4925E467B", # user "Downloads" folder 32 | "1777F761-68AD-4D8A-87BD-30B759FA33DD", # user "Favorites" folder 33 | "FD228CB7-AE11-4AE3-864C-16F3910AB8FE", # "C:\\WINDOWS\\Fonts" folder 34 | "4BD8D571-6D19-48D3-BE97-422220080E43", # user "Music" folder 35 | "18989B1D-99B5-455B-841C-AB7C74E4DDFC", # user "Videos" folder 36 | "C5ABBF53-E17F-4121-8900-86626FC2C973", # "AppData\\Roaming\\Microsoft\\Windows\\Network Shortcuts" folder 37 | "D65231B0-B2F1-4857-A4CE-A8E7C6EA7D27", # "C:\\WINDOWS\\SysWOW64" folder 38 | "A63293E8-664E-48DB-A079-DF759E0509F7", # "AppData\\Roaming\\Microsoft\\Windows\\Templates" folder 39 | "B94237E7-57AC-4347-9151-B08C6C32D1F7", # "C:\\ProgramData\\Microsoft\\Windows\\Templates" folder 40 | ] 41 | for folder_id in folder_ids: 42 | assert os.path.isdir(gsms.get_win_path(folder_id=folder_id)) 43 | assert os.path.isdir(gsms.get_win_path(folder_id='{%s}' % folder_id)) 44 | 45 | 46 | def test_copy_image(): 47 | src_image = os.path.join('tests', 'box.png') 48 | dst_image = "box_copy.png" 49 | 50 | gsms.copy_image(src_image, dst_image) 51 | assert os.path.isfile(dst_image) 52 | 53 | # compare the hash of each file 54 | with open(src_image, 'rb') as f: 55 | src_hash = hash(f.read()) 56 | with open(dst_image, 'rb') as f: 57 | dst_hash = hash(f.read()) 58 | assert src_hash == dst_hash 59 | 60 | # remove the copied image 61 | os.remove(dst_image) 62 | 63 | 64 | def test_has_app(sunshine_default_apps): 65 | assert gsms.has_app(sunshine_apps=sunshine_default_apps, name="Desktop") 66 | assert not gsms.has_app(sunshine_apps=sunshine_default_apps, name="Not a valid app") 67 | 68 | 69 | def test_add_game(sunshine_default_apps): 70 | name = "Added Game" 71 | gsms.add_game(sunshine_apps=sunshine_default_apps, 72 | name=name, 73 | logfile="added_game.log", 74 | cmd="added_game.exe", 75 | working_dir=os.path.join(os.path.curdir, 'test_directory'), 76 | image_path=os.path.join(os.path.curdir, 'image_directory') 77 | ) 78 | assert gsms.has_app(sunshine_apps=sunshine_default_apps, name=name) 79 | 80 | 81 | def test_known_path_to_absolute(): 82 | # UUID is for "C:\Windows" 83 | paths = [ 84 | r"::{F38BF404-1D43-42F2-9305-67DE0B28FC23}\System32", 85 | r"::{F38BF404-1D43-42F2-9305-67DE0B28FC23}", 86 | ] 87 | for path in paths: 88 | assert os.path.isdir(gsms.known_path_to_absolute(path=path)) 89 | 90 | 91 | def test_stopwatch(): 92 | seconds = 5 93 | 94 | # measure the time taken by the function 95 | start_time = time.time() 96 | gsms.stopwatch(message="test_stopwatch", sec=seconds) 97 | stop_time = time.time() 98 | 99 | # calculate the difference between the start and stop times 100 | elapsed_time = stop_time - start_time 101 | 102 | assert elapsed_time >= seconds, f"Elapsed time: {elapsed_time} is not greater than or equal to {seconds}" 103 | --------------------------------------------------------------------------------