[
  {
    "path": ".coderabbit.yaml",
    "content": "issue_enrichment:\n  auto_enrich:\n    enabled: false\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n"
  },
  {
    "path": ".github/labeler.yml",
    "content": "📦 aur:\n  - \"(aur)\"\n📦 snap:\n  - \"(snap)\"\n📦 brew:\n  - \"(brew)\"\n🪟 windows:\n  - \"(windows)\"\n🐧 linux:\n  - \"(linux)\"\n🍎 macos:\n  - \"(macos)\"\n🔵 extension:\n  - \"(extension|auto.*?skip|bookmark|full.*?app.*?display|keyboard.*?shortcut|shuffle|web.*?now.*?playing|popup.*?lyrics)\"\n🔴 custom app:\n  - \"(custom.*?app|lyrics.*?plus|new.*?releases|store|reddit|lyrics)\"\n🖇 duplicate:\n  - \"(duplicate)\"\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\n\non:\n  pull_request:\n    branches:\n      - \"main\"\n      - \"*/main/*/**\"\n  push:\n    branches:\n      - \"main\"\n      - \"*/main/*/**\"\n  release:\n    types: [published]\n\njobs:\n  build:\n    name: Build\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n\n      - name: Setup Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: \"go.mod\"\n\n      - name: Build\n        run: go build .\n\n      - name: Format\n        run: |\n          gofmt -s -l .\n          if [ \"$(gofmt -s -l . | wc -l)\" -gt 0 ]; then exit 1; fi\n\n  release:\n    permissions:\n      id-token: write\n      contents: write\n      attestations: write\n    name: Release\n    strategy:\n      matrix:\n        os: [\"linux\", \"darwin\", \"windows\"]\n        arch: [\"amd64\", \"arm64\", \"386\"]\n    runs-on: ubuntu-latest\n    if: startsWith(github.ref, 'refs/tags/v2')\n    needs: build\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n\n      - name: Get Tag\n        run: echo \"TAG=${GITHUB_REF#refs/*/v}\" >> $GITHUB_ENV\n\n      - name: Is Unix Platform\n        run: echo \"IS_UNIX=${{ matrix.os != 'windows' && matrix.arch != '386' && (matrix.os != 'linux' || matrix.arch != 'arm64') }}\" >> $GITHUB_ENV\n\n      - name: Is Windows Platform\n        run: echo \"IS_WIN=${{ matrix.os == 'windows' }}\" >> $GITHUB_ENV\n\n      - name: Setup Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: \"go.mod\"\n\n      - name: Build\n        if: env.IS_UNIX == 'true' || env.IS_WIN == 'true'\n        run: |\n          go build -ldflags \"-X main.version=${{ env.TAG }}\" -o \"./spicetify${{ matrix.os == 'windows' && '.exe' || '' }}\"\n          chmod +x \"./spicetify${{ matrix.os == 'windows' && '.exe' || '' }}\"\n        env:\n          GOOS: ${{ matrix.os }}\n          GOARCH: ${{ matrix.arch }}\n          CGO_ENABLED: 0\n\n      - name: Upload Artifact for Signing\n        if: env.IS_WIN == 'true'\n        id: upload-artifact-for-signing\n        uses: actions/upload-artifact@v6\n        with:\n          name: spicetify-${{ env.TAG }}-${{ matrix.os }}-${{ (matrix.arch == 'amd64' && 'x64') || (matrix.arch == 'arm64' && 'arm64') || 'x32' }}-unsigned\n          path: ./spicetify.exe\n\n      - name: Sign Windows Executable\n        if: env.IS_WIN == 'true'\n        uses: signpath/github-action-submit-signing-request@v2\n        with:\n          api-token: ${{ secrets.SIGNPATH_API_TOKEN }}\n          organization-id: ${{ secrets.SIGNPATH_ORG_ID }}\n          project-slug: \"cli\"\n          signing-policy-slug: \"release-signing\"\n          github-artifact-id: ${{ steps.upload-artifact-for-signing.outputs.artifact-id }}\n          wait-for-completion: true\n          output-artifact-directory: \"./signed\"\n\n      - name: Copy Signed Windows Executable\n        if: env.IS_WIN == 'true'\n        run: |\n          cp ./signed/spicetify.exe ./spicetify.exe\n\n      - name: Attest output\n        uses: actions/attest-build-provenance@v4\n        if: env.IS_UNIX == 'true' || env.IS_WIN == 'true'\n        with:\n          subject-path: \"./spicetify${{ matrix.os == 'windows' && '.exe' || '' }}\"\n          subject-name: \"spicetify v${{ env.TAG }} (${{ matrix.os }}, ${{ (matrix.os == 'windows' && matrix.arch == 'amd64' && 'x64') || (matrix.os == 'windows' && matrix.arch == '386' && 'x32') || matrix.arch }})\"\n\n      - name: 7z - .tar\n        if: env.IS_UNIX == 'true'\n        uses: edgarrc/action-7z@v1\n        with:\n          args: 7z a -bb0 \"spicetify-${{ env.TAG }}-${{ matrix.os }}-${{ matrix.arch }}.tar\" \"./spicetify\" \"./CustomApps\" \"./Extensions\" \"./Themes\" \"./jsHelper\" \"globals.d.ts\" \"css-map.json\"\n\n      - name: 7z - .tar.gz\n        if: env.IS_UNIX == 'true'\n        uses: edgarrc/action-7z@v1\n        with:\n          args: 7z a -bb0 -sdel -mx9 \"spicetify-${{ env.TAG }}-${{ matrix.os }}-${{ matrix.arch }}.tar.gz\" \"spicetify-${{ env.TAG }}-${{ matrix.os }}-${{ matrix.arch }}.tar\"\n\n      - name: 7z - .zip\n        if: env.IS_WIN == 'true'\n        uses: edgarrc/action-7z@v1\n        with:\n          args: 7z a -bb0 -mx9 \"spicetify-${{ env.TAG }}-${{ matrix.os }}-${{ (matrix.arch == 'amd64' && 'x64') || (matrix.arch == 'arm64' && 'arm64') || 'x32' }}.zip\" \"./spicetify.exe\" \"./CustomApps\" \"./Extensions\" \"./Themes\" \"./jsHelper\" \"globals.d.ts\" \"css-map.json\"\n\n      - name: Release\n        if: env.IS_UNIX == 'true' || env.IS_WIN == 'true'\n        uses: softprops/action-gh-release@v2\n        with:\n          files: \"spicetify-${{ env.TAG }}-${{ matrix.os }}-${{ (matrix.os == 'windows' && matrix.arch == 'amd64' && 'x64') || (matrix.os == 'windows' && matrix.arch == '386' && 'x32') || matrix.arch }}.${{ matrix.os == 'windows' && 'zip' || 'tar.gz' }}\"\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n  trigger-release:\n    name: Trigger Homebrew/AUR Release\n    runs-on: ubuntu-latest\n    needs: release\n    steps:\n      - name: Update AUR package\n        uses: fjogeleit/http-request-action@master\n        with:\n          url: https://spicetify-update.itsmeow.dev/spicetify-update\n          method: GET\n      - name: Update Winget package\n        uses: vedantmgoyal9/winget-releaser@main\n        with:\n          identifier: Spicetify.Spicetify\n          installers-regex: '-windows-\\w+\\.zip$'\n          token: ${{ secrets.SPICETIFY_WINGET_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/labeler.yml",
    "content": "name: Issue Labeler\n\non:\n  issues:\n    types: [opened, edited]\n\njobs:\n  triage:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: github/issue-labeler@v3.4\n        with:\n          repo-token: \"${{ secrets.GITHUB_TOKEN }}\"\n          configuration-path: .github/labeler.yml\n          enable-versioned-regex: 0\n"
  },
  {
    "path": ".github/workflows/linter.yml",
    "content": "name: Code quality\n\non:\n  pull_request:\n    branches:\n      - \"main\"\n      - \"*/main/*/**\"\n  push:\n    branches:\n      - \"main\"\n      - \"*/main/*/**\"\n\njobs:\n  linter:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout the repo\n        uses: actions/checkout@v6\n\n      - name: Setup Biome\n        uses: biomejs/setup-biome@v2\n        with:\n          version: latest\n\n      - name: Run Biome\n        run: biome ci . --files-ignore-unknown=true --diagnostic-level=error\n"
  },
  {
    "path": ".github/workflows/lintpr.yml",
    "content": "name: Lint Pull Request\n\non:\n  pull_request_target:\n    types: [opened, edited, synchronize]\n\njobs:\n  lintpr:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Lint pull request title\n        uses: amannn/action-semantic-pull-request@v6\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          disallowScopes: |\n            [A-Z]+\n          subjectPattern: ^(?![A-Z]).+$\n"
  },
  {
    "path": ".gitignore",
    "content": "# Executables\nbin\ncli\nspicetify\nspicetify-cli\n*.exe\n\n# MacOS\n.DS_Store\n\n# Node.js\nnode_modules\npackage-lock.json\npackage.json\n\n# Logs\ninstall.log\n\npnpm-lock.yaml\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n\t\"recommendations\": [\"timonwong.shellcheck\", \"biomejs.biome\", \"golang.go\", \"ms-vscode.powershell\"]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n\t\"editor.formatOnSave\": true,\n\t\"[go]\": {\n\t\t\"editor.defaultFormatter\": \"golang.go\"\n\t},\n\t\"[powershell]\": {\n\t\t\"editor.defaultFormatter\": \"ms-vscode.powershell\"\n\t},\n\t\"[javascript][typescript][json]\": {\n\t\t\"editor.defaultFormatter\": \"biomejs.biome\"\n\t}\n}\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Spicetify-cli\n\n## Table of Contents\n\n- [I Have a Question](#i-have-a-question)\n- [How to Contribute](#how-to-contribute)\n  - [Reporting Bugs](#reporting-bugs)\n  - [Suggesting Enhancements](#suggesting-enhancements)\n  - [Your First Code Contribution](#your-first-code-contribution)\n  - [Improving The Documentation](#improving-the-documentation)\n  - [Commit Message Format](#commit-message-format)\n\n## I Have a Question\n\n> If you want to ask a question, we assume that you have read the available [Documentation](https://spicetify.app/docs/getting-started/).\n\nBefore you ask a question, it is best to search for existing [issues](https://github.com/spicetify/cli/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first.\n\nIf you then still feel the need to ask a question and need clarification, we recommend the following:\n\n- Open an [issue](https://github.com/spicetify/cli/issues/new).\n- Provide both Spicetify and Spotify version.\n- Explain what the problem is.\n\nWe will then take care of the issue as soon as possible.\n\n## How to Contribute\n\n> ### Legal Notice\n> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.\n\n### Reporting Bugs\n\n#### Before Submitting a Bug Report\n\nA good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible.\n\n- Make sure that you are using the latest version.\n- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://spicetify.app/docs/getting-started/). If you are looking for support, you might want to check [this section](#i-have-a-question)).\n- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/spicetify/cli/labels/%F0%9F%90%9B%20bug).\n\n#### How Do I Submit a Good Bug Report?\n\nWe use GitHub issues to track bugs and errors. If you run into an issue with the project:\n\n- Open an [issue](https://github.com/spicetify/cli/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.)\n  - Use the provided [bug template](https://github.com/spicetify/cli/issues/new?assignees=&labels=%F0%9F%90%9B+bug&projects=&template=bug_report.yml).\n- Explain the behavior you would expect and the actual behavior.\n- Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case.\n- Provide the information you collected in the previous section.\n\n### Suggesting Enhancements\n\nThis section guides you through submitting an enhancement suggestion for spicetify, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions.\n\n#### Before Submitting an Enhancement\n\n- Make sure that you are using the latest version.\n- Read the [documentation](https://spicetify.app/docs/getting-started/) carefully and find out if the functionality is already covered, maybe by an individual configuration.\n- Perform a [search](https://github.com/spicetify/cli/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.\n- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.\n\n#### How Do I Submit a Good Enhancement Suggestion?\n\nEnhancement suggestions are tracked as [GitHub issues](https://github.com/spicetify/cli/issues). Create an enhancement suggestion using the provided [feature request template](https://github.com/spicetify/cli/issues/new?assignees=&labels=%E2%9C%A8+feature&projects=&template=feature_request.yml).\n\n- Use a **clear and descriptive title** for the issue to identify the suggestion.\n- Provide a **step-by-step description of the suggested enhancement** in as many details as possible.\n- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you.\n- For GUIs, you may want to **include screenshots** which help you demonstrate the steps or point out the part which the suggestion is related to. Animated GIFS and videos may be helpful but are not expected. Some tools available are the [built-in screen recorder](https://support.apple.com/en-us/102618) on macOS, [LICEcap](https://www.cockos.com/licecap/) on macOS and Windows, and [ShareX](https://getsharex.com/) on Linux.\n- **Explain why this enhancement would be useful** to most spicetify users. You may also want to point out the other projects that solved it better and which could serve as inspiration.\n\n### Your First Code Contribution\n\n#### Requirements\n\n- [Go](https://go.dev/dl/)\n\n#### Environment Setup and Development\n\nFollow the steps outlined in the [documentation](https://spicetify.app/docs/development/compiling) or the steps below.\n1. Clone the repository using `git clone https://github.com/spicetify/cli`.\n2. Enter the repository directory and build the project.\n   * Windows\n      ```\n      cd cli\n      go build -o spicetify.exe\n      ```\n   * Linux and MacOS\n      ```\n      cd cli\n      go build -o spicetify\n      ```\n3. Execute the executable file generated by `go build` using `./spicetify` or `./spicetify.exe`.\n\n### Improving The Documentation\n\nTo improve the [documentation](https://spicetify.app/docs/getting-started), navigate to the documentation [repository](https://github.com/spicetify/docs).\n\n### Commit Message Format\n\n    <type>(<scope>): <subject>\n    <BLANK LINE>\n    <body>[optional]\n\n*   **type:** feat | fix | docs | chore | revert\n    *   **feat:** A new feature\n    *   **fix:** A bug fix\n    *   **docs:** Documentation only changes\n    *   **chore:** Changes to build process, auxiliary tools, libraries, and other things\n    *   **revert:** A reversion to a previous commit\n*   **scope:** Anything specifying place of the commit change\n*   **subject:** What changes you have done\n    *   Use the imperative, present tense: \"change\" not \"changed\" nor \"changes\"\n    *   Don't capitalize first letter\n    *   No dot (.) at the end\n*   **body**: More details of your changes, you can mention the most important changes here\n    *   Use the imperative, present tense: \"change\" not \"changed\" nor \"changes\"\n\nIf you want to learn more, view the [Angular - Git Commit Guidelines](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines).\n"
  },
  {
    "path": "CustomApps/lyrics-plus/OptionsMenu.js",
    "content": "const OptionsMenuItemIcon = react.createElement(\n\t\"svg\",\n\t{\n\t\twidth: 16,\n\t\theight: 16,\n\t\tviewBox: \"0 0 16 16\",\n\t\tfill: \"currentColor\",\n\t},\n\treact.createElement(\"path\", {\n\t\td: \"M13.985 2.383L5.127 12.754 1.388 8.375l-.658.77 4.397 5.149 9.618-11.262z\",\n\t})\n);\n\nconst OptionsMenuItem = react.memo(({ onSelect, value, isSelected }) => {\n\treturn react.createElement(\n\t\tSpicetify.ReactComponent.MenuItem,\n\t\t{\n\t\t\tonClick: onSelect,\n\t\t\ticon: isSelected ? OptionsMenuItemIcon : null,\n\t\t\ttrailingIcon: isSelected ? OptionsMenuItemIcon : null,\n\t\t},\n\t\tvalue\n\t);\n});\n\nconst OptionsMenu = react.memo(({ options, onSelect, selected, defaultValue, bold = false }) => {\n\t/**\n\t * <Spicetify.ReactComponent.ContextMenu\n\t *      menu = { options.map(a => <OptionsMenuItem>) }\n\t * >\n\t *      <button>\n\t *          <span> {select.value} </span>\n\t *          <svg> arrow icon </svg>\n\t *      </button>\n\t * </Spicetify.ReactComponent.ContextMenu>\n\t */\n\tconst menuRef = react.useRef(null);\n\treturn react.createElement(\n\t\tSpicetify.ReactComponent.ContextMenu,\n\t\t{\n\t\t\tmenu: react.createElement(\n\t\t\t\tSpicetify.ReactComponent.Menu,\n\t\t\t\t{},\n\t\t\t\toptions.map(({ key, value }) =>\n\t\t\t\t\treact.createElement(OptionsMenuItem, {\n\t\t\t\t\t\tvalue,\n\t\t\t\t\t\tonSelect: () => {\n\t\t\t\t\t\t\tonSelect(key);\n\t\t\t\t\t\t\t// Close menu on item click\n\t\t\t\t\t\t\tmenuRef.current?.click();\n\t\t\t\t\t\t},\n\t\t\t\t\t\tisSelected: selected?.key === key,\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t\t),\n\t\t\ttrigger: \"click\",\n\t\t\taction: \"toggle\",\n\t\t\trenderInline: false,\n\t\t},\n\t\treact.createElement(\n\t\t\t\"button\",\n\t\t\t{\n\t\t\t\tclassName: \"optionsMenu-dropBox\",\n\t\t\t\tref: menuRef,\n\t\t\t},\n\t\t\treact.createElement(\n\t\t\t\t\"span\",\n\t\t\t\t{\n\t\t\t\t\tclassName: bold ? \"main-type-mestoBold\" : \"main-type-mesto\",\n\t\t\t\t},\n\t\t\t\tselected?.value || defaultValue\n\t\t\t),\n\t\t\treact.createElement(\n\t\t\t\t\"svg\",\n\t\t\t\t{\n\t\t\t\t\theight: \"16\",\n\t\t\t\t\twidth: \"16\",\n\t\t\t\t\tfill: \"currentColor\",\n\t\t\t\t\tviewBox: \"0 0 16 16\",\n\t\t\t\t},\n\t\t\t\treact.createElement(\"path\", {\n\t\t\t\t\td: \"M3 6l5 5.794L13 6z\",\n\t\t\t\t})\n\t\t\t)\n\t\t)\n\t);\n});\n\nfunction getMusixmatchTranslationPrefix() {\n\tif (typeof window !== \"undefined\" && typeof window.__lyricsPlusMusixmatchTranslationPrefix === \"string\") {\n\t\treturn window.__lyricsPlusMusixmatchTranslationPrefix;\n\t}\n\n\treturn \"musixmatchTranslation:\";\n}\n\nconst TranslationMenu = react.memo(({ friendlyLanguage, hasTranslation, musixmatchLanguages, musixmatchSelectedLanguage }) => {\n\tconst musixmatchTranslationPrefix = getMusixmatchTranslationPrefix();\n\n\tconst [languageMap, setLanguageMap] = react.useState({});\n\n\treact.useEffect(() => {\n\t\tlet cancelled = false;\n\n\t\tif (typeof ProviderMusixmatch !== \"undefined\" && ProviderMusixmatch && typeof ProviderMusixmatch.getLanguages === \"function\") {\n\t\t\t(async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst languages = await ProviderMusixmatch.getLanguages();\n\t\t\t\t\tif (!cancelled) {\n\t\t\t\t\t\tsetLanguageMap(languages);\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\"Failed to fetch Musixmatch languages:\", error);\n\t\t\t\t}\n\t\t\t})();\n\t\t}\n\n\t\treturn () => {\n\t\t\tcancelled = true;\n\t\t};\n\t}, []);\n\n\tconst items = useMemo(() => {\n\t\tlet sourceOptions = {\n\t\t\tnone: \"None\",\n\t\t};\n\n\t\tconst translationDisplayOptions = {\n\t\t\treplace: \"Replace original\",\n\t\t\tbelow: \"Below original\",\n\t\t};\n\n\t\tconst languageOptions = {\n\t\t\toff: \"Off\",\n\t\t\t\"zh-hans\": \"Chinese (Simplified)\",\n\t\t\t\"zh-hant\": \"Chinese (Traditional)\",\n\t\t\tja: \"Japanese\",\n\t\t\tko: \"Korean\",\n\t\t};\n\n\t\tlet modeOptions = {\n\t\t\tnone: \"None\",\n\t\t};\n\n\t\tconst musixmatchDisplay = new Intl.DisplayNames([\"en\"], { type: \"language\" });\n\t\tconst availableMusixmatchLanguages = Array.isArray(musixmatchLanguages) ? [...new Set(musixmatchLanguages.filter(Boolean))] : [];\n\t\tconst activeMusixmatchLanguage = musixmatchSelectedLanguage && musixmatchSelectedLanguage !== \"none\" ? musixmatchSelectedLanguage : null;\n\t\tif (hasTranslation.musixmatch && activeMusixmatchLanguage) {\n\t\t\tavailableMusixmatchLanguages.push(activeMusixmatchLanguage);\n\t\t}\n\n\t\tif (availableMusixmatchLanguages.length) {\n\t\t\tconst musixmatchOptionsArray = availableMusixmatchLanguages.map((code) => {\n\t\t\t\tlet label = \"\";\n\t\t\t\ttry {\n\t\t\t\t\tif (languageMap && languageMap[code]) {\n\t\t\t\t\t\tlabel = languageMap[code];\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlabel = musixmatchDisplay.of(code) ?? code.toUpperCase();\n\t\t\t\t\t}\n\t\t\t\t} catch (e) {\n\t\t\t\t\tlabel = code.toUpperCase();\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tkey: `${musixmatchTranslationPrefix}${code}`,\n\t\t\t\t\tlabel: `${label} (Musixmatch)`,\n\t\t\t\t};\n\t\t\t});\n\n\t\t\tmusixmatchOptionsArray.sort((a, b) => a.label.localeCompare(b.label));\n\n\t\t\tconst musixmatchOptions = musixmatchOptionsArray.reduce((acc, { key, label }) => {\n\t\t\t\tacc[key] = label;\n\t\t\t\treturn acc;\n\t\t\t}, {});\n\t\t\tsourceOptions = { ...sourceOptions, ...musixmatchOptions };\n\t\t}\n\n\t\tif (hasTranslation.netease) {\n\t\t\tsourceOptions = {\n\t\t\t\t...sourceOptions,\n\t\t\t\tneteaseTranslation: \"Chinese (Netease)\",\n\t\t\t};\n\t\t}\n\n\t\tswitch (friendlyLanguage) {\n\t\t\tcase \"japanese\": {\n\t\t\t\tmodeOptions = {\n\t\t\t\t\tfurigana: \"Furigana\",\n\t\t\t\t\tromaji: \"Romaji\",\n\t\t\t\t\thiragana: \"Hiragana\",\n\t\t\t\t\tkatakana: \"Katakana\",\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"korean\": {\n\t\t\t\tmodeOptions = {\n\t\t\t\t\tromaja: \"Romaja\",\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"chinese\": {\n\t\t\t\tmodeOptions = {\n\t\t\t\t\tcn: \"Simplified Chinese\",\n\t\t\t\t\thk: \"Traditional Chinese (Hong Kong)\",\n\t\t\t\t\ttw: \"Traditional Chinese (Taiwan)\",\n\t\t\t\t};\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\tconst configItems = [\n\t\t\t{\n\t\t\t\tdesc: \"Translation Provider\",\n\t\t\t\tkey: \"translate:translated-lyrics-source\",\n\t\t\t\ttype: ConfigSelection,\n\t\t\t\toptions: sourceOptions,\n\t\t\t\trenderInline: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc: \"Translation Display\",\n\t\t\t\tkey: \"translate:display-mode\",\n\t\t\t\ttype: ConfigSelection,\n\t\t\t\toptions: translationDisplayOptions,\n\t\t\t\trenderInline: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc: \"Language Override\",\n\t\t\t\tkey: \"translate:detect-language-override\",\n\t\t\t\ttype: ConfigSelection,\n\t\t\t\toptions: languageOptions,\n\t\t\t\trenderInline: true,\n\t\t\t\t// for songs in languages that support translation but not Convert (e.g., English), the option is disabled.\n\t\t\t\twhen: () => friendlyLanguage,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc: \"Display Mode\",\n\t\t\t\tkey: `translation-mode:${friendlyLanguage}`,\n\t\t\t\ttype: ConfigSelection,\n\t\t\t\toptions: modeOptions,\n\t\t\t\trenderInline: true,\n\t\t\t\t// for songs in languages that support translation but not Convert (e.g., English), the option is disabled.\n\t\t\t\twhen: () => friendlyLanguage,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdesc: \"Convert\",\n\t\t\t\tkey: \"translate\",\n\t\t\t\ttype: ConfigSlider,\n\t\t\t\ttrigger: \"click\",\n\t\t\t\taction: \"toggle\",\n\t\t\t\trenderInline: true,\n\t\t\t\t// for songs in languages that support translation but not Convert (e.g., English), the option is disabled.\n\t\t\t\twhen: () => friendlyLanguage,\n\t\t\t},\n\t\t];\n\n\t\treturn configItems;\n\t}, [\n\t\tfriendlyLanguage,\n\t\thasTranslation.musixmatch,\n\t\thasTranslation.netease,\n\t\tArray.isArray(musixmatchLanguages) ? musixmatchLanguages.join(\",\") : \"\",\n\t\tmusixmatchSelectedLanguage || \"\",\n\t\tmusixmatchTranslationPrefix,\n\t\tlanguageMap,\n\t]);\n\n\tuseEffect(() => {\n\t\t// Currently opened Context Menu does not receive prop changes\n\t\t// If we were to use keys the Context Menu would close on re-render\n\t\tconst event = new CustomEvent(\"lyrics-plus\", {\n\t\t\tdetail: {\n\t\t\t\ttype: \"translation-menu\",\n\t\t\t\titems,\n\t\t\t},\n\t\t});\n\t\tdocument.dispatchEvent(event);\n\t}, [friendlyLanguage, items]);\n\n\treturn react.createElement(\n\t\tSpicetify.ReactComponent.TooltipWrapper,\n\t\t{\n\t\t\tlabel: \"Conversion\",\n\t\t},\n\t\treact.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tclassName: \"lyrics-tooltip-wrapper\",\n\t\t\t},\n\t\t\treact.createElement(\n\t\t\t\tSpicetify.ReactComponent.ContextMenu,\n\t\t\t\t{\n\t\t\t\t\tmenu: react.createElement(\n\t\t\t\t\t\tSpicetify.ReactComponent.Menu,\n\t\t\t\t\t\t{},\n\t\t\t\t\t\treact.createElement(\"h3\", null, \" Conversions\"),\n\t\t\t\t\t\treact.createElement(OptionList, {\n\t\t\t\t\t\t\ttype: \"translation-menu\",\n\t\t\t\t\t\t\titems,\n\t\t\t\t\t\t\tonChange: (name, value) => {\n\t\t\t\t\t\t\t\tif (name === \"translate\") {\n\t\t\t\t\t\t\t\t\tCONFIG.visual[\"translate:translated-lyrics-source\"] = \"none\";\n\t\t\t\t\t\t\t\t\tlocalStorage.setItem(`${APP_NAME}:visual:translate:translated-lyrics-source`, \"none\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tif (name === \"translate:translated-lyrics-source\") {\n\t\t\t\t\t\t\t\t\tconst hasTranslationProvider = typeof value === \"string\" && value !== \"none\";\n\t\t\t\t\t\t\t\t\tif (hasTranslationProvider && CONFIG.visual.translate) {\n\t\t\t\t\t\t\t\t\t\tCONFIG.visual.translate = false;\n\t\t\t\t\t\t\t\t\t\tlocalStorage.setItem(`${APP_NAME}:visual:translate`, \"false\");\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tlet nextMusixmatchLanguage = \"none\";\n\t\t\t\t\t\t\t\t\tif (typeof value === \"string\" && value.startsWith(musixmatchTranslationPrefix)) {\n\t\t\t\t\t\t\t\t\t\tnextMusixmatchLanguage = value.slice(musixmatchTranslationPrefix.length) || \"none\";\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tif (CONFIG.visual[\"musixmatch-translation-language\"] !== nextMusixmatchLanguage) {\n\t\t\t\t\t\t\t\t\t\tCONFIG.visual[\"musixmatch-translation-language\"] = nextMusixmatchLanguage;\n\t\t\t\t\t\t\t\t\t\tlocalStorage.setItem(`${APP_NAME}:visual:musixmatch-translation-language`, nextMusixmatchLanguage);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tCONFIG.visual[name] = value;\n\t\t\t\t\t\t\t\tlocalStorage.setItem(`${APP_NAME}:visual:${name}`, value);\n\t\t\t\t\t\t\t\tlyricContainerUpdate?.();\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t})\n\t\t\t\t\t),\n\t\t\t\t\ttrigger: \"click\",\n\t\t\t\t\taction: \"toggle\",\n\t\t\t\t\trenderInline: true,\n\t\t\t\t},\n\t\t\t\treact.createElement(\n\t\t\t\t\t\"button\",\n\t\t\t\t\t{\n\t\t\t\t\t\tclassName: \"lyrics-config-button\",\n\t\t\t\t\t},\n\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\"p1\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\twidth: 16,\n\t\t\t\t\t\t\theight: 16,\n\t\t\t\t\t\t\tviewBox: \"0 0 16 10.3\",\n\t\t\t\t\t\t\tfill: \"currentColor\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"⇄\"\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t)\n\t\t)\n\t);\n});\n\nconst AdjustmentsMenu = react.memo(({ mode, hasPerformer }) => {\n\treturn react.createElement(\n\t\tSpicetify.ReactComponent.TooltipWrapper,\n\t\t{\n\t\t\tlabel: \"Adjustments\",\n\t\t},\n\t\treact.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tclassName: \"lyrics-tooltip-wrapper\",\n\t\t\t},\n\t\t\treact.createElement(\n\t\t\t\tSpicetify.ReactComponent.ContextMenu,\n\t\t\t\t{\n\t\t\t\t\tmenu: react.createElement(\n\t\t\t\t\t\tSpicetify.ReactComponent.Menu,\n\t\t\t\t\t\t{},\n\t\t\t\t\t\treact.createElement(\"h3\", null, \" Adjustments\"),\n\t\t\t\t\t\treact.createElement(OptionList, {\n\t\t\t\t\t\t\titems: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tdesc: \"Font size\",\n\t\t\t\t\t\t\t\t\tkey: \"font-size\",\n\t\t\t\t\t\t\t\t\ttype: ConfigAdjust,\n\t\t\t\t\t\t\t\t\tmin: fontSizeLimit.min,\n\t\t\t\t\t\t\t\t\tmax: fontSizeLimit.max,\n\t\t\t\t\t\t\t\t\tstep: fontSizeLimit.step,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tdesc: \"Track delay\",\n\t\t\t\t\t\t\t\t\tkey: \"delay\",\n\t\t\t\t\t\t\t\t\ttype: ConfigAdjust,\n\t\t\t\t\t\t\t\t\tmin: Number.NEGATIVE_INFINITY,\n\t\t\t\t\t\t\t\t\tmax: Number.POSITIVE_INFINITY,\n\t\t\t\t\t\t\t\t\tstep: 250,\n\t\t\t\t\t\t\t\t\twhen: () => mode === SYNCED || mode === KARAOKE,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tdesc: \"Compact\",\n\t\t\t\t\t\t\t\t\tkey: \"synced-compact\",\n\t\t\t\t\t\t\t\t\ttype: ConfigSlider,\n\t\t\t\t\t\t\t\t\twhen: () => mode === SYNCED || mode === KARAOKE,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tdesc: \"Show performers\",\n\t\t\t\t\t\t\t\t\tkey: \"show-performers\",\n\t\t\t\t\t\t\t\t\ttype: ConfigSlider,\n\t\t\t\t\t\t\t\t\twhen: () => hasPerformer && (mode === SYNCED || mode === KARAOKE || mode === UNSYNCED),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tdesc: \"Dual panel\",\n\t\t\t\t\t\t\t\t\tkey: \"dual-genius\",\n\t\t\t\t\t\t\t\t\ttype: ConfigSlider,\n\t\t\t\t\t\t\t\t\twhen: () => mode === GENIUS,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tonChange: (name, value) => {\n\t\t\t\t\t\t\t\tCONFIG.visual[name] = value;\n\t\t\t\t\t\t\t\tlocalStorage.setItem(`${APP_NAME}:visual:${name}`, value);\n\t\t\t\t\t\t\t\tname === \"delay\" && localStorage.setItem(`lyrics-delay:${Spicetify.Player.data.item.uri}`, value);\n\t\t\t\t\t\t\t\tlyricContainerUpdate?.();\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t})\n\t\t\t\t\t),\n\t\t\t\t\ttrigger: \"click\",\n\t\t\t\t\taction: \"toggle\",\n\t\t\t\t\trenderInline: true,\n\t\t\t\t},\n\t\t\t\treact.createElement(\n\t\t\t\t\t\"button\",\n\t\t\t\t\t{\n\t\t\t\t\t\tclassName: \"lyrics-config-button\",\n\t\t\t\t\t},\n\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\"svg\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\twidth: 16,\n\t\t\t\t\t\t\theight: 16,\n\t\t\t\t\t\t\tviewBox: \"0 0 16 10.3\",\n\t\t\t\t\t\t\tfill: \"currentColor\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\treact.createElement(\"path\", {\n\t\t\t\t\t\t\td: \"M 10.8125,0 C 9.7756347,0 8.8094481,0.30798341 8,0.836792 7.1905519,0.30798341 6.2243653,0 5.1875,0 2.3439941,0 0,2.3081055 0,5.15625 0,8.0001222 2.3393555,10.3125 5.1875,10.3125 6.2243653,10.3125 7.1905519,10.004517 8,9.4757081 8.8094481,10.004517 9.7756347,10.3125 10.8125,10.3125 13.656006,10.3125 16,8.0043944 16,5.15625 16,2.3123779 13.660644,0 10.8125,0 Z M 8,2.0146484 C 8.2629394,2.2503662 8.4963378,2.5183106 8.6936034,2.8125 H 7.3063966 C 7.5036622,2.5183106 7.7370606,2.2503662 8,2.0146484 Z M 6.619995,4.6875 C 6.6560059,4.3625487 6.7292481,4.0485841 6.8350831,3.75 h 2.3298338 c 0.1059572,0.2985841 0.1790772,0.6125487 0.21521,0.9375 z M 9.380005,5.625 C 9.3439941,5.9499512 9.2707519,6.2639159 9.1649169,6.5625 H 6.8350831 C 6.7291259,6.2639159 6.6560059,5.9499512 6.6198731,5.625 Z M 5.1875,9.375 c -2.3435059,0 -4.25,-1.8925781 -4.25,-4.21875 0,-2.3261719 1.9064941,-4.21875 4.25,-4.21875 0.7366944,0 1.4296875,0.1899414 2.0330809,0.5233154 C 6.2563478,2.3981934 5.65625,3.7083741 5.65625,5.15625 c 0,1.4478759 0.6000978,2.7580566 1.5643309,3.6954347 C 6.6171875,9.1850584 5.9241944,9.375 5.1875,9.375 Z M 8,8.2978516 C 7.7370606,8.0621337 7.5036622,7.7938231 7.3063966,7.4996337 H 8.6936034 C 8.4963378,7.7938231 8.2629394,8.0621338 8,8.2978516 Z M 10.8125,9.375 C 10.075806,9.375 9.3828125,9.1850584 8.7794191,8.8516847 9.7436522,7.9143066 10.34375,6.6041259 10.34375,5.15625 10.34375,3.7083741 9.7436522,2.3981934 8.7794191,1.4608154 9.3828125,1.1274414 10.075806,0.9375 10.8125,0.9375 c 2.343506,0 4.25,1.8925781 4.25,4.21875 0,2.3261719 -1.906494,4.21875 -4.25,4.21875 z m 0,0\",\n\t\t\t\t\t\t})\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t)\n\t\t)\n\t);\n});\n"
  },
  {
    "path": "CustomApps/lyrics-plus/Pages.js",
    "content": "const CreditFooter = react.memo(({ provider, copyright }) => {\n\tif (provider === \"local\") return null;\n\tconst credit = [Spicetify.Locale.get(\"web-player.lyrics.providedBy\", provider)];\n\tif (copyright) {\n\t\tcredit.push(...copyright.split(\"\\n\"));\n\t}\n\n\treturn (\n\t\tprovider &&\n\t\treact.createElement(\n\t\t\t\"p\",\n\t\t\t{\n\t\t\t\tclassName: \"lyrics-lyricsContainer-Provider main-type-mesto\",\n\t\t\t\tdir: \"auto\",\n\t\t\t},\n\t\t\tcredit.join(\" • \")\n\t\t)\n\t);\n});\n\nconst IdlingIndicator = ({ isActive, progress, delay }) => {\n\treturn react.createElement(\n\t\t\"div\",\n\t\t{\n\t\t\tclassName: `lyrics-idling-indicator ${\n\t\t\t\t!isActive ? \"lyrics-idling-indicator-hidden\" : \"\"\n\t\t\t} lyrics-lyricsContainer-LyricsLine lyrics-lyricsContainer-LyricsLine-active`,\n\t\t\tstyle: {\n\t\t\t\t\"--position-index\": 0,\n\t\t\t\t\"--animation-index\": 1,\n\t\t\t\t\"--indicator-delay\": `${delay}ms`,\n\t\t\t},\n\t\t},\n\t\treact.createElement(\"div\", { className: `lyrics-idling-indicator__circle ${progress >= 0.05 ? \"active\" : \"\"}` }),\n\t\treact.createElement(\"div\", { className: `lyrics-idling-indicator__circle ${progress >= 0.33 ? \"active\" : \"\"}` }),\n\t\treact.createElement(\"div\", { className: `lyrics-idling-indicator__circle ${progress >= 0.66 ? \"active\" : \"\"}` })\n\t);\n};\n\nconst emptyLine = {\n\tstartTime: 0,\n\tendTime: 0,\n\ttext: [],\n};\n\nconst useTrackPosition = (callback) => {\n\tconst callbackRef = useRef();\n\tcallbackRef.current = callback;\n\n\tuseEffect(() => {\n\t\tconst interval = setInterval(callbackRef.current, 50);\n\n\t\treturn () => {\n\t\t\tclearInterval(interval);\n\t\t};\n\t}, [callbackRef]);\n};\n\nconst KaraokeLine = ({ text, isActive, position, startTime, endTime }) => {\n\tif (endTime && position > endTime) {\n\t\treturn text.map(({ word }) => word).join(\"\");\n\t}\n\n\treturn text.map(({ word, time }, i) => {\n\t\tconst isWordActive = position >= startTime;\n\t\tstartTime += time;\n\t\treturn react.createElement(\n\t\t\t\"span\",\n\t\t\t{\n\t\t\t\tkey: i,\n\t\t\t\tclassName: `lyrics-lyricsContainer-Karaoke-Word${isWordActive ? \" lyrics-lyricsContainer-Karaoke-WordActive\" : \"\"}`,\n\t\t\t\tstyle: {\n\t\t\t\t\t\"--word-duration\": `${time}ms`,\n\t\t\t\t\t// don't animate unless we have to\n\t\t\t\t\ttransition: !isWordActive ? \"all 0s linear\" : \"\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tword\n\t\t);\n\t});\n};\n\nconst SyncedLyricsPage = react.memo(({ lyrics = [], provider, copyright, isKara }) => {\n\tconst [position, setPosition] = useState(0);\n\tconst activeLineEle = useRef();\n\tconst lyricContainerEle = useRef();\n\n\tuseTrackPosition(() => {\n\t\tconst newPos = Spicetify.Player.getProgress();\n\t\tconst delay = CONFIG.visual[\"global-delay\"] + CONFIG.visual.delay;\n\t\tif (newPos !== position) {\n\t\t\tsetPosition(newPos + delay);\n\t\t}\n\t});\n\n\tconst lyricWithEmptyLines = useMemo(\n\t\t() =>\n\t\t\t[emptyLine, emptyLine, ...lyrics].map((line, i) => ({\n\t\t\t\t...line,\n\t\t\t\tlineNumber: i,\n\t\t\t})),\n\t\t[lyrics]\n\t);\n\n\tconst lyricsId = lyrics[0].text;\n\n\tlet activeLineIndex = 0;\n\tfor (let i = lyricWithEmptyLines.length - 1; i > 0; i--) {\n\t\tif (position >= lyricWithEmptyLines[i].startTime) {\n\t\t\tactiveLineIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tconst activeLines = useMemo(() => {\n\t\tconst startIndex = Math.max(activeLineIndex - 1 - CONFIG.visual[\"lines-before\"], 0);\n\t\t// 3 lines = 1 padding top + 1 padding bottom + 1 active\n\t\tconst linesCount = CONFIG.visual[\"lines-before\"] + CONFIG.visual[\"lines-after\"] + 3;\n\t\treturn lyricWithEmptyLines.slice(startIndex, startIndex + linesCount);\n\t}, [activeLineIndex, lyricWithEmptyLines]);\n\n\tlet offset = lyricContainerEle.current ? lyricContainerEle.current.clientHeight / 2 : 0;\n\tif (activeLineEle.current) {\n\t\toffset += -(activeLineEle.current.offsetTop + activeLineEle.current.clientHeight / 2);\n\t}\n\n\treturn react.createElement(\n\t\t\"div\",\n\t\t{\n\t\t\tclassName: \"lyrics-lyricsContainer-SyncedLyricsPage\",\n\t\t\tref: lyricContainerEle,\n\t\t},\n\t\treact.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tclassName: \"lyrics-lyricsContainer-SyncedLyrics\",\n\t\t\t\tstyle: {\n\t\t\t\t\t\"--offset\": `${offset}px`,\n\t\t\t\t},\n\t\t\t\tkey: lyricsId,\n\t\t\t},\n\t\t\tactiveLines.map(({ text, lineNumber, startTime, endTime, originalText, performer }, i) => {\n\t\t\t\tif (i === 1 && activeLineIndex === 1) {\n\t\t\t\t\treturn react.createElement(IdlingIndicator, {\n\t\t\t\t\t\tprogress: position / activeLines[2].startTime,\n\t\t\t\t\t\tdelay: activeLines[2].startTime / 3,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tlet className = \"lyrics-lyricsContainer-LyricsLine\";\n\t\t\t\tconst activeElementIndex = Math.min(activeLineIndex, CONFIG.visual[\"lines-before\"] + 1);\n\t\t\t\tlet ref;\n\n\t\t\t\tconst isActive = activeElementIndex === i;\n\t\t\t\tif (isActive) {\n\t\t\t\t\tclassName += \" lyrics-lyricsContainer-LyricsLine-active\";\n\t\t\t\t\tref = activeLineEle;\n\t\t\t\t}\n\n\t\t\t\tlet animationIndex;\n\t\t\t\tif (activeLineIndex <= CONFIG.visual[\"lines-before\"]) {\n\t\t\t\t\tanimationIndex = i - activeLineIndex;\n\t\t\t\t} else {\n\t\t\t\t\tanimationIndex = i - CONFIG.visual[\"lines-before\"] - 1;\n\t\t\t\t}\n\n\t\t\t\tconst paddingLine = (animationIndex < 0 && -animationIndex > CONFIG.visual[\"lines-before\"]) || animationIndex > CONFIG.visual[\"lines-after\"];\n\t\t\t\tif (paddingLine) {\n\t\t\t\t\tclassName += \" lyrics-lyricsContainer-LyricsLine-paddingLine\";\n\t\t\t\t}\n\t\t\t\tconst showTranslatedBelow = CONFIG.visual[\"translate:display-mode\"] === \"below\";\n\t\t\t\t// If we have original text and we are showing translated below, we should show the original text\n\t\t\t\t// Otherwise we should show the translated text\n\t\t\t\tconst lineText = originalText && showTranslatedBelow ? originalText : text;\n\n\t\t\t\t// Convert lyrics to text for comparison\n\t\t\t\tconst belowOrigin = (typeof originalText === \"object\" ? originalText?.props?.children?.[0] : originalText)?.replace(/\\s+/g, \"\");\n\t\t\t\tconst belowTxt = (typeof text === \"object\" ? text?.props?.children?.[0] : text)?.replace(/\\s+/g, \"\");\n\n\t\t\t\tconst belowMode = showTranslatedBelow && originalText && belowOrigin !== belowTxt;\n\n\t\t\t\treturn react.createElement(\n\t\t\t\t\t\"div\",\n\t\t\t\t\t{\n\t\t\t\t\t\tclassName,\n\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\tcursor: \"pointer\",\n\t\t\t\t\t\t\t\"--position-index\": animationIndex,\n\t\t\t\t\t\t\t\"--animation-index\": (animationIndex < 0 ? 0 : animationIndex) + 1,\n\t\t\t\t\t\t\t\"--blur-index\": Math.abs(animationIndex),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tdir: \"auto\",\n\t\t\t\t\t\tref,\n\t\t\t\t\t\tkey: lineNumber,\n\t\t\t\t\t\tonClick: (event) => {\n\t\t\t\t\t\t\tif (startTime) {\n\t\t\t\t\t\t\t\tSpicetify.Player.seek(startTime);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\"p\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tonContextMenu: (event) => {\n\t\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t\t\tSpicetify.Platform.ClipboardAPI.copy(Utils.convertParsedToLRC(lyrics, belowMode).original)\n\t\t\t\t\t\t\t\t\t.then(() => Spicetify.showNotification(\"Lyrics copied to clipboard\"))\n\t\t\t\t\t\t\t\t\t.catch(() => Spicetify.showNotification(\"Failed to copy lyrics to clipboard\"));\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t(() => {\n\t\t\t\t\t\t\tif (!CONFIG.visual[\"show-performers\"] || !performer) return null;\n\n\t\t\t\t\t\t\tif (!CONFIG.visual[\"synced-compact\"]) {\n\t\t\t\t\t\t\t\tconst previousLine = lyricWithEmptyLines[lineNumber - 1];\n\t\t\t\t\t\t\t\tif (previousLine && previousLine.performer === performer) return null;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\treturn react.createElement(\n\t\t\t\t\t\t\t\t\"span\",\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tclassName: \"lyrics-lyricsContainer-Performer\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tperformer\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})(),\n\t\t\t\t\t\t!isKara ? lineText : react.createElement(KaraokeLine, { text, startTime, endTime, position, isActive })\n\t\t\t\t\t),\n\t\t\t\t\tbelowMode &&\n\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\"p\",\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\t\t\topacity: 0.5,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tonContextMenu: (event) => {\n\t\t\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t\t\t\tSpicetify.Platform.ClipboardAPI.copy(Utils.convertParsedToLRC(lyrics, belowMode).conver)\n\t\t\t\t\t\t\t\t\t\t.then(() => Spicetify.showNotification(\"Translated lyrics copied to clipboard\"))\n\t\t\t\t\t\t\t\t\t\t.catch(() => Spicetify.showNotification(\"Failed to copy translated lyrics to clipboard\"));\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\ttext\n\t\t\t\t\t\t)\n\t\t\t\t);\n\t\t\t})\n\t\t),\n\t\treact.createElement(CreditFooter, {\n\t\t\tprovider,\n\t\t\tcopyright,\n\t\t})\n\t);\n});\n\nclass SearchBar extends react.Component {\n\tconstructor() {\n\t\tsuper();\n\t\tthis.state = {\n\t\t\thidden: true,\n\t\t\tatNode: 0,\n\t\t\tfoundNodes: [],\n\t\t};\n\t\tthis.container = null;\n\t}\n\n\tcomponentDidMount() {\n\t\tthis.viewPort = document.querySelector(\".main-view-container .os-viewport\");\n\t\tthis.mainViewOffsetTop = document.querySelector(\".Root__main-view\").offsetTop;\n\t\tthis.toggleCallback = () => {\n\t\t\tif (!(Spicetify.Platform.History.location.pathname === \"/lyrics-plus\" && this.container)) return;\n\n\t\t\tif (this.state.hidden) {\n\t\t\t\tthis.setState({ hidden: false });\n\t\t\t\tthis.container.focus();\n\t\t\t} else {\n\t\t\t\tthis.setState({ hidden: true });\n\t\t\t\tthis.container.blur();\n\t\t\t}\n\t\t};\n\t\tthis.unFocusCallback = () => {\n\t\t\tthis.container.blur();\n\t\t\tthis.setState({ hidden: true });\n\t\t};\n\t\tthis.loopThroughCallback = (event) => {\n\t\t\tif (!this.state.foundNodes.length) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (event.key === \"Enter\") {\n\t\t\t\tconst dir = event.shiftKey ? -1 : 1;\n\t\t\t\tlet atNode = this.state.atNode + dir;\n\t\t\t\tif (atNode < 0) {\n\t\t\t\t\tatNode = this.state.foundNodes.length - 1;\n\t\t\t\t}\n\t\t\t\tatNode %= this.state.foundNodes.length;\n\t\t\t\tconst rects = this.state.foundNodes[atNode].getBoundingClientRect();\n\t\t\t\tthis.viewPort.scrollBy(0, rects.y - 100);\n\t\t\t\tthis.setState({ atNode });\n\t\t\t}\n\t\t};\n\n\t\tSpicetify.Mousetrap().bind(\"mod+shift+f\", this.toggleCallback);\n\t\tSpicetify.Mousetrap(this.container).bind(\"mod+shift+f\", this.toggleCallback);\n\t\tSpicetify.Mousetrap(this.container).bind(\"enter\", this.loopThroughCallback);\n\t\tSpicetify.Mousetrap(this.container).bind(\"shift+enter\", this.loopThroughCallback);\n\t\tSpicetify.Mousetrap(this.container).bind(\"esc\", this.unFocusCallback);\n\t}\n\n\tcomponentWillUnmount() {\n\t\tSpicetify.Mousetrap().unbind(\"mod+shift+f\", this.toggleCallback);\n\t\tSpicetify.Mousetrap(this.container).unbind(\"mod+shift+f\", this.toggleCallback);\n\t\tSpicetify.Mousetrap(this.container).unbind(\"enter\", this.loopThroughCallback);\n\t\tSpicetify.Mousetrap(this.container).unbind(\"shift+enter\", this.loopThroughCallback);\n\t\tSpicetify.Mousetrap(this.container).unbind(\"esc\", this.unFocusCallback);\n\t}\n\n\tgetNodeFromInput(event) {\n\t\tconst value = event.target.value.toLowerCase();\n\t\tif (!value) {\n\t\t\tthis.setState({ foundNodes: [] });\n\t\t\tthis.viewPort.scrollTo(0, 0);\n\t\t\treturn;\n\t\t}\n\n\t\tconst lyricsPage = document.querySelector(\".lyrics-lyricsContainer-UnsyncedLyricsPage\");\n\t\tconst walker = document.createTreeWalker(\n\t\t\tlyricsPage,\n\t\t\tNodeFilter.SHOW_TEXT,\n\t\t\t(node) => {\n\t\t\t\tif (node.textContent.toLowerCase().includes(value)) {\n\t\t\t\t\treturn NodeFilter.FILTER_ACCEPT;\n\t\t\t\t}\n\t\t\t\treturn NodeFilter.FILTER_REJECT;\n\t\t\t},\n\t\t\tfalse\n\t\t);\n\n\t\tconst foundNodes = [];\n\t\twhile (walker.nextNode()) {\n\t\t\tconst range = document.createRange();\n\t\t\trange.selectNodeContents(walker.currentNode);\n\t\t\tfoundNodes.push(range);\n\t\t}\n\n\t\tif (!foundNodes.length) {\n\t\t\tthis.viewPort.scrollBy(0, 0);\n\t\t} else {\n\t\t\tconst rects = foundNodes[0].getBoundingClientRect();\n\t\t\tthis.viewPort.scrollBy(0, rects.y - 100);\n\t\t}\n\n\t\tthis.setState({ foundNodes, atNode: 0 });\n\t}\n\n\trender() {\n\t\tlet y = 0;\n\t\tlet height = 0;\n\t\tif (this.state.foundNodes.length) {\n\t\t\tconst node = this.state.foundNodes[this.state.atNode];\n\t\t\tconst rects = node.getBoundingClientRect();\n\t\t\ty = rects.y + this.viewPort.scrollTop - this.mainViewOffsetTop;\n\t\t\theight = rects.height;\n\t\t}\n\t\treturn react.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tclassName: `lyrics-Searchbar${this.state.hidden ? \" hidden\" : \"\"}`,\n\t\t\t},\n\t\t\treact.createElement(\"input\", {\n\t\t\t\tref: (c) => {\n\t\t\t\t\tthis.container = c;\n\t\t\t\t},\n\t\t\t\tonChange: this.getNodeFromInput.bind(this),\n\t\t\t}),\n\t\t\treact.createElement(\"svg\", {\n\t\t\t\twidth: 16,\n\t\t\t\theight: 16,\n\t\t\t\tviewBox: \"0 0 16 16\",\n\t\t\t\tfill: \"currentColor\",\n\t\t\t\tdangerouslySetInnerHTML: {\n\t\t\t\t\t__html: Spicetify.SVGIcons.search,\n\t\t\t\t},\n\t\t\t}),\n\t\t\treact.createElement(\n\t\t\t\t\"span\",\n\t\t\t\t{\n\t\t\t\t\thidden: this.state.foundNodes.length === 0,\n\t\t\t\t},\n\t\t\t\t`${this.state.atNode + 1}/${this.state.foundNodes.length}`\n\t\t\t),\n\t\t\treact.createElement(\"div\", {\n\t\t\t\tclassName: \"lyrics-Searchbar-highlight\",\n\t\t\t\tstyle: {\n\t\t\t\t\t\"--search-highlight-top\": `${y}px`,\n\t\t\t\t\t\"--search-highlight-height\": `${height}px`,\n\t\t\t\t},\n\t\t\t})\n\t\t);\n\t}\n}\n\nfunction isInViewport(element) {\n\tconst rect = element.getBoundingClientRect();\n\treturn (\n\t\trect.top >= 0 &&\n\t\trect.left >= 0 &&\n\t\trect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n\t\trect.right <= (window.innerWidth || document.documentElement.clientWidth)\n\t);\n}\n\nconst SyncedExpandedLyricsPage = react.memo(({ lyrics, provider, copyright, isKara }) => {\n\tconst [position, setPosition] = useState(0);\n\tconst activeLineRef = useRef(null);\n\tconst pageRef = useRef(null);\n\n\tuseTrackPosition(() => {\n\t\tif (!Spicetify.Player.data.is_paused) {\n\t\t\tsetPosition(Spicetify.Player.getProgress() + CONFIG.visual[\"global-delay\"] + CONFIG.visual.delay);\n\t\t}\n\t});\n\n\tconst padded = useMemo(() => [emptyLine, ...lyrics], [lyrics]);\n\n\tconst intialScroll = useMemo(() => [false], [lyrics]);\n\n\tconst lyricsId = lyrics[0].text;\n\n\tlet activeLineIndex = 0;\n\tfor (let i = padded.length - 1; i >= 0; i--) {\n\t\tconst line = padded[i];\n\t\tif (position >= line.startTime) {\n\t\t\tactiveLineIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tuseEffect(() => {\n\t\tif (activeLineRef.current && (!intialScroll[0] || isInViewport(activeLineRef.current))) {\n\t\t\tactiveLineRef.current.scrollIntoView({\n\t\t\t\tbehavior: \"smooth\",\n\t\t\t\tblock: \"center\",\n\t\t\t\tinline: \"nearest\",\n\t\t\t});\n\t\t\tintialScroll[0] = true;\n\t\t}\n\t}, [activeLineRef.current]);\n\n\treturn react.createElement(\n\t\t\"div\",\n\t\t{\n\t\t\tclassName: \"lyrics-lyricsContainer-UnsyncedLyricsPage\",\n\t\t\tkey: lyricsId,\n\t\t\tref: pageRef,\n\t\t},\n\t\treact.createElement(\"p\", {\n\t\t\tclassName: \"lyrics-lyricsContainer-LyricsUnsyncedPadding\",\n\t\t}),\n\t\tpadded.map(({ text, startTime, endTime, originalText, performer }, i) => {\n\t\t\tif (i === 0) {\n\t\t\t\treturn react.createElement(IdlingIndicator, {\n\t\t\t\t\tisActive: activeLineIndex === 0,\n\t\t\t\t\tprogress: position / padded[1].startTime,\n\t\t\t\t\tdelay: padded[1].startTime / 3,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst isActive = i === activeLineIndex;\n\t\t\tconst showTranslatedBelow = CONFIG.visual[\"translate:display-mode\"] === \"below\";\n\t\t\t// If we have original text and we are showing translated below, we should show the original text\n\t\t\t// Otherwise we should show the translated text\n\t\t\tconst lineText = originalText && showTranslatedBelow ? originalText : text;\n\n\t\t\t// Convert lyrics to text for comparison\n\t\t\tconst belowOrigin = (typeof originalText === \"object\" ? originalText?.props?.children?.[0] : originalText)?.replace(/\\s+/g, \"\");\n\t\t\tconst belowTxt = (typeof text === \"object\" ? text?.props?.children?.[0] : text)?.replace(/\\s+/g, \"\");\n\n\t\t\tconst belowMode = showTranslatedBelow && originalText && belowOrigin !== belowTxt;\n\n\t\t\treturn react.createElement(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tclassName: `lyrics-lyricsContainer-LyricsLine${i <= activeLineIndex ? \" lyrics-lyricsContainer-LyricsLine-active\" : \"\"}`,\n\t\t\t\t\tkey: i,\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\tcursor: \"pointer\",\n\t\t\t\t\t},\n\t\t\t\t\tdir: \"auto\",\n\t\t\t\t\tref: isActive ? activeLineRef : null,\n\t\t\t\t\tonClick: (event) => {\n\t\t\t\t\t\tif (startTime) {\n\t\t\t\t\t\t\tSpicetify.Player.seek(startTime);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\treact.createElement(\n\t\t\t\t\t\"p\",\n\t\t\t\t\t{\n\t\t\t\t\t\tonContextMenu: (event) => {\n\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t\tSpicetify.Platform.ClipboardAPI.copy(Utils.convertParsedToLRC(lyrics, belowMode).original)\n\t\t\t\t\t\t\t\t.then(() => Spicetify.showNotification(\"Lyrics copied to clipboard\"))\n\t\t\t\t\t\t\t\t.catch(() => Spicetify.showNotification(\"Failed to copy lyrics to clipboard\"));\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t(() => {\n\t\t\t\t\t\tif (!CONFIG.visual[\"show-performers\"] || !performer) return null;\n\n\t\t\t\t\t\tif (!CONFIG.visual[\"synced-compact\"]) {\n\t\t\t\t\t\t\tconst previousLine = padded[i - 1];\n\t\t\t\t\t\t\tif (previousLine && previousLine.performer === performer) return null;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn react.createElement(\n\t\t\t\t\t\t\t\"span\",\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tclassName: \"lyrics-lyricsContainer-Performer\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tperformer\n\t\t\t\t\t\t);\n\t\t\t\t\t})(),\n\t\t\t\t\t!isKara ? lineText : react.createElement(KaraokeLine, { text, startTime, endTime, position, isActive })\n\t\t\t\t),\n\t\t\t\tbelowMode &&\n\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\"p\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tstyle: { opacity: 0.5 },\n\t\t\t\t\t\t\tonContextMenu: (event) => {\n\t\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t\t\tSpicetify.Platform.ClipboardAPI.copy(Utils.convertParsedToLRC(lyrics, belowMode).conver)\n\t\t\t\t\t\t\t\t\t.then(() => Spicetify.showNotification(\"Translated lyrics copied to clipboard\"))\n\t\t\t\t\t\t\t\t\t.catch(() => Spicetify.showNotification(\"Failed to copy translated lyrics to clipboard\"));\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\ttext\n\t\t\t\t\t)\n\t\t\t);\n\t\t}),\n\t\treact.createElement(\"p\", {\n\t\t\tclassName: \"lyrics-lyricsContainer-LyricsUnsyncedPadding\",\n\t\t}),\n\t\treact.createElement(CreditFooter, {\n\t\t\tprovider,\n\t\t\tcopyright,\n\t\t}),\n\t\treact.createElement(SearchBar, null)\n\t);\n});\n\nconst UnsyncedLyricsPage = react.memo(({ lyrics, provider, copyright }) => {\n\treturn react.createElement(\n\t\t\"div\",\n\t\t{\n\t\t\tclassName: \"lyrics-lyricsContainer-UnsyncedLyricsPage\",\n\t\t},\n\t\treact.createElement(\"p\", {\n\t\t\tclassName: \"lyrics-lyricsContainer-LyricsUnsyncedPadding\",\n\t\t}),\n\t\tlyrics.map(({ text, originalText, performer }, index) => {\n\t\t\tconst showTranslatedBelow = CONFIG.visual[\"translate:display-mode\"] === \"below\";\n\t\t\t// If we have original text and we are showing translated below, we should show the original text\n\t\t\t// Otherwise we should show the translated text\n\t\t\tconst lineText = originalText && showTranslatedBelow ? originalText : text;\n\n\t\t\t// Convert lyrics to text for comparison\n\t\t\tconst belowOrigin = (typeof originalText === \"object\" ? originalText?.props?.children?.[0] : originalText)?.replace(/\\s+/g, \"\");\n\t\t\tconst belowTxt = (typeof text === \"object\" ? text?.props?.children?.[0] : text)?.replace(/\\s+/g, \"\");\n\n\t\t\tconst belowMode = showTranslatedBelow && originalText && belowOrigin !== belowTxt;\n\n\t\t\treturn react.createElement(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tclassName: \"lyrics-lyricsContainer-LyricsLine lyrics-lyricsContainer-LyricsLine-active\",\n\t\t\t\t\tkey: index,\n\t\t\t\t\tdir: \"auto\",\n\t\t\t\t},\n\t\t\t\treact.createElement(\n\t\t\t\t\t\"p\",\n\t\t\t\t\t{\n\t\t\t\t\t\tonContextMenu: (event) => {\n\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t\tSpicetify.Platform.ClipboardAPI.copy(Utils.convertParsedToUnsynced(lyrics, belowMode).original)\n\t\t\t\t\t\t\t\t.then(() => Spicetify.showNotification(\"Lyrics copied to clipboard\"))\n\t\t\t\t\t\t\t\t.catch(() => Spicetify.showNotification(\"Failed to copy lyrics to clipboard\"));\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t(() => {\n\t\t\t\t\t\tif (!CONFIG.visual[\"show-performers\"] || !performer) return null;\n\n\t\t\t\t\t\tconst previousLine = lyrics[index - 1];\n\t\t\t\t\t\tif (previousLine && previousLine.performer === performer) return null;\n\n\t\t\t\t\t\treturn react.createElement(\n\t\t\t\t\t\t\t\"span\",\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tclassName: \"lyrics-lyricsContainer-Performer\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tperformer\n\t\t\t\t\t\t);\n\t\t\t\t\t})(),\n\t\t\t\t\tlineText\n\t\t\t\t),\n\t\t\t\tbelowMode &&\n\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\"p\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tstyle: { opacity: 0.5 },\n\t\t\t\t\t\t\tonContextMenu: (event) => {\n\t\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t\t\tSpicetify.Platform.ClipboardAPI.copy(Utils.convertParsedToUnsynced(lyrics, belowMode).conver)\n\t\t\t\t\t\t\t\t\t.then(() => Spicetify.showNotification(\"Translated lyrics copied to clipboard\"))\n\t\t\t\t\t\t\t\t\t.catch(() => Spicetify.showNotification(\"Failed to copy translated lyrics to clipboard\"));\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\ttext\n\t\t\t\t\t)\n\t\t\t);\n\t\t}),\n\t\treact.createElement(\"p\", {\n\t\t\tclassName: \"lyrics-lyricsContainer-LyricsUnsyncedPadding\",\n\t\t}),\n\t\treact.createElement(CreditFooter, {\n\t\t\tprovider,\n\t\t\tcopyright,\n\t\t}),\n\t\treact.createElement(SearchBar, null)\n\t);\n});\n\nconst noteContainer = document.createElement(\"div\");\nnoteContainer.classList.add(\"lyrics-Genius-noteContainer\");\nconst noteDivider = document.createElement(\"div\");\nnoteDivider.classList.add(\"lyrics-Genius-divider\");\nnoteDivider.innerHTML = `<svg width=\"32\" height=\"32\" viewBox=\"0 0 13 4\" fill=\"currentColor\"><path d=\\\"M13 10L8 4.206 3 10z\\\"/></svg>`;\nnoteDivider.style.setProperty(\"--link-left\", 0);\nconst noteTextContainer = document.createElement(\"div\");\nnoteTextContainer.classList.add(\"lyrics-Genius-noteTextContainer\");\nnoteTextContainer.onclick = (event) => {\n\tevent.preventDefault();\n\tevent.stopPropagation();\n};\nnoteContainer.append(noteDivider, noteTextContainer);\n\nfunction showNote(parent, note) {\n\tif (noteContainer.parentElement === parent) {\n\t\tnoteContainer.remove();\n\t\treturn;\n\t}\n\tnoteTextContainer.innerText = note;\n\tparent.append(noteContainer);\n\tconst arrowPos = parent.offsetLeft - noteContainer.offsetLeft;\n\tnoteDivider.style.setProperty(\"--link-left\", `${arrowPos}px`);\n\tconst box = noteTextContainer.getBoundingClientRect();\n\tif (box.y + box.height > window.innerHeight) {\n\t\t// Wait for noteContainer is mounted\n\t\tsetTimeout(() => {\n\t\t\tnoteContainer.scrollIntoView({\n\t\t\t\tbehavior: \"smooth\",\n\t\t\t\tblock: \"center\",\n\t\t\t\tinline: \"nearest\",\n\t\t\t});\n\t\t}, 50);\n\t}\n}\n\nconst GeniusPage = react.memo(\n\t({ lyrics, provider, copyright, versions, versionIndex, onVersionChange, isSplitted, lyrics2, versionIndex2, onVersionChange2 }) => {\n\t\tlet notes = {};\n\t\tlet container = null;\n\t\tlet container2 = null;\n\n\t\t// Fetch notes\n\t\tuseEffect(() => {\n\t\t\tif (!container) return;\n\t\t\tnotes = {};\n\t\t\tlet links = container.querySelectorAll(\"a\");\n\t\t\tif (isSplitted && container2) {\n\t\t\t\tlinks = [...links, ...container2.querySelectorAll(\"a\")];\n\t\t\t}\n\t\t\tfor (const link of links) {\n\t\t\t\tlet id = link.pathname.match(/\\/(\\d+)\\//);\n\t\t\t\tif (!id) {\n\t\t\t\t\tid = link.dataset.id;\n\t\t\t\t} else {\n\t\t\t\t\tid = id[1];\n\t\t\t\t}\n\t\t\t\tProviderGenius.getNote(id).then((note) => {\n\t\t\t\t\tnotes[id] = note;\n\t\t\t\t\tlink.classList.add(\"fetched\");\n\t\t\t\t});\n\t\t\t\tlink.onclick = (event) => {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tif (!notes[id]) return;\n\t\t\t\t\tshowNote(link, notes[id]);\n\t\t\t\t};\n\t\t\t}\n\t\t}, [lyrics, lyrics2]);\n\n\t\tconst lyricsEl1 = react.createElement(\n\t\t\t\"div\",\n\t\t\tnull,\n\t\t\treact.createElement(VersionSelector, { items: versions, index: versionIndex, callback: onVersionChange }),\n\t\t\treact.createElement(\"div\", {\n\t\t\t\tclassName: \"lyrics-lyricsContainer-LyricsLine lyrics-lyricsContainer-LyricsLine-active\",\n\t\t\t\tref: (c) => {\n\t\t\t\t\tcontainer = c;\n\t\t\t\t},\n\t\t\t\tdangerouslySetInnerHTML: {\n\t\t\t\t\t__html: lyrics,\n\t\t\t\t},\n\t\t\t\tonContextMenu: (event) => {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tconst copylyrics = lyrics.replace(/<br>/g, \"\\n\").replace(/<[^>]*>/g, \"\");\n\t\t\t\t\tSpicetify.Platform.ClipboardAPI.copy(copylyrics)\n\t\t\t\t\t\t.then(() => Spicetify.showNotification(\"Lyrics copied to clipboard\"))\n\t\t\t\t\t\t.catch(() => Spicetify.showNotification(\"Failed to copy lyrics to clipboard\"));\n\t\t\t\t},\n\t\t\t})\n\t\t);\n\n\t\tconst mainContainer = [lyricsEl1];\n\t\tconst shouldSplit = versions.length > 1 && isSplitted;\n\n\t\tif (shouldSplit) {\n\t\t\tconst lyricsEl2 = react.createElement(\n\t\t\t\t\"div\",\n\t\t\t\tnull,\n\t\t\t\treact.createElement(VersionSelector, { items: versions, index: versionIndex2, callback: onVersionChange2 }),\n\t\t\t\treact.createElement(\"div\", {\n\t\t\t\t\tclassName: \"lyrics-lyricsContainer-LyricsLine lyrics-lyricsContainer-LyricsLine-active\",\n\t\t\t\t\tref: (c) => {\n\t\t\t\t\t\tcontainer2 = c;\n\t\t\t\t\t},\n\t\t\t\t\tdangerouslySetInnerHTML: {\n\t\t\t\t\t\t__html: lyrics2,\n\t\t\t\t\t},\n\t\t\t\t\tonContextMenu: (event) => {\n\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\tconst copylyrics = lyrics.replace(/<br>/g, \"\\n\").replace(/<[^>]*>/g, \"\");\n\t\t\t\t\t\tSpicetify.Platform.ClipboardAPI.copy(copylyrics)\n\t\t\t\t\t\t\t.then(() => Spicetify.showNotification(\"Lyrics copied to clipboard\"))\n\t\t\t\t\t\t\t.catch(() => Spicetify.showNotification(\"Failed to copy lyrics to clipboard\"));\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t);\n\t\t\tmainContainer.push(lyricsEl2);\n\t\t}\n\n\t\treturn react.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tclassName: \"lyrics-lyricsContainer-UnsyncedLyricsPage\",\n\t\t\t},\n\t\t\treact.createElement(\"p\", {\n\t\t\t\tclassName: \"lyrics-lyricsContainer-LyricsUnsyncedPadding main-type-ballad\",\n\t\t\t}),\n\t\t\treact.createElement(\"div\", { className: shouldSplit ? \"split\" : \"\" }, mainContainer),\n\t\t\treact.createElement(CreditFooter, {\n\t\t\t\tprovider,\n\t\t\t\tcopyright,\n\t\t\t}),\n\t\t\treact.createElement(SearchBar, null)\n\t\t);\n\t}\n);\n\nconst LoadingIcon = react.createElement(\n\t\"svg\",\n\t{\n\t\twidth: \"200px\",\n\t\theight: \"200px\",\n\t\tviewBox: \"0 0 100 100\",\n\t\tpreserveAspectRatio: \"xMidYMid\",\n\t},\n\treact.createElement(\n\t\t\"circle\",\n\t\t{\n\t\t\tcx: \"50\",\n\t\t\tcy: \"50\",\n\t\t\tr: \"0\",\n\t\t\tfill: \"none\",\n\t\t\tstroke: \"currentColor\",\n\t\t\t\"stroke-width\": \"2\",\n\t\t},\n\t\treact.createElement(\"animate\", {\n\t\t\tattributeName: \"r\",\n\t\t\trepeatCount: \"indefinite\",\n\t\t\tdur: \"1s\",\n\t\t\tvalues: \"0;40\",\n\t\t\tkeyTimes: \"0;1\",\n\t\t\tkeySplines: \"0 0.2 0.8 1\",\n\t\t\tcalcMode: \"spline\",\n\t\t\tbegin: \"0s\",\n\t\t}),\n\t\treact.createElement(\"animate\", {\n\t\t\tattributeName: \"opacity\",\n\t\t\trepeatCount: \"indefinite\",\n\t\t\tdur: \"1s\",\n\t\t\tvalues: \"1;0\",\n\t\t\tkeyTimes: \"0;1\",\n\t\t\tkeySplines: \"0.2 0 0.8 1\",\n\t\t\tcalcMode: \"spline\",\n\t\t\tbegin: \"0s\",\n\t\t})\n\t),\n\treact.createElement(\n\t\t\"circle\",\n\t\t{\n\t\t\tcx: \"50\",\n\t\t\tcy: \"50\",\n\t\t\tr: \"0\",\n\t\t\tfill: \"none\",\n\t\t\tstroke: \"currentColor\",\n\t\t\t\"stroke-width\": \"2\",\n\t\t},\n\t\treact.createElement(\"animate\", {\n\t\t\tattributeName: \"r\",\n\t\t\trepeatCount: \"indefinite\",\n\t\t\tdur: \"1s\",\n\t\t\tvalues: \"0;40\",\n\t\t\tkeyTimes: \"0;1\",\n\t\t\tkeySplines: \"0 0.2 0.8 1\",\n\t\t\tcalcMode: \"spline\",\n\t\t\tbegin: \"-0.5s\",\n\t\t}),\n\t\treact.createElement(\"animate\", {\n\t\t\tattributeName: \"opacity\",\n\t\t\trepeatCount: \"indefinite\",\n\t\t\tdur: \"1s\",\n\t\t\tvalues: \"1;0\",\n\t\t\tkeyTimes: \"0;1\",\n\t\t\tkeySplines: \"0.2 0 0.8 1\",\n\t\t\tcalcMode: \"spline\",\n\t\t\tbegin: \"-0.5s\",\n\t\t})\n\t)\n);\n\nconst VersionSelector = react.memo(({ items, index, callback }) => {\n\tif (items.length < 2) {\n\t\treturn null;\n\t}\n\treturn react.createElement(\n\t\t\"div\",\n\t\t{\n\t\t\tclassName: \"lyrics-versionSelector\",\n\t\t},\n\t\treact.createElement(\n\t\t\t\"select\",\n\t\t\t{\n\t\t\t\tonChange: (event) => {\n\t\t\t\t\tcallback(items, event.target.value);\n\t\t\t\t},\n\t\t\t\tvalue: index,\n\t\t\t},\n\t\t\titems.map((a, i) => {\n\t\t\t\treturn react.createElement(\"option\", { value: i }, a.title);\n\t\t\t})\n\t\t),\n\t\treact.createElement(\n\t\t\t\"svg\",\n\t\t\t{\n\t\t\t\theight: \"16\",\n\t\t\t\twidth: \"16\",\n\t\t\t\tfill: \"currentColor\",\n\t\t\t\tviewBox: \"0 0 16 16\",\n\t\t\t},\n\t\t\treact.createElement(\"path\", {\n\t\t\t\td: \"M3 6l5 5.794L13 6z\",\n\t\t\t})\n\t\t)\n\t);\n});\n"
  },
  {
    "path": "CustomApps/lyrics-plus/PlaybarButton.js",
    "content": "(function PlaybarButton() {\n\tif (!Spicetify.Platform.History) {\n\t\tsetTimeout(PlaybarButton, 300);\n\t\treturn;\n\t}\n\n\tconst button = new Spicetify.Playbar.Button(\n\t\t\"Lyrics Plus\",\n\t\t`<svg role=\"img\" height=\"16\" width=\"16\" aria-hidden=\"true\" viewBox=\"0 0 16 16\" data-encore-id=\"icon\" fill=\"currentColor\"><path d=\"M13.426 2.574a2.831 2.831 0 0 0-4.797 1.55l3.247 3.247a2.831 2.831 0 0 0 1.55-4.797zM10.5 8.118l-2.619-2.62A63303.13 63303.13 0 0 0 4.74 9.075L2.065 12.12a1.287 1.287 0 0 0 1.816 1.816l3.06-2.688 3.56-3.129zM7.12 4.094a4.331 4.331 0 1 1 4.786 4.786l-3.974 3.493-3.06 2.689a2.787 2.787 0 0 1-3.933-3.933l2.676-3.045 3.505-3.99z\"></path></svg>`,\n\t\t() =>\n\t\t\tSpicetify.Platform.History.location.pathname !== \"/lyrics-plus\"\n\t\t\t\t? Spicetify.Platform.History.push(\"/lyrics-plus\")\n\t\t\t\t: Spicetify.Platform.History.goBack(),\n\t\tfalse,\n\t\tSpicetify.Platform.History.location.pathname === \"/lyrics-plus\",\n\t\tfalse\n\t);\n\n\tconst style = document.createElement(\"style\");\n\tstyle.innerHTML = `\n\t\t.main-nowPlayingBar-lyricsButton[data-testid=\"lyrics-button\"] {\n\t\t\tdisplay: none !important;\n\t\t}\n\t\tli[data-id=\"/lyrics-plus\"] {\n\t\t\tdisplay: none;\n\t\t}\n\t`;\n\tstyle.classList.add(\"lyrics-plus:visual:playbar-button\");\n\n\tif (Spicetify.LocalStorage.get(\"lyrics-plus:visual:playbar-button\") === \"true\") setPlaybarButton();\n\twindow.addEventListener(\"lyrics-plus\", (event) => {\n\t\tif (event.detail?.name === \"playbar-button\") event.detail.value ? setPlaybarButton() : removePlaybarButton();\n\t});\n\n\tSpicetify.Platform.History.listen((location) => {\n\t\tbutton.active = location.pathname === \"/lyrics-plus\";\n\t});\n\n\tfunction setPlaybarButton() {\n\t\tdocument.head.appendChild(style);\n\t\tbutton.register();\n\t}\n\n\tfunction removePlaybarButton() {\n\t\tstyle.remove();\n\t\tbutton.deregister();\n\t}\n})();\n"
  },
  {
    "path": "CustomApps/lyrics-plus/ProviderGenius.js",
    "content": "const ProviderGenius = (() => {\n\tfunction getChildDeep(parent, isDeep = false) {\n\t\tlet acc = \"\";\n\n\t\tif (!parent.children) {\n\t\t\treturn acc;\n\t\t}\n\n\t\tfor (const child of parent.children) {\n\t\t\tif (typeof child === \"string\") {\n\t\t\t\tacc += child;\n\t\t\t} else if (child.children) {\n\t\t\t\tacc += getChildDeep(child, true);\n\t\t\t}\n\t\t\tif (!isDeep) {\n\t\t\t\tacc += \"\\n\";\n\t\t\t}\n\t\t}\n\t\treturn acc.trim();\n\t}\n\n\tasync function getNote(id) {\n\t\tconst body = await Spicetify.CosmosAsync.get(`https://genius.com/api/annotations/${id}`);\n\t\tconst response = body.response;\n\t\tlet note = \"\";\n\n\t\t// Authors annotations\n\t\tif (response.referent && response.referent.classification === \"verified\") {\n\t\t\tconst referentsBody = await Spicetify.CosmosAsync.get(`https://genius.com/api/referents/${id}`);\n\t\t\tconst referents = referentsBody.response;\n\t\t\tfor (const ref of referents.referent.annotations) {\n\t\t\t\tnote += getChildDeep(ref.body.dom);\n\t\t\t}\n\t\t}\n\n\t\t// Users annotations\n\t\tif (!note && response.annotation) {\n\t\t\tnote = getChildDeep(response.annotation.body.dom);\n\t\t}\n\n\t\t// Users comments\n\t\tif (!note && response.annotation && response.annotation.top_comment) {\n\t\t\tnote += getChildDeep(response.annotation.top_comment.body.dom);\n\t\t}\n\t\tnote = note.replace(/\\n\\n\\n?/, \"\\n\");\n\n\t\treturn note;\n\t}\n\n\tfunction fetchHTML(url) {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst request = JSON.stringify({\n\t\t\t\tmethod: \"GET\",\n\t\t\t\turi: url,\n\t\t\t});\n\n\t\t\twindow.sendCosmosRequest({\n\t\t\t\trequest,\n\t\t\t\tpersistent: false,\n\t\t\t\tonSuccess: resolve,\n\t\t\t\tonFailure: reject,\n\t\t\t});\n\t\t});\n\t}\n\n\tasync function fetchLyricsVersion(results, index) {\n\t\tconst result = results[index];\n\t\tif (!result) {\n\t\t\tconsole.warn(result);\n\t\t\treturn;\n\t\t}\n\n\t\tconst site = await fetchHTML(result.url);\n\t\tconst body = JSON.parse(site)?.body;\n\t\tif (!body) {\n\t\t\treturn null;\n\t\t}\n\n\t\tlet lyrics = \"\";\n\t\tconst parser = new DOMParser();\n\t\tconst htmlDoc = parser.parseFromString(body, \"text/html\");\n\t\tconst lyricsDiv = htmlDoc.querySelectorAll('div[data-lyrics-container=\"true\"]');\n\n\t\tfor (const i of lyricsDiv) {\n\t\t\tlyrics += `${i.innerHTML}<br>`;\n\t\t}\n\n\t\tif (!lyrics?.length) {\n\t\t\tconsole.warn(\"forceError\");\n\t\t\treturn null;\n\t\t}\n\n\t\treturn lyrics;\n\t}\n\n\tasync function fetchLyrics(info) {\n\t\tconst titles = new Set([info.title]);\n\n\t\tconst titleNoExtra = Utils.removeExtraInfo(info.title);\n\t\ttitles.add(titleNoExtra);\n\t\ttitles.add(Utils.removeSongFeat(info.title));\n\t\ttitles.add(Utils.removeSongFeat(titleNoExtra));\n\n\t\tlet lyrics;\n\t\tlet hits;\n\t\tfor (const title of titles) {\n\t\t\tconst query = new URLSearchParams({ per_page: 20, q: `${info.artist} ${title}` });\n\t\t\tconst url = `https://genius.com/api/search/song?${query.toString()}`;\n\n\t\t\tconst geniusSearch = await Spicetify.CosmosAsync.get(url);\n\n\t\t\thits = geniusSearch.response.sections[0].hits.map((item) => ({\n\t\t\t\ttitle: item.result.full_title,\n\t\t\t\turl: item.result.url,\n\t\t\t}));\n\n\t\t\tif (!hits.length) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlyrics = await fetchLyricsVersion(hits, 0);\n\t\t\tbreak;\n\t\t}\n\n\t\tif (!lyrics) {\n\t\t\treturn { lyrics: null, versions: [] };\n\t\t}\n\n\t\treturn { lyrics, versions: hits };\n\t}\n\n\treturn { fetchLyrics, getNote, fetchLyricsVersion };\n})();\n"
  },
  {
    "path": "CustomApps/lyrics-plus/ProviderLRCLIB.js",
    "content": "const ProviderLRCLIB = (() => {\n\tasync function findLyrics(info) {\n\t\tconst baseURL = \"https://lrclib.net/api/get\";\n\t\tconst durr = info.duration / 1000;\n\t\tconst params = {\n\t\t\ttrack_name: info.title,\n\t\t\tartist_name: info.artist,\n\t\t\talbum_name: info.album,\n\t\t\tduration: durr,\n\t\t};\n\n\t\tconst finalURL = `${baseURL}?${Object.keys(params)\n\t\t\t.map((key) => `${key}=${encodeURIComponent(params[key])}`)\n\t\t\t.join(\"&\")}`;\n\n\t\tconst body = await fetch(finalURL, {\n\t\t\theaders: {\n\t\t\t\t\"x-user-agent\": `spicetify v${Spicetify.Config.version} (https://github.com/spicetify/cli)`,\n\t\t\t},\n\t\t});\n\n\t\tif (body.status !== 200) {\n\t\t\treturn {\n\t\t\t\terror: \"Request error: Track wasn't found\",\n\t\t\t\turi: info.uri,\n\t\t\t};\n\t\t}\n\n\t\treturn await body.json();\n\t}\n\n\tfunction getUnsynced(body) {\n\t\tconst unsyncedLyrics = body?.plainLyrics;\n\t\tconst isInstrumental = body.instrumental;\n\t\tif (isInstrumental) return [{ text: \"♪ Instrumental ♪\" }];\n\n\t\tif (!unsyncedLyrics) return null;\n\n\t\treturn Utils.parseLocalLyrics(unsyncedLyrics).unsynced;\n\t}\n\n\tfunction getSynced(body) {\n\t\tconst syncedLyrics = body?.syncedLyrics;\n\t\tconst isInstrumental = body.instrumental;\n\t\tif (isInstrumental) return [{ text: \"♪ Instrumental ♪\" }];\n\n\t\tif (!syncedLyrics) return null;\n\n\t\treturn Utils.parseLocalLyrics(syncedLyrics).synced;\n\t}\n\n\treturn { findLyrics, getSynced, getUnsynced };\n})();\n"
  },
  {
    "path": "CustomApps/lyrics-plus/ProviderMusixmatch.js",
    "content": "const ProviderMusixmatch = (() => {\n\tconst headers = {\n\t\tauthority: \"apic-desktop.musixmatch.com\",\n\t\tcookie: \"x-mxm-token-guid=\",\n\t};\n\n\tfunction findTranslationStatus(body) {\n\t\tif (!body || typeof body !== \"object\") {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (Array.isArray(body)) {\n\t\t\tfor (const item of body) {\n\t\t\t\tconst result = findTranslationStatus(item);\n\t\t\t\tif (result) {\n\t\t\t\t\treturn result;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn null;\n\t\t}\n\n\t\tif (Array.isArray(body.track_lyrics_translation_status)) {\n\t\t\treturn body.track_lyrics_translation_status;\n\t\t}\n\n\t\tfor (const value of Object.values(body)) {\n\t\t\tconst result = findTranslationStatus(value);\n\t\t\tif (result) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tasync function findLyrics(info) {\n\t\tconst baseURL =\n\t\t\t\"https://apic-desktop.musixmatch.com/ws/1.1/macro.subtitles.get?format=json&namespace=lyrics_richsynched&subtitle_format=mxm&app_id=web-desktop-app-v1.0&\";\n\n\t\tconst durr = info.duration / 1000;\n\n\t\tconst params = {\n\t\t\tq_album: info.album,\n\t\t\tq_artist: info.artist,\n\t\t\tq_artists: info.artist,\n\t\t\tq_track: info.title,\n\t\t\ttrack_spotify_id: info.uri,\n\t\t\tq_duration: durr,\n\t\t\tf_subtitle_length: Math.floor(durr),\n\t\t\tusertoken: CONFIG.providers.musixmatch.token,\n\t\t\tpart: \"track_lyrics_translation_status,track_structure,track_performer_tagging\",\n\t\t};\n\n\t\tconst finalURL =\n\t\t\tbaseURL +\n\t\t\tObject.keys(params)\n\t\t\t\t.map((key) => `${key}=${encodeURIComponent(params[key])}`)\n\t\t\t\t.join(\"&\");\n\n\t\tlet body = await Spicetify.CosmosAsync.get(finalURL, null, headers);\n\n\t\tbody = body.message.body.macro_calls;\n\n\t\tif (body[\"matcher.track.get\"].message.header.status_code !== 200) {\n\t\t\treturn {\n\t\t\t\terror: `Requested error: ${body[\"matcher.track.get\"].message.header.mode}`,\n\t\t\t\turi: info.uri,\n\t\t\t};\n\t\t}\n\t\tif (body[\"track.lyrics.get\"]?.message?.body?.lyrics?.restricted) {\n\t\t\treturn {\n\t\t\t\terror: \"Unfortunately we're not authorized to show these lyrics.\",\n\t\t\t\turi: info.uri,\n\t\t\t};\n\t\t}\n\n\t\tconst translationStatus = findTranslationStatus(body);\n\t\tconst meta = body?.[\"matcher.track.get\"]?.message?.body;\n\t\tconst availableTranslations = Array.isArray(translationStatus) ? [...new Set(translationStatus.map((status) => status?.to).filter(Boolean))] : [];\n\n\t\tObject.defineProperties(body, {\n\t\t\t__musixmatchTranslationStatus: {\n\t\t\t\tvalue: availableTranslations,\n\t\t\t},\n\t\t\t__musixmatchTrackId: {\n\t\t\t\tvalue: meta?.track?.track_id ?? null,\n\t\t\t},\n\t\t});\n\n\t\treturn body;\n\t}\n\n\tfunction parsePerformerData(meta) {\n\t\tif (!meta || !meta.track || !meta.track.performer_tagging) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst tagging = meta.track.performer_tagging;\n\t\tconst miscTags = meta.track.performer_tagging_misc_tags || {};\n\t\tlet performerMap = [];\n\t\tif (tagging && tagging.content && tagging.content.length > 0) {\n\t\t\tconst resources = tagging.resources?.artists || [];\n\t\t\tconst resourcesList = Array.isArray(resources) ? resources : Object.values(resources);\n\n\t\t\tperformerMap = tagging.content\n\t\t\t\t.map((c) => {\n\t\t\t\t\tif (!c.performers || c.performers.length === 0) return null;\n\n\t\t\t\t\tconst resolvedPerformers = c.performers\n\t\t\t\t\t\t.map((p) => {\n\t\t\t\t\t\t\tlet name = \"Unknown\";\n\t\t\t\t\t\t\tif (p.type === \"artist\") {\n\t\t\t\t\t\t\t\tconst fqid = p.fqid;\n\t\t\t\t\t\t\t\tconst idFromFqid = fqid ? parseInt(fqid.split(\":\")[2]) : null;\n\n\t\t\t\t\t\t\t\tconst artist = resourcesList.find((r) => r.artist_id === idFromFqid);\n\t\t\t\t\t\t\t\tif (artist) name = artist.artist_name;\n\t\t\t\t\t\t\t} else if (miscTags[p.type]) {\n\t\t\t\t\t\t\t\tname = miscTags[p.type];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tfqid: p.fqid,\n\t\t\t\t\t\t\t\tartist_id: p.fqid ? parseInt(p.fqid.split(\":\")[2]) : null,\n\t\t\t\t\t\t\t\tname: name,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.filter((p) => p.name !== \"Unknown\");\n\n\t\t\t\t\tconst names = resolvedPerformers.map((p) => p.name);\n\t\t\t\t\tif (names.length === 0) return null;\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\tname: names.join(\", \"),\n\t\t\t\t\t\tsnippet: c.snippet,\n\t\t\t\t\t\tperformers: resolvedPerformers,\n\t\t\t\t\t};\n\t\t\t\t})\n\t\t\t\t.filter(Boolean);\n\t\t}\n\n\t\tconst normalizeForMatch = (text) => text.replace(/\\s+/g, \"\").toLowerCase();\n\n\t\tconst snippetQueue = [];\n\t\tif (performerMap.length > 0) {\n\t\t\tfor (const tag of performerMap) {\n\t\t\t\tif (!tag.snippet) continue;\n\t\t\t\tconst snippetLines = tag.snippet\n\t\t\t\t\t.split(/\\n+/)\n\t\t\t\t\t.map((s) => s.trim())\n\t\t\t\t\t.filter(Boolean);\n\t\t\t\tfor (const sLine of snippetLines) {\n\t\t\t\t\tif (sLine.length < 2 && !/^[\\u3131-\\uD79D]/.test(sLine)) continue;\n\t\t\t\t\tsnippetQueue.push({\n\t\t\t\t\t\ttext: normalizeForMatch(sLine),\n\t\t\t\t\t\traw: sLine,\n\t\t\t\t\t\tperformers: tag.performers,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn snippetQueue;\n\t}\n\n\tfunction matchSequential(lyricsLines, snippetQueue, getTextCallback = (l) => l.text) {\n\t\tif (!snippetQueue || snippetQueue.length === 0) return lyricsLines;\n\n\t\tconst normalizeForMatch = (text) => text.replace(/\\s+/g, \"\").toLowerCase();\n\t\tlet queueCursor = 0;\n\t\tconst LOOKAHEAD = 5;\n\n\t\treturn lyricsLines.map((line) => {\n\t\t\tconst lineText = getTextCallback(line) || \"♪\";\n\t\t\tlet normalizedLine = normalizeForMatch(lineText);\n\n\t\t\tlet matchedPerformers = [];\n\n\t\t\twhile (queueCursor < snippetQueue.length) {\n\t\t\t\tlet matchFoundAtOffset = -1;\n\n\t\t\t\tfor (let i = 0; i < LOOKAHEAD && queueCursor + i < snippetQueue.length; i++) {\n\t\t\t\t\tconst snippet = snippetQueue[queueCursor + i];\n\n\t\t\t\t\tif (normalizedLine.includes(snippet.text) && snippet.text.length > 0) {\n\t\t\t\t\t\tmatchFoundAtOffset = i;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (matchFoundAtOffset !== -1) {\n\t\t\t\t\tqueueCursor += matchFoundAtOffset;\n\t\t\t\t\tconst matchedSnippet = snippetQueue[queueCursor];\n\t\t\t\t\tmatchedPerformers.push(...matchedSnippet.performers);\n\t\t\t\t\tnormalizedLine = normalizedLine.replace(matchedSnippet.text, \"\");\n\t\t\t\t\tqueueCursor++;\n\t\t\t\t} else {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst uniquePerformers = [];\n\t\t\tconst sawMap = new Set();\n\t\t\tfor (const p of matchedPerformers) {\n\t\t\t\tconst key = p.fqid || p.name;\n\t\t\t\tif (!sawMap.has(key)) {\n\t\t\t\t\tsawMap.add(key);\n\t\t\t\t\tuniquePerformers.push(p);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\t...line,\n\t\t\t\tperformers: uniquePerformers,\n\t\t\t};\n\t\t});\n\t}\n\n\tasync function getKaraoke(body) {\n\t\tconst meta = body?.[\"matcher.track.get\"]?.message?.body;\n\t\tif (!meta) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (!meta.track.has_richsync || meta.track.instrumental) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst baseURL = \"https://apic-desktop.musixmatch.com/ws/1.1/track.richsync.get?format=json&subtitle_format=mxm&app_id=web-desktop-app-v1.0&\";\n\n\t\tconst params = {\n\t\t\tf_subtitle_length: meta.track.track_length,\n\t\t\tq_duration: meta.track.track_length,\n\t\t\tcommontrack_id: meta.track.commontrack_id,\n\t\t\tusertoken: CONFIG.providers.musixmatch.token,\n\t\t};\n\n\t\tconst finalURL =\n\t\t\tbaseURL +\n\t\t\tObject.keys(params)\n\t\t\t\t.map((key) => `${key}=${encodeURIComponent(params[key])}`)\n\t\t\t\t.join(\"&\");\n\n\t\tlet result = await Spicetify.CosmosAsync.get(finalURL, null, headers);\n\n\t\tif (result.message.header.status_code !== 200) {\n\t\t\treturn null;\n\t\t}\n\n\t\tresult = result.message.body;\n\n\t\tconst snippetQueue = parsePerformerData(meta);\n\n\t\tconst parsedKaraoke = JSON.parse(result.richsync.richsync_body).map((line) => {\n\t\t\tconst startTime = line.ts * 1000;\n\t\t\tconst endTime = line.te * 1000;\n\t\t\tconst words = line.l;\n\n\t\t\tconst text = words.map((word, index, words) => {\n\t\t\t\tconst wordText = word.c;\n\t\t\t\tconst wordStartTime = word.o * 1000;\n\t\t\t\tconst nextWordStartTime = words[index + 1]?.o * 1000;\n\n\t\t\t\tconst time = !Number.isNaN(nextWordStartTime) ? nextWordStartTime - wordStartTime : endTime - (wordStartTime + startTime);\n\n\t\t\t\treturn {\n\t\t\t\t\tword: wordText,\n\t\t\t\t\ttime,\n\t\t\t\t};\n\t\t\t});\n\t\t\treturn {\n\t\t\t\tstartTime,\n\t\t\t\tendTime,\n\t\t\t\ttext,\n\t\t\t};\n\t\t});\n\n\t\treturn matchSequential(parsedKaraoke, snippetQueue, (line) => {\n\t\t\tif (Array.isArray(line.text)) {\n\t\t\t\treturn line.text.map((t) => t.word).join(\"\");\n\t\t\t}\n\t\t\treturn line.text;\n\t\t}).map((line) => {\n\t\t\tconst performerNames = (line.performers || [])\n\t\t\t\t.map((p) => p.name)\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.join(\", \");\n\t\t\treturn {\n\t\t\t\t...line,\n\t\t\t\tperformer: performerNames || null,\n\t\t\t};\n\t\t});\n\t}\n\n\tfunction getSynced(body) {\n\t\tconst meta = body?.[\"matcher.track.get\"]?.message?.body;\n\t\tif (!meta) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst hasSynced = meta?.track?.has_subtitles;\n\n\t\tconst isInstrumental = meta?.track?.instrumental;\n\n\t\tif (isInstrumental) {\n\t\t\treturn [{ text: \"♪ Instrumental ♪\", startTime: \"0000\" }];\n\t\t}\n\t\tif (hasSynced) {\n\t\t\tconst subtitle = body[\"track.subtitles.get\"]?.message?.body?.subtitle_list?.[0]?.subtitle;\n\t\t\tif (!subtitle) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst snippetQueue = parsePerformerData(meta);\n\t\t\tconst rawLines = JSON.parse(subtitle.subtitle_body);\n\n\t\t\treturn matchSequential(rawLines, snippetQueue, (l) => l.text).map((line) => {\n\t\t\t\tconst lineText = line.text || \"♪\";\n\t\t\t\tconst performerNames = (line.performers || [])\n\t\t\t\t\t.map((p) => p.name)\n\t\t\t\t\t.filter(Boolean)\n\t\t\t\t\t.join(\", \");\n\n\t\t\t\treturn {\n\t\t\t\t\ttext: lineText,\n\t\t\t\t\tstartTime: line.time.total * 1000,\n\t\t\t\t\tperformer: performerNames || null,\n\t\t\t\t};\n\t\t\t});\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tfunction getUnsynced(body) {\n\t\tconst meta = body?.[\"matcher.track.get\"]?.message?.body;\n\t\tif (!meta) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst hasUnSynced = meta.track.has_lyrics || meta.track.has_lyrics_crowd;\n\n\t\tconst isInstrumental = meta?.track?.instrumental;\n\n\t\tif (isInstrumental) {\n\t\t\treturn [{ text: \"♪ Instrumental ♪\" }];\n\t\t}\n\t\tif (hasUnSynced) {\n\t\t\tconst lyrics = body[\"track.lyrics.get\"]?.message?.body?.lyrics?.lyrics_body;\n\t\t\tif (!lyrics) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst snippetQueue = parsePerformerData(meta);\n\t\t\tconst rawLines = lyrics.split(\"\\n\").map((text) => ({ text }));\n\n\t\t\treturn matchSequential(rawLines, snippetQueue, (l) => l.text).map((line) => {\n\t\t\t\tconst performerNames = (line.performers || [])\n\t\t\t\t\t.map((p) => p.name)\n\t\t\t\t\t.filter(Boolean)\n\t\t\t\t\t.join(\", \");\n\n\t\t\t\treturn {\n\t\t\t\t\t...line,\n\t\t\t\t\tperformer: performerNames || null,\n\t\t\t\t};\n\t\t\t});\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tasync function getTranslation(trackId) {\n\t\tif (!trackId) return null;\n\n\t\tconst selectedLanguage = CONFIG.visual[\"musixmatch-translation-language\"] || \"none\";\n\t\tif (selectedLanguage === \"none\") return null;\n\n\t\tconst baseURL =\n\t\t\t\"https://apic-desktop.musixmatch.com/ws/1.1/crowd.track.translations.get?translation_fields_set=minimal&comment_format=text&format=json&app_id=web-desktop-app-v1.0&\";\n\n\t\tconst params = {\n\t\t\ttrack_id: trackId,\n\t\t\tselected_language: selectedLanguage,\n\t\t\tusertoken: CONFIG.providers.musixmatch.token,\n\t\t};\n\n\t\tconst finalURL =\n\t\t\tbaseURL +\n\t\t\tObject.keys(params)\n\t\t\t\t.map((key) => `${key}=${encodeURIComponent(params[key])}`)\n\t\t\t\t.join(\"&\");\n\n\t\tlet result = await Spicetify.CosmosAsync.get(finalURL, null, headers);\n\n\t\tif (result.message.header.status_code !== 200) return null;\n\n\t\tresult = result.message.body;\n\n\t\tif (!result.translations_list?.length) return null;\n\n\t\treturn result.translations_list.map(({ translation }) => ({\n\t\t\ttranslation: translation.description,\n\t\t\tmatchedLine: translation.matched_line,\n\t\t}));\n\t}\n\n\tlet languageMap = null;\n\tasync function getLanguages() {\n\t\tif (languageMap) return languageMap;\n\n\t\ttry {\n\t\t\tconst cached = localStorage.getItem(\"lyrics-plus:musixmatch-languages\");\n\t\t\tif (cached) {\n\t\t\t\tconst tempMap = JSON.parse(cached);\n\t\t\t\t// Check cache version\n\t\t\t\tif (tempMap.__version === 1) {\n\t\t\t\t\tdelete tempMap.__version;\n\t\t\t\t\tlanguageMap = tempMap;\n\t\t\t\t\treturn languageMap;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tconsole.warn(\"Failed to parse cached languages\", e);\n\t\t}\n\n\t\tconst baseURL = \"https://apic-desktop.musixmatch.com/ws/1.1/languages.get?app_id=web-desktop-app-v1.0&get_romanized_info=1&\";\n\n\t\tconst params = {\n\t\t\tusertoken: CONFIG.providers.musixmatch.token,\n\t\t};\n\n\t\tconst finalURL =\n\t\t\tbaseURL +\n\t\t\tObject.keys(params)\n\t\t\t\t.map((key) => `${key}=${encodeURIComponent(params[key])}`)\n\t\t\t\t.join(\"&\");\n\n\t\ttry {\n\t\t\tlet body = await Spicetify.CosmosAsync.get(finalURL, null, headers);\n\t\t\tif (body?.message?.body?.language_list) {\n\t\t\t\tlanguageMap = {};\n\t\t\t\tbody.message.body.language_list.forEach((item) => {\n\t\t\t\t\tconst lang = item.language;\n\t\t\t\t\tif (lang.language_name) {\n\t\t\t\t\t\tconst name = lang.language_name.charAt(0).toUpperCase() + lang.language_name.slice(1);\n\t\t\t\t\t\tif (lang.language_iso_code_1) languageMap[lang.language_iso_code_1] = name;\n\t\t\t\t\t\tif (lang.language_iso_code_3) languageMap[lang.language_iso_code_3] = name;\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tlocalStorage.setItem(\"lyrics-plus:musixmatch-languages\", JSON.stringify({ ...languageMap, __version: 1 }));\n\t\t\t\treturn languageMap;\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tconsole.error(\"Failed to fetch languages\", e);\n\t\t}\n\t\treturn {};\n\t}\n\n\treturn { findLyrics, getKaraoke, getSynced, getUnsynced, getTranslation, getLanguages };\n})();\n"
  },
  {
    "path": "CustomApps/lyrics-plus/ProviderNetease.js",
    "content": "const ProviderNetease = (() => {\n\tconst requestHeader = {\n\t\t\"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0\",\n\t};\n\n\tasync function findLyrics(info) {\n\t\tconst searchURL = \"https://music.xianqiao.wang/neteaseapiv2/search?limit=10&type=1&keywords=\";\n\t\tconst lyricURL = \"https://music.xianqiao.wang/neteaseapiv2/lyric?id=\";\n\n\t\tconst cleanTitle = Utils.removeExtraInfo(Utils.removeSongFeat(Utils.normalize(info.title)));\n\t\tconst finalURL = searchURL + encodeURIComponent(`${cleanTitle} ${info.artist}`);\n\n\t\tconst searchResults = await Spicetify.CosmosAsync.get(finalURL, null, requestHeader);\n\t\tconst items = searchResults.result.songs;\n\t\tif (!items?.length) {\n\t\t\tthrow \"Cannot find track\";\n\t\t}\n\n\t\t// normalized expected album name\n\t\tconst neAlbumName = Utils.normalize(info.album);\n\t\tconst expectedAlbumName = Utils.containsHanCharacter(neAlbumName) ? await Utils.toSimplifiedChinese(neAlbumName) : neAlbumName;\n\t\tlet itemId = items.findIndex((val) => Utils.normalize(val.album.name) === expectedAlbumName);\n\t\tif (itemId === -1) itemId = items.findIndex((val) => Math.abs(info.duration - val.duration) < 3000);\n\t\tif (itemId === -1) itemId = items.findIndex((val) => val.name === cleanTitle);\n\t\tif (itemId === -1) throw \"Cannot find track\";\n\n\t\treturn await Spicetify.CosmosAsync.get(lyricURL + items[itemId].id, null, requestHeader);\n\t}\n\n\tconst creditInfo = [\n\t\t\"\\\\s?作?\\\\s*词|\\\\s?作?\\\\s*曲|\\\\s?编\\\\s*曲?|\\\\s?监\\\\s*制?\",\n\t\t\".*编写|.*和音|.*和声|.*合声|.*提琴|.*录|.*工程|.*工作室|.*设计|.*剪辑|.*制作|.*发行|.*出品|.*后期|.*混音|.*缩混\",\n\t\t\"原唱|翻唱|题字|文案|海报|古筝|二胡|钢琴|吉他|贝斯|笛子|鼓|弦乐\",\n\t\t\"lrc|publish|vocal|guitar|program|produce|write|mix\",\n\t];\n\tconst creditInfoRegExp = new RegExp(`^(${creditInfo.join(\"|\")}).*(:|：)`, \"i\");\n\n\tfunction containCredits(text) {\n\t\treturn creditInfoRegExp.test(text);\n\t}\n\n\tfunction parseTimestamp(line) {\n\t\t// [\"[ar:Beyond]\"]\n\t\t// [\"[03:10]\"]\n\t\t// [\"[03:10]\", \"lyrics\"]\n\t\t// [\"lyrics\"]\n\t\t// [\"[03:10]\", \"[03:10]\", \"lyrics\"]\n\t\t// [\"[1235,300]\", \"lyrics\"]\n\t\tconst matchResult = line.match(/(\\[.*?\\])|([^[\\]]+)/g);\n\t\tif (!matchResult?.length || matchResult.length === 1) {\n\t\t\treturn { text: line };\n\t\t}\n\n\t\tconst textIndex = matchResult.findIndex((slice) => !slice.endsWith(\"]\"));\n\t\tlet text = \"\";\n\n\t\tif (textIndex > -1) {\n\t\t\ttext = matchResult.splice(textIndex, 1)[0];\n\t\t\ttext = Utils.capitalize(Utils.normalize(text, false));\n\t\t}\n\n\t\tconst time = matchResult[0].replace(\"[\", \"\").replace(\"]\", \"\");\n\n\t\treturn { time, text };\n\t}\n\n\tfunction breakdownLine(text) {\n\t\t// (0,508)Don't(0,1) (0,151)want(0,1) (0,162)to(0,1) (0,100)be(0,1) (0,157)an(0,1)\n\t\tconst components = text.split(/\\(\\d+,(\\d+)\\)/g);\n\t\t// [\"\", \"508\", \"Don't\", \"1\", \" \", \"151\", \"want\" , \"1\" ...]\n\t\tconst result = [];\n\t\tfor (let i = 1; i < components.length; i += 2) {\n\t\t\tif (components[i + 1] === \" \") continue;\n\t\t\tresult.push({\n\t\t\t\tword: `${components[i + 1]} `,\n\t\t\t\ttime: Number.parseInt(components[i]),\n\t\t\t});\n\t\t}\n\t\treturn result;\n\t}\n\n\tfunction getKaraoke(list) {\n\t\tconst lyricStr = list?.klyric?.lyric;\n\n\t\tif (!lyricStr) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst lines = lyricStr.split(/\\r?\\n/).map((line) => line.trim());\n\t\tconst karaoke = lines\n\t\t\t.map((line) => {\n\t\t\t\tconst { time, text } = parseTimestamp(line);\n\t\t\t\tif (!time || !text) return null;\n\n\t\t\t\tconst [key, value] = time.split(\",\") || [];\n\t\t\t\tconst [start, durr] = [Number.parseFloat(key), Number.parseFloat(value)];\n\n\t\t\t\tif (!Number.isNaN(start) && !Number.isNaN(durr) && !containCredits(text)) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tstartTime: start,\n\t\t\t\t\t\t// endTime: start + durr,\n\t\t\t\t\t\ttext: breakdownLine(text),\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\treturn null;\n\t\t\t})\n\t\t\t.filter(Boolean);\n\n\t\tif (!karaoke.length) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn karaoke;\n\t}\n\n\tfunction getSynced(list) {\n\t\tconst lyricStr = list?.lrc?.lyric;\n\t\tlet noLyrics = false;\n\n\t\tif (!lyricStr) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst lines = lyricStr.split(/\\r?\\n/).map((line) => line.trim());\n\t\tconst lyrics = lines\n\t\t\t.map((line) => {\n\t\t\t\tconst { time, text } = parseTimestamp(line);\n\t\t\t\tif (text === \"纯音乐, 请欣赏\") noLyrics = true;\n\t\t\t\tif (!time || !text) return null;\n\n\t\t\t\tconst [key, value] = time.split(\":\") || [];\n\t\t\t\tconst [min, sec] = [Number.parseFloat(key), Number.parseFloat(value)];\n\t\t\t\tif (!Number.isNaN(min) && !Number.isNaN(sec) && !containCredits(text)) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tstartTime: (min * 60 + sec) * 1000,\n\t\t\t\t\t\ttext: text || \"\",\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\treturn null;\n\t\t\t})\n\t\t\t.filter(Boolean);\n\n\t\tif (!lyrics.length || noLyrics) {\n\t\t\treturn null;\n\t\t}\n\t\treturn lyrics;\n\t}\n\n\tfunction getTranslation(list) {\n\t\tconst lyricStr = list?.tlyric?.lyric;\n\n\t\tif (!lyricStr) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst lines = lyricStr.split(/\\r?\\n/).map((line) => line.trim());\n\t\tconst translation = lines\n\t\t\t.map((line) => {\n\t\t\t\tconst { time, text } = parseTimestamp(line);\n\t\t\t\tif (!time || !text) return null;\n\n\t\t\t\tconst [key, value] = time.split(\":\") || [];\n\t\t\t\tconst [min, sec] = [Number.parseFloat(key), Number.parseFloat(value)];\n\t\t\t\tif (!Number.isNaN(min) && !Number.isNaN(sec) && !containCredits(text)) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tstartTime: (min * 60 + sec) * 1000,\n\t\t\t\t\t\ttext: text || \"\",\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\treturn null;\n\t\t\t})\n\t\t\t.filter(Boolean);\n\n\t\tif (!translation.length) {\n\t\t\treturn null;\n\t\t}\n\t\treturn translation;\n\t}\n\n\tfunction getUnsynced(list) {\n\t\tconst lyricStr = list?.lrc?.lyric;\n\t\tlet noLyrics = false;\n\n\t\tif (!lyricStr) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst lines = lyricStr.split(/\\r?\\n/).map((line) => line.trim());\n\t\tconst lyrics = lines\n\t\t\t.map((line) => {\n\t\t\t\tconst parsed = parseTimestamp(line);\n\t\t\t\tif (parsed.text === \"纯音乐, 请欣赏\") noLyrics = true;\n\t\t\t\tif (!parsed.text || containCredits(parsed.text)) return null;\n\t\t\t\treturn parsed;\n\t\t\t})\n\t\t\t.filter(Boolean);\n\n\t\tif (!lyrics.length || noLyrics) {\n\t\t\treturn null;\n\t\t}\n\t\treturn lyrics;\n\t}\n\n\treturn { findLyrics, getKaraoke, getSynced, getUnsynced, getTranslation };\n})();\n"
  },
  {
    "path": "CustomApps/lyrics-plus/Providers.js",
    "content": "const Providers = {\n\tspotify: async (info) => {\n\t\tconst result = {\n\t\t\turi: info.uri,\n\t\t\tkaraoke: null,\n\t\t\tsynced: null,\n\t\t\tunsynced: null,\n\t\t\tprovider: \"Spotify\",\n\t\t\tcopyright: null,\n\t\t};\n\n\t\tconst baseURL = \"https://spclient.wg.spotify.com/color-lyrics/v2/track/\";\n\t\tconst id = info.uri.split(\":\")[2];\n\t\tlet body;\n\t\ttry {\n\t\t\tbody = await Spicetify.CosmosAsync.get(`${baseURL + id}?format=json&vocalRemoval=false&market=from_token`);\n\t\t} catch {\n\t\t\treturn { error: \"Request error\", uri: info.uri };\n\t\t}\n\n\t\tconst lyrics = body.lyrics;\n\t\tif (!lyrics) {\n\t\t\treturn { error: \"No lyrics\", uri: info.uri };\n\t\t}\n\n\t\tconst lines = lyrics.lines;\n\t\tif (lyrics.syncType === \"LINE_SYNCED\") {\n\t\t\tresult.synced = lines.map((line) => ({\n\t\t\t\tstartTime: line.startTimeMs,\n\t\t\t\ttext: line.words,\n\t\t\t}));\n\t\t\tresult.unsynced = result.synced;\n\t\t} else {\n\t\t\tresult.unsynced = lines.map((line) => ({\n\t\t\t\ttext: line.words,\n\t\t\t}));\n\t\t}\n\n\t\t/**\n\t\t * to distinguish it from the existing Musixmatch, the provider will remain as Spotify.\n\t\t * if Spotify official lyrics support multiple providers besides Musixmatch in the future, please uncomment the under section. */\n\t\t// result.provider = lyrics.provider;\n\n\t\treturn result;\n\t},\n\tmusixmatch: async (info) => {\n\t\tconst result = {\n\t\t\terror: null,\n\t\t\turi: info.uri,\n\t\t\tkaraoke: null,\n\t\t\tsynced: null,\n\t\t\tunsynced: null,\n\t\t\tmusixmatchTranslation: null,\n\t\t\tmusixmatchAvailableTranslations: [],\n\t\t\tmusixmatchTrackId: null,\n\t\t\tmusixmatchTranslationLanguage: null,\n\t\t\tprovider: \"Musixmatch\",\n\t\t\tcopyright: null,\n\t\t};\n\n\t\tlet list;\n\t\ttry {\n\t\t\tlist = await ProviderMusixmatch.findLyrics(info);\n\t\t\tif (list.error) {\n\t\t\t\tthrow \"\";\n\t\t\t}\n\t\t} catch {\n\t\t\tresult.error = \"No lyrics\";\n\t\t\treturn result;\n\t\t}\n\n\t\tconst karaoke = await ProviderMusixmatch.getKaraoke(list);\n\t\tif (karaoke) {\n\t\t\tresult.karaoke = karaoke;\n\t\t\tresult.copyright = list[\"track.lyrics.get\"].message?.body?.lyrics?.lyrics_copyright?.trim();\n\t\t}\n\t\tconst synced = ProviderMusixmatch.getSynced(list);\n\t\tif (synced) {\n\t\t\tresult.synced = synced;\n\t\t\tresult.copyright = list[\"track.subtitles.get\"].message?.body?.subtitle_list?.[0]?.subtitle.lyrics_copyright.trim();\n\t\t}\n\t\tconst unsynced = synced || ProviderMusixmatch.getUnsynced(list);\n\t\tif (unsynced) {\n\t\t\tresult.unsynced = unsynced;\n\t\t\tresult.copyright = list[\"track.lyrics.get\"].message?.body?.lyrics?.lyrics_copyright?.trim();\n\t\t}\n\t\tresult.musixmatchAvailableTranslations = Array.isArray(list.__musixmatchTranslationStatus) ? list.__musixmatchTranslationStatus : [];\n\t\tresult.musixmatchTrackId = list.__musixmatchTrackId ?? null;\n\n\t\tconst selectedLanguage = CONFIG.visual[\"musixmatch-translation-language\"];\n\t\tconst canRequestTranslation =\n\t\t\tselectedLanguage && selectedLanguage !== \"none\" && result.musixmatchAvailableTranslations.includes(selectedLanguage);\n\n\t\tconst translation = canRequestTranslation ? await ProviderMusixmatch.getTranslation(result.musixmatchTrackId) : null;\n\t\tif ((synced || unsynced) && Array.isArray(translation) && translation.length) {\n\t\t\tconst normalizeLyrics =\n\t\t\t\ttypeof Utils !== \"undefined\" && typeof Utils.processLyrics === \"function\"\n\t\t\t\t\t? (value) => Utils.processLyrics(value ?? \"\")\n\t\t\t\t\t: (value) =>\n\t\t\t\t\t\t\ttypeof value === \"string\" ? value.replace(/　| /g, \"\").replace(/[!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~？！，。、《》【】「」]/g, \"\") : \"\";\n\n\t\t\tconst translationMap = new Map();\n\t\t\tfor (const entry of translation) {\n\t\t\t\tconst normalizedMatched = normalizeLyrics(entry.matchedLine);\n\t\t\t\tif (!translationMap.has(normalizedMatched)) {\n\t\t\t\t\ttranslationMap.set(normalizedMatched, entry.translation);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst baseLyrics = synced ?? unsynced;\n\t\t\tresult.musixmatchTranslation = baseLyrics.map((line) => {\n\t\t\t\tconst originalText = line.text;\n\t\t\t\tconst normalizedOriginal = normalizeLyrics(originalText);\n\t\t\t\treturn {\n\t\t\t\t\t...line,\n\t\t\t\t\ttext: translationMap.get(normalizedOriginal) ?? line.text,\n\t\t\t\t\toriginalText,\n\t\t\t\t};\n\t\t\t});\n\t\t\tresult.musixmatchTranslationLanguage = selectedLanguage;\n\t\t}\n\n\t\treturn result;\n\t},\n\tnetease: async (info) => {\n\t\tconst result = {\n\t\t\turi: info.uri,\n\t\t\tkaraoke: null,\n\t\t\tsynced: null,\n\t\t\tunsynced: null,\n\t\t\tneteaseTranslation: null,\n\t\t\tprovider: \"Netease\",\n\t\t\tcopyright: null,\n\t\t};\n\n\t\tlet list;\n\t\ttry {\n\t\t\tlist = await ProviderNetease.findLyrics(info);\n\t\t} catch {\n\t\t\tresult.error = \"No lyrics\";\n\t\t\treturn result;\n\t\t}\n\n\t\tconst karaoke = ProviderNetease.getKaraoke(list);\n\t\tif (karaoke) {\n\t\t\tresult.karaoke = karaoke;\n\t\t}\n\t\tconst synced = ProviderNetease.getSynced(list);\n\t\tif (synced) {\n\t\t\tresult.synced = synced;\n\t\t}\n\t\tconst unsynced = synced || ProviderNetease.getUnsynced(list);\n\t\tif (unsynced) {\n\t\t\tresult.unsynced = unsynced;\n\t\t}\n\t\tconst translation = ProviderNetease.getTranslation(list);\n\t\tif ((synced || unsynced) && Array.isArray(translation)) {\n\t\t\tconst baseLyrics = synced ?? unsynced;\n\t\t\tresult.neteaseTranslation = baseLyrics.map((line) => ({\n\t\t\t\t...line,\n\t\t\t\ttext: translation.find((t) => t.startTime === line.startTime)?.text ?? line.text,\n\t\t\t\toriginalText: line.text,\n\t\t\t}));\n\t\t}\n\n\t\treturn result;\n\t},\n\tlrclib: async (info) => {\n\t\tconst result = {\n\t\t\turi: info.uri,\n\t\t\tkaraoke: null,\n\t\t\tsynced: null,\n\t\t\tunsynced: null,\n\t\t\tprovider: \"lrclib\",\n\t\t\tcopyright: null,\n\t\t};\n\n\t\tlet list;\n\t\ttry {\n\t\t\tlist = await ProviderLRCLIB.findLyrics(info);\n\t\t} catch {\n\t\t\tresult.error = \"No lyrics\";\n\t\t\treturn result;\n\t\t}\n\n\t\tconst synced = ProviderLRCLIB.getSynced(list);\n\t\tif (synced) {\n\t\t\tresult.synced = synced;\n\t\t}\n\n\t\tconst unsynced = synced || ProviderLRCLIB.getUnsynced(list);\n\n\t\tif (unsynced) {\n\t\t\tresult.unsynced = unsynced;\n\t\t}\n\n\t\treturn result;\n\t},\n\tgenius: async (info) => {\n\t\tconst { lyrics, versions } = await ProviderGenius.fetchLyrics(info);\n\n\t\tlet versionIndex2 = 0;\n\t\tlet genius2 = lyrics;\n\t\tif (CONFIG.visual[\"dual-genius\"] && versions.length > 1) {\n\t\t\tgenius2 = await ProviderGenius.fetchLyricsVersion(versions, 1);\n\t\t\tversionIndex2 = 1;\n\t\t}\n\n\t\treturn {\n\t\t\turi: info.uri,\n\t\t\tgenius: lyrics,\n\t\t\tprovider: \"Genius\",\n\t\t\tkaraoke: null,\n\t\t\tsynced: null,\n\t\t\tunsynced: null,\n\t\t\tcopyright: null,\n\t\t\terror: null,\n\t\t\tversions,\n\t\t\tversionIndex: 0,\n\t\t\tgenius2,\n\t\t\tversionIndex2,\n\t\t};\n\t},\n\tlocal: (info) => {\n\t\tlet result = {\n\t\t\turi: info.uri,\n\t\t\tkaraoke: null,\n\t\t\tsynced: null,\n\t\t\tunsynced: null,\n\t\t\tprovider: \"local\",\n\t\t};\n\n\t\ttry {\n\t\t\tconst savedLyrics = JSON.parse(localStorage.getItem(\"lyrics-plus:local-lyrics\"));\n\t\t\tconst lyrics = savedLyrics[info.uri];\n\t\t\tif (!lyrics) {\n\t\t\t\tthrow \"\";\n\t\t\t}\n\n\t\t\tresult = {\n\t\t\t\t...result,\n\t\t\t\t...lyrics,\n\t\t\t};\n\t\t} catch {\n\t\t\tresult.error = \"No lyrics\";\n\t\t}\n\n\t\treturn result;\n\t},\n};\n"
  },
  {
    "path": "CustomApps/lyrics-plus/README.md",
    "content": "# Spicetify Custom App\n\n### Lyrics Plus\n\nShow current track lyrics. Current lyrics providers:\n\n- Internal Spotify lyrics service.\n- Netease: From Chinese developers and users. Provides karaoke and synced lyrics.\n- Musixmatch: A company from Italy. Provided synced lyrics.\n- Genius: Provides unsynced lyrics but with description/insight from artists themselves (Disabled and cannot be used as a provider on `1.2.31` and higher).\n\n![kara](./kara.png)\n\n![genius](./genius.png)\n\nDifferent lyrics modes: Karaoke, Synced, Unsynced and Genius. At the moment, only Netease provides karaoke-able lyrics. Mode is automatically falled back, from Karaoke, Synced, Unsynced to Genius when lyrics are not available in that mode.\n\nRight click or Double click at any mode tab to \"lock in\", so lyric mode won't auto switch. It should show a dot next to mode name when mode is locked. Right click or double click again to unlock\n\n![lockin](./lockin.png)\n\nLyrics in Unsynced and Genius modes can be search and jump to. Hit Ctrl + Shift + F to open search box at bottom left of screen. Hit Enter/Shift+Enter to loop over results.\n\n![search](./search.png)\n\nChoose between different option of displaying Japanese lyrics. (Furigana, Romaji, Hiragana, Katakana)\n\n![conversion](./conversion.png)\n\nCustomise colors, change providers' priorities in config menu. Config menu locates in Profile Menu (top right button with your user name).\n\nTo install, run:\n\n```bash\nspicetify config custom_apps lyrics-plus\nspicetify apply\n```\n\n### Credits\n\n- A few parts of app code are taken from Spotify official app, including SyncedLyricsPage, CSS animation and TabBar. Please do not distribute these code else where out of Spotify/Spicetify context.\n- Netease synced lyrics parser is adapted from [mantou132/Spotify-Lyrics](https://github.com/mantou132/Spotify-Lyrics). Give it a Star if you like this app.\n- The algorithm for converting Japanese lyrics is based on [Hexenq's Kuroshiro](https://github.com/hexenq/kuroshiro).\n- The algorithm for converting Chinese lyrics is based on [BYVoid's OpenCC](https://github.com/BYVoid/OpenCC) via [nk2028's opencc-js](https://github.com/nk2028/opencc-js).\n- The algorithm for converting Korean lyrics is based on [fujaru's aromanize-js](https://github.com/fujaru/aromanize-js)\n- The algorithm for detecting Simplified Chinese is adapted from [nickdrewe's traditional-or-simplified](https://github.com/nickdrewe/traditional-or-simplified).\n"
  },
  {
    "path": "CustomApps/lyrics-plus/Settings.js",
    "content": "const ButtonSVG = ({ icon, active = true, onClick }) => {\n\treturn react.createElement(\n\t\t\"button\",\n\t\t{\n\t\t\tclassName: `switch${active ? \"\" : \" disabled\"}`,\n\t\t\tonClick,\n\t\t},\n\t\treact.createElement(\"svg\", {\n\t\t\twidth: 16,\n\t\t\theight: 16,\n\t\t\tviewBox: \"0 0 16 16\",\n\t\t\tfill: \"currentColor\",\n\t\t\tdangerouslySetInnerHTML: {\n\t\t\t\t__html: icon,\n\t\t\t},\n\t\t})\n\t);\n};\n\nconst SwapButton = ({ icon, disabled, onClick }) => {\n\treturn react.createElement(\n\t\t\"button\",\n\t\t{\n\t\t\tclassName: \"switch small\",\n\t\t\tonClick,\n\t\t\tdisabled,\n\t\t},\n\t\treact.createElement(\"svg\", {\n\t\t\twidth: 10,\n\t\t\theight: 10,\n\t\t\tviewBox: \"0 0 16 16\",\n\t\t\tfill: \"currentColor\",\n\t\t\tdangerouslySetInnerHTML: {\n\t\t\t\t__html: icon,\n\t\t\t},\n\t\t})\n\t);\n};\n\nconst CacheButton = () => {\n\tlet lyrics = {};\n\n\ttry {\n\t\tconst localLyrics = JSON.parse(localStorage.getItem(\"lyrics-plus:local-lyrics\"));\n\t\tif (!localLyrics || typeof localLyrics !== \"object\") {\n\t\t\tthrow \"\";\n\t\t}\n\t\tlyrics = localLyrics;\n\t} catch {\n\t\tlyrics = {};\n\t}\n\n\tconst [count, setCount] = useState(Object.keys(lyrics).length);\n\tconst text = count ? \"Clear all cached lyrics\" : \"No cached lyrics\";\n\n\treturn react.createElement(\n\t\t\"button\",\n\t\t{\n\t\t\tclassName: \"btn\",\n\t\t\tonClick: () => {\n\t\t\t\tlocalStorage.removeItem(\"lyrics-plus:local-lyrics\");\n\t\t\t\tsetCount(0);\n\t\t\t},\n\t\t\tdisabled: !count,\n\t\t},\n\t\ttext\n\t);\n};\n\nconst RefreshTokenButton = ({ setTokenCallback }) => {\n\tconst [buttonText, setButtonText] = useState(\"Refresh token\");\n\n\tuseEffect(() => {\n\t\tif (buttonText === \"Refreshing token...\") {\n\t\t\tSpicetify.CosmosAsync.get(\"https://apic-desktop.musixmatch.com/ws/1.1/token.get?app_id=web-desktop-app-v1.0\", null, {\n\t\t\t\tauthority: \"apic-desktop.musixmatch.com\",\n\t\t\t})\n\t\t\t\t.then(({ message: response }) => {\n\t\t\t\t\tif (response.header.status_code === 200 && response.body.user_token) {\n\t\t\t\t\t\tsetTokenCallback(response.body.user_token);\n\t\t\t\t\t\tsetButtonText(\"Token refreshed\");\n\t\t\t\t\t} else if (response.header.status_code === 401) {\n\t\t\t\t\t\tsetButtonText(\"Too many attempts\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsetButtonText(\"Failed to refresh token\");\n\t\t\t\t\t\tconsole.error(\"Failed to refresh token\", response);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch((error) => {\n\t\t\t\t\tsetButtonText(\"Failed to refresh token\");\n\t\t\t\t\tconsole.error(\"Failed to refresh token\", error);\n\t\t\t\t});\n\t\t}\n\t}, [buttonText]);\n\n\treturn react.createElement(\n\t\t\"button\",\n\t\t{\n\t\t\tclassName: \"btn\",\n\t\t\tonClick: () => {\n\t\t\t\tsetButtonText(\"Refreshing token...\");\n\t\t\t},\n\t\t\tdisabled: buttonText !== \"Refresh token\",\n\t\t},\n\t\tbuttonText\n\t);\n};\n\nconst ConfigButton = ({ name, text, onChange = () => {} }) => {\n\treturn react.createElement(\n\t\t\"div\",\n\t\t{\n\t\t\tclassName: \"setting-row\",\n\t\t},\n\t\treact.createElement(\n\t\t\t\"label\",\n\t\t\t{\n\t\t\t\tclassName: \"col description\",\n\t\t\t},\n\t\t\tname\n\t\t),\n\t\treact.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tclassName: \"col action\",\n\t\t\t},\n\t\t\treact.createElement(\n\t\t\t\t\"button\",\n\t\t\t\t{\n\t\t\t\t\tclassName: \"btn\",\n\t\t\t\t\tonClick: onChange,\n\t\t\t\t},\n\t\t\t\ttext\n\t\t\t)\n\t\t)\n\t);\n};\n\nconst ConfigSlider = ({ name, defaultValue, onChange = () => {} }) => {\n\tconst [active, setActive] = useState(defaultValue);\n\n\tuseEffect(() => {\n\t\tsetActive(defaultValue);\n\t}, [defaultValue]);\n\n\tconst toggleState = useCallback(() => {\n\t\tconst state = !active;\n\t\tsetActive(state);\n\t\tonChange(state);\n\t}, [active]);\n\n\treturn react.createElement(\n\t\t\"div\",\n\t\t{\n\t\t\tclassName: \"setting-row\",\n\t\t},\n\t\treact.createElement(\n\t\t\t\"label\",\n\t\t\t{\n\t\t\t\tclassName: \"col description\",\n\t\t\t},\n\t\t\tname\n\t\t),\n\t\treact.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tclassName: \"col action\",\n\t\t\t},\n\t\t\treact.createElement(ButtonSVG, {\n\t\t\t\ticon: Spicetify.SVGIcons.check,\n\t\t\t\tactive,\n\t\t\t\tonClick: toggleState,\n\t\t\t})\n\t\t)\n\t);\n};\n\nconst ConfigSelection = ({ name, defaultValue, options, onChange = () => {} }) => {\n\tconst [value, setValue] = useState(defaultValue);\n\n\tconst setValueCallback = useCallback(\n\t\t(event) => {\n\t\t\tlet value = event.target.value;\n\t\t\tif (!Number.isNaN(Number(value))) {\n\t\t\t\tvalue = Number.parseInt(value);\n\t\t\t}\n\t\t\tsetValue(value);\n\t\t\tonChange(value);\n\t\t},\n\t\t[value, options]\n\t);\n\n\tuseEffect(() => {\n\t\tsetValue(defaultValue);\n\t}, [defaultValue]);\n\n\tif (!Object.keys(options).length) return null;\n\n\treturn react.createElement(\n\t\t\"div\",\n\t\t{\n\t\t\tclassName: \"setting-row\",\n\t\t},\n\t\treact.createElement(\n\t\t\t\"label\",\n\t\t\t{\n\t\t\t\tclassName: \"col description\",\n\t\t\t},\n\t\t\tname\n\t\t),\n\t\treact.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tclassName: \"col action\",\n\t\t\t},\n\t\t\treact.createElement(\n\t\t\t\t\"select\",\n\t\t\t\t{\n\t\t\t\t\tclassName: \"main-dropDown-dropDown\",\n\t\t\t\t\tvalue,\n\t\t\t\t\tonChange: setValueCallback,\n\t\t\t\t},\n\t\t\t\tObject.keys(options).map((item) =>\n\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\"option\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvalue: item,\n\t\t\t\t\t\t},\n\t\t\t\t\t\toptions[item]\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t)\n\t\t)\n\t);\n};\n\nconst ConfigInput = ({ name, defaultValue, onChange = () => {} }) => {\n\tconst [value, setValue] = useState(defaultValue);\n\n\tconst setValueCallback = useCallback(\n\t\t(event) => {\n\t\t\tconst value = event.target.value;\n\t\t\tsetValue(value);\n\t\t\tonChange(value);\n\t\t},\n\t\t[value]\n\t);\n\n\treturn react.createElement(\n\t\t\"div\",\n\t\t{\n\t\t\tclassName: \"setting-row\",\n\t\t},\n\t\treact.createElement(\n\t\t\t\"label\",\n\t\t\t{\n\t\t\t\tclassName: \"col description\",\n\t\t\t},\n\t\t\tname\n\t\t),\n\t\treact.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tclassName: \"col action\",\n\t\t\t},\n\t\t\treact.createElement(\"input\", {\n\t\t\t\tvalue,\n\t\t\t\tonChange: setValueCallback,\n\t\t\t})\n\t\t)\n\t);\n};\n\nconst ConfigAdjust = ({ name, defaultValue, step, min, max, onChange = () => {} }) => {\n\tconst [value, setValue] = useState(defaultValue);\n\n\tfunction adjust(dir) {\n\t\tlet temp = value + dir * step;\n\t\tif (temp < min) {\n\t\t\ttemp = min;\n\t\t} else if (temp > max) {\n\t\t\ttemp = max;\n\t\t}\n\t\tsetValue(temp);\n\t\tonChange(temp);\n\t}\n\treturn react.createElement(\n\t\t\"div\",\n\t\t{\n\t\t\tclassName: \"setting-row\",\n\t\t},\n\t\treact.createElement(\n\t\t\t\"label\",\n\t\t\t{\n\t\t\t\tclassName: \"col description\",\n\t\t\t},\n\t\t\tname\n\t\t),\n\t\treact.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tclassName: \"col action\",\n\t\t\t},\n\t\t\treact.createElement(SwapButton, {\n\t\t\t\ticon: `<path d=\"M2 7h12v2H0z\"/>`,\n\t\t\t\tonClick: () => adjust(-1),\n\t\t\t\tdisabled: value === min,\n\t\t\t}),\n\t\t\treact.createElement(\n\t\t\t\t\"p\",\n\t\t\t\t{\n\t\t\t\t\tclassName: \"adjust-value\",\n\t\t\t\t},\n\t\t\t\tvalue\n\t\t\t),\n\t\t\treact.createElement(SwapButton, {\n\t\t\t\ticon: Spicetify.SVGIcons.plus2px,\n\t\t\t\tonClick: () => adjust(1),\n\t\t\t\tdisabled: value === max,\n\t\t\t})\n\t\t)\n\t);\n};\n\nconst ConfigHotkey = ({ name, defaultValue, onChange = () => {} }) => {\n\tconst [value, setValue] = useState(defaultValue);\n\tconst [trap] = useState(new Spicetify.Mousetrap());\n\n\tfunction record() {\n\t\ttrap.handleKey = (character, modifiers, e) => {\n\t\t\tif (e.type === \"keydown\") {\n\t\t\t\tconst sequence = [...new Set([...modifiers, character])];\n\t\t\t\tif (sequence.length === 1 && sequence[0] === \"esc\") {\n\t\t\t\t\tonChange(\"\");\n\t\t\t\t\tsetValue(\"\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsetValue(sequence.join(\"+\"));\n\t\t\t}\n\t\t};\n\t}\n\n\tfunction finishRecord() {\n\t\ttrap.handleKey = () => {};\n\t\tonChange(value);\n\t}\n\n\treturn react.createElement(\n\t\t\"div\",\n\t\t{\n\t\t\tclassName: \"setting-row\",\n\t\t},\n\t\treact.createElement(\n\t\t\t\"label\",\n\t\t\t{\n\t\t\t\tclassName: \"col description\",\n\t\t\t},\n\t\t\tname\n\t\t),\n\t\treact.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tclassName: \"col action\",\n\t\t\t},\n\t\t\treact.createElement(\"input\", {\n\t\t\t\tvalue,\n\t\t\t\tonFocus: record,\n\t\t\t\tonBlur: finishRecord,\n\t\t\t})\n\t\t)\n\t);\n};\n\nconst ServiceAction = ({ item, setTokenCallback }) => {\n\tswitch (item.name) {\n\t\tcase \"local\":\n\t\t\treturn react.createElement(CacheButton);\n\t\tcase \"musixmatch\":\n\t\t\treturn react.createElement(RefreshTokenButton, { setTokenCallback });\n\t\tdefault:\n\t\t\treturn null;\n\t}\n};\n\nconst ServiceOption = ({ item, onToggle, onSwap, isFirst = false, isLast = false, onTokenChange = null }) => {\n\tconst [token, setToken] = useState(item.token);\n\tconst [active, setActive] = useState(item.on);\n\n\tconst setTokenCallback = useCallback(\n\t\t(token) => {\n\t\t\tsetToken(token);\n\t\t\tonTokenChange(item.name, token);\n\t\t},\n\t\t[item.token]\n\t);\n\n\tconst toggleActive = useCallback(() => {\n\t\tif (item.name === \"genius\" && spotifyVersion >= \"1.2.31\") return;\n\t\tconst state = !active;\n\t\tsetActive(state);\n\t\tonToggle(item.name, state);\n\t}, [active]);\n\n\treturn react.createElement(\n\t\t\"div\",\n\t\tnull,\n\t\treact.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tclassName: \"setting-row\",\n\t\t\t},\n\t\t\treact.createElement(\n\t\t\t\t\"h3\",\n\t\t\t\t{\n\t\t\t\t\tclassName: \"col description\",\n\t\t\t\t},\n\t\t\t\titem.name\n\t\t\t),\n\t\t\treact.createElement(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tclassName: \"col action\",\n\t\t\t\t},\n\t\t\t\treact.createElement(ServiceAction, {\n\t\t\t\t\titem,\n\t\t\t\t\tsetTokenCallback,\n\t\t\t\t}),\n\t\t\t\treact.createElement(SwapButton, {\n\t\t\t\t\ticon: Spicetify.SVGIcons[\"chart-up\"],\n\t\t\t\t\tonClick: () => onSwap(item.name, -1),\n\t\t\t\t\tdisabled: isFirst,\n\t\t\t\t}),\n\t\t\t\treact.createElement(SwapButton, {\n\t\t\t\t\ticon: Spicetify.SVGIcons[\"chart-down\"],\n\t\t\t\t\tonClick: () => onSwap(item.name, 1),\n\t\t\t\t\tdisabled: isLast,\n\t\t\t\t}),\n\t\t\t\treact.createElement(ButtonSVG, {\n\t\t\t\t\ticon: Spicetify.SVGIcons.check,\n\t\t\t\t\tactive,\n\t\t\t\t\tonClick: toggleActive,\n\t\t\t\t})\n\t\t\t)\n\t\t),\n\t\treact.createElement(\"span\", {\n\t\t\tdangerouslySetInnerHTML: {\n\t\t\t\t__html: item.desc,\n\t\t\t},\n\t\t}),\n\t\titem.token !== undefined &&\n\t\t\treact.createElement(\"input\", {\n\t\t\t\tplaceholder: `Place your ${item.name} token here`,\n\t\t\t\tvalue: token,\n\t\t\t\tonChange: (event) => setTokenCallback(event.target.value),\n\t\t\t})\n\t);\n};\n\nconst ServiceList = ({ itemsList, onListChange = () => {}, onToggle = () => {}, onTokenChange = () => {} }) => {\n\tconst [items, setItems] = useState(itemsList);\n\tconst maxIndex = items.length - 1;\n\n\tconst onSwap = useCallback(\n\t\t(name, direction) => {\n\t\t\tconst curPos = items.findIndex((val) => val === name);\n\t\t\tconst newPos = curPos + direction;\n\t\t\t[items[curPos], items[newPos]] = [items[newPos], items[curPos]];\n\t\t\tonListChange(items);\n\t\t\tsetItems([...items]);\n\t\t},\n\t\t[items]\n\t);\n\n\treturn items.map((key, index) => {\n\t\tconst item = CONFIG.providers[key];\n\t\titem.name = key;\n\t\treturn react.createElement(ServiceOption, {\n\t\t\titem,\n\t\t\tkey,\n\t\t\tisFirst: index === 0,\n\t\t\tisLast: index === maxIndex,\n\t\t\tonSwap,\n\t\t\tonTokenChange,\n\t\t\tonToggle,\n\t\t});\n\t});\n};\n\nconst corsProxyTemplate = () => {\n\tconst [proxyValue, setProxyValue] = react.useState(localStorage.getItem(\"spicetify:corsProxyTemplate\") || \"https://cors-proxy.spicetify.app/{url}\");\n\n\treturn react.createElement(\"input\", {\n\t\tplaceholder: \"CORS Proxy Template\",\n\t\tvalue: proxyValue,\n\t\tonChange: (event) => {\n\t\t\tconst value = event.target.value;\n\t\t\tsetProxyValue(value);\n\n\t\t\tif (value === \"\" || !value) return localStorage.removeItem(\"spicetify:corsProxyTemplate\");\n\t\t\tlocalStorage.setItem(\"spicetify:corsProxyTemplate\", value);\n\t\t},\n\t});\n};\n\nconst OptionList = ({ type, items, onChange }) => {\n\tconst [itemList, setItemList] = useState(items);\n\tconst [, forceUpdate] = useState();\n\n\tuseEffect(() => {\n\t\tif (!type) return;\n\n\t\tconst eventListener = (event) => {\n\t\t\tif (event.detail?.type !== type) return;\n\t\t\tsetItemList(event.detail.items);\n\t\t};\n\t\tdocument.addEventListener(\"lyrics-plus\", eventListener);\n\n\t\treturn () => document.removeEventListener(\"lyrics-plus\", eventListener);\n\t}, []);\n\n\treturn itemList.map((item) => {\n\t\tif (!item || (item.when && !item.when())) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst onChangeItem = item.onChange || onChange;\n\n\t\treturn react.createElement(\n\t\t\t\"div\",\n\t\t\tnull,\n\t\t\treact.createElement(item.type, {\n\t\t\t\t...item,\n\t\t\t\tname: item.desc,\n\t\t\t\tdefaultValue: CONFIG.visual[item.key],\n\t\t\t\tonChange: (value) => {\n\t\t\t\t\tonChangeItem(item.key, value);\n\t\t\t\t\tforceUpdate({});\n\t\t\t\t},\n\t\t\t}),\n\t\t\titem.info &&\n\t\t\t\treact.createElement(\"span\", {\n\t\t\t\t\tdangerouslySetInnerHTML: {\n\t\t\t\t\t\t__html: item.info,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t);\n\t});\n};\n\nfunction openConfig() {\n\tconst configContainer = react.createElement(\n\t\t\"div\",\n\t\t{\n\t\t\tid: `${APP_NAME}-config-container`,\n\t\t},\n\t\treact.createElement(\"h2\", null, \"Options\"),\n\t\treact.createElement(OptionList, {\n\t\t\titems: [\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Playbar button\",\n\t\t\t\t\tkey: \"playbar-button\",\n\t\t\t\t\tinfo: \"Replace Spotify's lyrics button with Lyrics Plus.\",\n\t\t\t\t\ttype: ConfigSlider,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Global delay\",\n\t\t\t\t\tinfo: \"Offset (in ms) across all tracks.\",\n\t\t\t\t\tkey: \"global-delay\",\n\t\t\t\t\ttype: ConfigAdjust,\n\t\t\t\t\tmin: -10000,\n\t\t\t\t\tmax: 10000,\n\t\t\t\t\tstep: 250,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Font size\",\n\t\t\t\t\tinfo: \"(or Ctrl + Mouse scroll in main app)\",\n\t\t\t\t\tkey: \"font-size\",\n\t\t\t\t\ttype: ConfigAdjust,\n\t\t\t\t\tmin: fontSizeLimit.min,\n\t\t\t\t\tmax: fontSizeLimit.max,\n\t\t\t\t\tstep: fontSizeLimit.step,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Alignment\",\n\t\t\t\t\tkey: \"alignment\",\n\t\t\t\t\ttype: ConfigSelection,\n\t\t\t\t\toptions: {\n\t\t\t\t\t\tleft: \"Left\",\n\t\t\t\t\t\tcenter: \"Center\",\n\t\t\t\t\t\tright: \"Right\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Fullscreen hotkey\",\n\t\t\t\t\tkey: \"fullscreen-key\",\n\t\t\t\t\ttype: ConfigHotkey,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Compact synced: Lines to show before\",\n\t\t\t\t\tkey: \"lines-before\",\n\t\t\t\t\ttype: ConfigSelection,\n\t\t\t\t\toptions: [0, 1, 2, 3, 4],\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Compact synced: Lines to show after\",\n\t\t\t\t\tkey: \"lines-after\",\n\t\t\t\t\ttype: ConfigSelection,\n\t\t\t\t\toptions: [0, 1, 2, 3, 4],\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Compact synced: Fade-out blur\",\n\t\t\t\t\tkey: \"fade-blur\",\n\t\t\t\t\ttype: ConfigSlider,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Noise overlay\",\n\t\t\t\t\tkey: \"noise\",\n\t\t\t\t\ttype: ConfigSlider,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Colorful background\",\n\t\t\t\t\tkey: \"colorful\",\n\t\t\t\t\ttype: ConfigSlider,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Background color\",\n\t\t\t\t\tkey: \"background-color\",\n\t\t\t\t\ttype: ConfigInput,\n\t\t\t\t\twhen: () => !CONFIG.visual.colorful,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Active text color\",\n\t\t\t\t\tkey: \"active-color\",\n\t\t\t\t\ttype: ConfigInput,\n\t\t\t\t\twhen: () => !CONFIG.visual.colorful,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Inactive text color\",\n\t\t\t\t\tkey: \"inactive-color\",\n\t\t\t\t\ttype: ConfigInput,\n\t\t\t\t\twhen: () => !CONFIG.visual.colorful,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Highlight text background\",\n\t\t\t\t\tkey: \"highlight-color\",\n\t\t\t\t\ttype: ConfigInput,\n\t\t\t\t\twhen: () => !CONFIG.visual.colorful,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Text convertion: Japanese Detection threshold (Advanced)\",\n\t\t\t\t\tinfo: \"Checks if whenever Kana is dominant in lyrics. If the result passes the threshold, it's most likely Japanese, and vice versa. This setting is in percentage.\",\n\t\t\t\t\tkey: \"ja-detect-threshold\",\n\t\t\t\t\ttype: ConfigAdjust,\n\t\t\t\t\tmin: thresholdSizeLimit.min,\n\t\t\t\t\tmax: thresholdSizeLimit.max,\n\t\t\t\t\tstep: thresholdSizeLimit.step,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Text convertion: Traditional-Simplified Detection threshold (Advanced)\",\n\t\t\t\t\tinfo: \"Checks if whenever Traditional or Simplified is dominant in lyrics. If the result passes the threshold, it's most likely Simplified, and vice versa. This setting is in percentage.\",\n\t\t\t\t\tkey: \"hans-detect-threshold\",\n\t\t\t\t\ttype: ConfigAdjust,\n\t\t\t\t\tmin: thresholdSizeLimit.min,\n\t\t\t\t\tmax: thresholdSizeLimit.max,\n\t\t\t\t\tstep: thresholdSizeLimit.step,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Clear Memory Cache\",\n\t\t\t\t\tinfo: \"Loaded lyrics are cached in memory for faster reloading. Press this button to clear the cached lyrics from memory without restarting Spotify.\",\n\t\t\t\t\tkey: \"clear-memore-cache\",\n\t\t\t\t\ttext: \"Clear memory cache\",\n\t\t\t\t\ttype: ConfigButton,\n\t\t\t\t\tonChange: () => {\n\t\t\t\t\t\treloadLyrics?.();\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t],\n\t\t\tonChange: (name, value) => {\n\t\t\t\tCONFIG.visual[name] = value;\n\t\t\t\tlocalStorage.setItem(`${APP_NAME}:visual:${name}`, value);\n\t\t\t\tlyricContainerUpdate?.();\n\n\t\t\t\tconst configChange = new CustomEvent(\"lyrics-plus\", {\n\t\t\t\t\tdetail: {\n\t\t\t\t\t\ttype: \"config\",\n\t\t\t\t\t\tname: name,\n\t\t\t\t\t\tvalue: value,\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t\twindow.dispatchEvent(configChange);\n\t\t\t},\n\t\t}),\n\t\treact.createElement(\"h2\", null, \"Providers\"),\n\t\treact.createElement(ServiceList, {\n\t\t\titemsList: CONFIG.providersOrder,\n\t\t\tonListChange: (list) => {\n\t\t\t\tCONFIG.providersOrder = list;\n\t\t\t\tlocalStorage.setItem(`${APP_NAME}:services-order`, JSON.stringify(list));\n\t\t\t\treloadLyrics?.();\n\t\t\t},\n\t\t\tonToggle: (name, value) => {\n\t\t\t\tCONFIG.providers[name].on = value;\n\t\t\t\tlocalStorage.setItem(`${APP_NAME}:provider:${name}:on`, value);\n\t\t\t\treloadLyrics?.();\n\t\t\t},\n\t\t\tonTokenChange: (name, value) => {\n\t\t\t\tCONFIG.providers[name].token = value;\n\t\t\t\tlocalStorage.setItem(`${APP_NAME}:provider:${name}:token`, value);\n\t\t\t\treloadLyrics?.();\n\t\t\t},\n\t\t}),\n\t\treact.createElement(\"h2\", null, \"CORS Proxy Template\"),\n\t\treact.createElement(\"span\", {\n\t\t\tdangerouslySetInnerHTML: {\n\t\t\t\t__html:\n\t\t\t\t\t\"Use this to bypass CORS restrictions. Replace the URL with your cors proxy server of your choice. <code>{url}</code> will be replaced with the request URL.\",\n\t\t\t},\n\t\t}),\n\t\treact.createElement(corsProxyTemplate),\n\t\treact.createElement(\"span\", {\n\t\t\tdangerouslySetInnerHTML: {\n\t\t\t\t__html: \"Spotify will reload its webview after applying. Leave empty to restore default: <code>https://cors-proxy.spicetify.app/{url}</code>\",\n\t\t\t},\n\t\t})\n\t);\n\n\tSpicetify.PopupModal.display({\n\t\ttitle: \"Lyrics Plus\",\n\t\tcontent: configContainer,\n\t\tisLarge: true,\n\t});\n}\n"
  },
  {
    "path": "CustomApps/lyrics-plus/TabBar.js",
    "content": "class TabBarItem extends react.Component {\n\tonSelect(event) {\n\t\tevent.preventDefault();\n\t\tthis.props.switchTo(this.props.item.key);\n\t}\n\tonLock(event) {\n\t\tevent.preventDefault();\n\t\tthis.props.lockIn(this.props.item.key);\n\t}\n\trender() {\n\t\treturn react.createElement(\n\t\t\t\"li\",\n\t\t\t{\n\t\t\t\tclassName: \"lyrics-tabBar-headerItem\",\n\t\t\t\tonClick: this.onSelect.bind(this),\n\t\t\t\tonDoubleClick: this.onLock.bind(this),\n\t\t\t\tonContextMenu: this.onLock.bind(this),\n\t\t\t},\n\t\t\treact.createElement(\n\t\t\t\t\"a\",\n\t\t\t\t{\n\t\t\t\t\t\"aria-current\": \"page\",\n\t\t\t\t\tclassName: `lyrics-tabBar-headerItemLink ${this.props.item.active ? \"lyrics-tabBar-active\" : \"\"}`,\n\t\t\t\t\tdraggable: \"false\",\n\t\t\t\t\thref: \"\",\n\t\t\t\t},\n\t\t\t\treact.createElement(\n\t\t\t\t\t\"span\",\n\t\t\t\t\t{\n\t\t\t\t\t\tclassName: \"main-type-mestoBold\",\n\t\t\t\t\t},\n\t\t\t\t\tthis.props.item.value\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\t}\n}\n\nconst TabBarMore = react.memo(({ items, switchTo, lockIn }) => {\n\tconst activeItem = items.find((item) => item.active);\n\n\tfunction onLock(event) {\n\t\tevent.preventDefault();\n\t\tif (activeItem) {\n\t\t\tlockIn(activeItem.key);\n\t\t}\n\t}\n\treturn react.createElement(\n\t\t\"li\",\n\t\t{\n\t\t\tclassName: `lyrics-tabBar-headerItem ${activeItem ? \"lyrics-tabBar-active\" : \"\"}`,\n\t\t\tonDoubleClick: onLock,\n\t\t\tonContextMenu: onLock,\n\t\t},\n\t\treact.createElement(OptionsMenu, {\n\t\t\toptions: items,\n\t\t\tonSelect: switchTo,\n\t\t\tselected: activeItem,\n\t\t\tdefaultValue: \"More\",\n\t\t\tbold: true,\n\t\t})\n\t);\n});\n\nconst TopBarContent = ({ links, activeLink, lockLink, switchCallback, lockCallback }) => {\n\tconst resizeHost = document.querySelector(\n\t\t\".Root__main-view .os-resize-observer-host, .Root__main-view .os-size-observer, .Root__main-view .main-view-container__scroll-node\"\n\t);\n\tconst [windowSize, setWindowSize] = useState(resizeHost.clientWidth);\n\tconst resizeHandler = () => setWindowSize(resizeHost.clientWidth);\n\n\tuseEffect(() => {\n\t\tconst observer = new ResizeObserver(resizeHandler);\n\t\tobserver.observe(resizeHost);\n\t\treturn () => {\n\t\t\tobserver.disconnect();\n\t\t};\n\t}, [resizeHandler]);\n\n\treturn react.createElement(\n\t\tTabBarContext,\n\t\tnull,\n\t\treact.createElement(TabBar, {\n\t\t\tclassName: \"queue-queueHistoryTopBar-tabBar\",\n\t\t\tlinks,\n\t\t\tactiveLink,\n\t\t\tlockLink,\n\t\t\tswitchCallback,\n\t\t\tlockCallback,\n\t\t\twindowSize,\n\t\t})\n\t);\n};\n\nconst TabBarContext = ({ children }) => {\n\treturn Spicetify.ReactDOM.createPortal(\n\t\treact.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tclassName: \"main-topBar-topbarContent\",\n\t\t\t},\n\t\t\tchildren\n\t\t),\n\t\tdocument.querySelector(\".main-topBar-topbarContentWrapper\")\n\t);\n};\n\nconst TabBar = react.memo(({ links, activeLink, lockLink, switchCallback, lockCallback, windowSize = Number.POSITIVE_INFINITY }) => {\n\tconst tabBarRef = react.useRef(null);\n\tconst [childrenSizes, setChildrenSizes] = useState([]);\n\tconst [availableSpace, setAvailableSpace] = useState(0);\n\tconst [droplistItem, setDroplistItems] = useState([]);\n\n\tconst options = [];\n\tfor (let i = 0; i < links.length; i++) {\n\t\tconst key = links[i];\n\t\tif (spotifyVersion >= \"1.2.31\" && key === \"genius\") continue;\n\t\tlet value = key[0].toUpperCase() + key.slice(1);\n\t\tif (key === lockLink) value = `• ${value}`;\n\t\tconst active = key === activeLink;\n\t\toptions.push({ key, value, active });\n\t}\n\n\tuseEffect(() => {\n\t\tif (!tabBarRef.current) return;\n\t\tsetAvailableSpace(tabBarRef.current.clientWidth);\n\t}, [windowSize]);\n\n\tuseEffect(() => {\n\t\tif (!tabBarRef.current) return;\n\n\t\tconst tabbarItemSizes = [];\n\t\tfor (const child of tabBarRef.current.children) {\n\t\t\ttabbarItemSizes.push(child.clientWidth);\n\t\t}\n\n\t\tsetChildrenSizes(tabbarItemSizes);\n\t}, [links]);\n\n\tuseEffect(() => {\n\t\tif (!tabBarRef.current) return;\n\n\t\tconst totalSize = childrenSizes.reduce((a, b) => a + b, 0);\n\n\t\t// Can we render everything?\n\t\tif (totalSize <= availableSpace) {\n\t\t\tsetDroplistItems([]);\n\t\t\treturn;\n\t\t}\n\n\t\t// The `More` button can be set to _any_ of the children. So we\n\t\t// reserve space for the largest item instead of always taking\n\t\t// the last item.\n\t\tconst viewMoreButtonSize = Math.max(...childrenSizes);\n\n\t\t// Figure out how many children we can render while also showing\n\t\t// the More button\n\t\tconst itemsToHide = [];\n\t\tlet stopWidth = viewMoreButtonSize;\n\n\t\tchildrenSizes.forEach((childWidth, i) => {\n\t\t\tif (availableSpace >= stopWidth + childWidth) {\n\t\t\t\tstopWidth += childWidth;\n\t\t\t} else {\n\t\t\t\t// First elem is edit button\n\t\t\t\titemsToHide.push(i);\n\t\t\t}\n\t\t});\n\n\t\tsetDroplistItems(itemsToHide);\n\t}, [availableSpace, childrenSizes]);\n\n\treturn react.createElement(\n\t\t\"nav\",\n\t\t{\n\t\t\tclassName: \"lyrics-tabBar lyrics-tabBar-nav\",\n\t\t},\n\t\treact.createElement(\n\t\t\t\"ul\",\n\t\t\t{\n\t\t\t\tclassName: \"lyrics-tabBar-header\",\n\t\t\t\tref: tabBarRef,\n\t\t\t},\n\t\t\treact.createElement(\"li\", {\n\t\t\t\tclassName: \"lyrics-tabBar-headerItem\",\n\t\t\t}),\n\t\t\toptions\n\t\t\t\t.filter((_, id) => !droplistItem.includes(id))\n\t\t\t\t.map((item) =>\n\t\t\t\t\treact.createElement(TabBarItem, {\n\t\t\t\t\t\titem,\n\t\t\t\t\t\tswitchTo: switchCallback,\n\t\t\t\t\t\tlockIn: lockCallback,\n\t\t\t\t\t})\n\t\t\t\t),\n\t\t\tdroplistItem.length || childrenSizes.length === 0\n\t\t\t\t? react.createElement(TabBarMore, {\n\t\t\t\t\t\titems: droplistItem.map((i) => options[i]).filter(Boolean),\n\t\t\t\t\t\tswitchTo: switchCallback,\n\t\t\t\t\t\tlockIn: lockCallback,\n\t\t\t\t\t})\n\t\t\t\t: null\n\t\t)\n\t);\n});\n"
  },
  {
    "path": "CustomApps/lyrics-plus/Translator.js",
    "content": "const kuroshiroPath = \"https://cdn.jsdelivr.net/npm/kuroshiro@1.2.0/dist/kuroshiro.min.js\";\nconst kuromojiPath = \"https://cdn.jsdelivr.net/npm/kuroshiro-analyzer-kuromoji@1.1.0/dist/kuroshiro-analyzer-kuromoji.min.js\";\nconst aromanize = \"https://cdn.jsdelivr.net/npm/aromanize@0.1.5/aromanize.min.js\";\nconst openCCPath = \"https://cdn.jsdelivr.net/npm/opencc-js@1.0.5/dist/umd/full.min.js\";\n\nconst dictPath = \"https:/cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict\";\n\nclass Translator {\n\tconstructor(lang, isUsingNetease = false) {\n\t\tthis.finished = {\n\t\t\tja: false,\n\t\t\tko: false,\n\t\t\tzh: false,\n\t\t};\n\t\tthis.isUsingNetease = isUsingNetease;\n\n\t\tthis.applyKuromojiFix();\n\t\tthis.injectExternals(lang);\n\t\tthis.createTranslator(lang);\n\t}\n\n\tincludeExternal(url) {\n\t\tif ((CONFIG.visual.translate || this.isUsingNetease) && !document.querySelector(`script[src=\"${url}\"]`)) {\n\t\t\tconst script = document.createElement(\"script\");\n\t\t\tscript.setAttribute(\"type\", \"text/javascript\");\n\t\t\tscript.setAttribute(\"src\", url);\n\t\t\tdocument.head.appendChild(script);\n\t\t}\n\t}\n\n\tinjectExternals(lang) {\n\t\tswitch (lang?.slice(0, 2)) {\n\t\t\tcase \"ja\":\n\t\t\t\tthis.includeExternal(kuromojiPath);\n\t\t\t\tthis.includeExternal(kuroshiroPath);\n\t\t\t\tbreak;\n\t\t\tcase \"ko\":\n\t\t\t\tthis.includeExternal(aromanize);\n\t\t\t\tbreak;\n\t\t\tcase \"zh\":\n\t\t\t\tthis.includeExternal(openCCPath);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tasync awaitFinished(language) {\n\t\treturn new Promise((resolve) => {\n\t\t\tconst interval = setInterval(() => {\n\t\t\t\tthis.injectExternals(language);\n\t\t\t\tthis.createTranslator(language);\n\n\t\t\t\tconst lan = language.slice(0, 2);\n\t\t\t\tif (this.finished[lan]) {\n\t\t\t\t\tclearInterval(interval);\n\t\t\t\t\tresolve();\n\t\t\t\t}\n\t\t\t}, 100);\n\t\t});\n\t}\n\n\t/**\n\t * Fix an issue with kuromoji when loading dict from external urls\n\t * Adapted from: https://github.com/mobilusoss/textlint-browser-runner/pull/7\n\t */\n\tapplyKuromojiFix() {\n\t\tif (typeof XMLHttpRequest.prototype.realOpen !== \"undefined\") return;\n\t\tXMLHttpRequest.prototype.realOpen = XMLHttpRequest.prototype.open;\n\t\tXMLHttpRequest.prototype.open = function (method, url, bool) {\n\t\t\tif (url.indexOf(dictPath.replace(\"https://\", \"https:/\")) === 0) {\n\t\t\t\tthis.realOpen(method, url.replace(\"https:/\", \"https://\"), bool);\n\t\t\t} else {\n\t\t\t\tthis.realOpen(method, url, bool);\n\t\t\t}\n\t\t};\n\t}\n\n\tasync createTranslator(lang) {\n\t\tswitch (lang.slice(0, 2)) {\n\t\t\tcase \"ja\":\n\t\t\t\tif (this.kuroshiro) return;\n\t\t\t\tif (typeof Kuroshiro === \"undefined\" || typeof KuromojiAnalyzer === \"undefined\") {\n\t\t\t\t\tawait Translator.#sleep(50);\n\t\t\t\t\treturn this.createTranslator(lang);\n\t\t\t\t}\n\n\t\t\t\tthis.kuroshiro = new Kuroshiro.default();\n\t\t\t\tthis.kuroshiro.init(new KuromojiAnalyzer({ dictPath })).then(\n\t\t\t\t\tfunction () {\n\t\t\t\t\t\tthis.finished.ja = true;\n\t\t\t\t\t}.bind(this)\n\t\t\t\t);\n\n\t\t\t\tbreak;\n\t\t\tcase \"ko\":\n\t\t\t\tif (this.Aromanize) return;\n\t\t\t\tif (typeof Aromanize === \"undefined\") {\n\t\t\t\t\tawait Translator.#sleep(50);\n\t\t\t\t\treturn this.createTranslator(lang);\n\t\t\t\t}\n\n\t\t\t\tthis.Aromanize = Aromanize;\n\t\t\t\tthis.finished.ko = true;\n\t\t\t\tbreak;\n\t\t\tcase \"zh\":\n\t\t\t\tif (this.OpenCC) return;\n\t\t\t\tif (typeof OpenCC === \"undefined\") {\n\t\t\t\t\tawait Translator.#sleep(50);\n\t\t\t\t\treturn this.createTranslator(lang);\n\t\t\t\t}\n\n\t\t\t\tthis.OpenCC = OpenCC;\n\t\t\t\tthis.finished.zh = true;\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tasync romajifyText(text, target = \"romaji\", mode = \"spaced\") {\n\t\tif (!this.finished.ja) {\n\t\t\tawait Translator.#sleep(100);\n\t\t\treturn this.romajifyText(text, target, mode);\n\t\t}\n\n\t\treturn this.kuroshiro.convert(text, {\n\t\t\tto: target,\n\t\t\tmode: mode,\n\t\t});\n\t}\n\n\tasync convertToRomaja(text, target) {\n\t\tif (!this.finished.ko) {\n\t\t\tawait Translator.#sleep(100);\n\t\t\treturn this.convertToRomaja(text, target);\n\t\t}\n\n\t\tif (target === \"hangul\") return text;\n\t\treturn Aromanize.hangulToLatin(text, \"rr-translit\");\n\t}\n\n\tasync convertChinese(text, from, target) {\n\t\tif (!this.finished.zh) {\n\t\t\tawait Translator.#sleep(100);\n\t\t\treturn this.convertChinese(text, from, target);\n\t\t}\n\n\t\tconst converter = this.OpenCC.Converter({\n\t\t\tfrom: from,\n\t\t\tto: target,\n\t\t});\n\n\t\treturn converter(text);\n\t}\n\n\t/**\n\t * Async wrapper of `setTimeout`.\n\t *\n\t * @param {number} ms\n\t * @returns {Promise<void>}\n\t */\n\tstatic async #sleep(ms) {\n\t\treturn new Promise((resolve) => setTimeout(resolve, ms));\n\t}\n}\n"
  },
  {
    "path": "CustomApps/lyrics-plus/Utils.js",
    "content": "const Utils = {\n\taddQueueListener(callback) {\n\t\tSpicetify.Player.origin._events.addListener(\"queue_update\", callback);\n\t},\n\tremoveQueueListener(callback) {\n\t\tSpicetify.Player.origin._events.removeListener(\"queue_update\", callback);\n\t},\n\tconvertIntToRGB(colorInt, div = 1) {\n\t\tconst rgb = {\n\t\t\tr: Math.round(((colorInt >> 16) & 0xff) / div),\n\t\t\tg: Math.round(((colorInt >> 8) & 0xff) / div),\n\t\t\tb: Math.round((colorInt & 0xff) / div),\n\t\t};\n\t\treturn `rgb(${rgb.r},${rgb.g},${rgb.b})`;\n\t},\n\t/**\n\t * @param {string} s\n\t * @param {boolean} emptySymbol\n\t * @returns {string}\n\t */\n\tnormalize(s, emptySymbol = true) {\n\t\tlet result = s\n\t\t\t.replace(/（/g, \"(\")\n\t\t\t.replace(/）/g, \")\")\n\t\t\t.replace(/【/g, \"[\")\n\t\t\t.replace(/】/g, \"]\")\n\t\t\t.replace(/。/g, \". \")\n\t\t\t.replace(/；/g, \"; \")\n\t\t\t.replace(/：/g, \": \")\n\t\t\t.replace(/？/g, \"? \")\n\t\t\t.replace(/！/g, \"! \")\n\t\t\t.replace(/、|，/g, \", \")\n\t\t\t.replace(/‘|’|′|＇/g, \"'\")\n\t\t\t.replace(/“|”/g, '\"')\n\t\t\t.replace(/〜/g, \"~\")\n\t\t\t.replace(/·|・/g, \"•\");\n\t\tif (emptySymbol) {\n\t\t\tresult = result.replace(/-/g, \" \").replace(/\\//g, \" \");\n\t\t}\n\t\treturn result.replace(/\\s+/g, \" \").trim();\n\t},\n\t/**\n\t * Check if the specified string contains Han character.\n\t *\n\t * @param {string} s\n\t * @returns {boolean}\n\t */\n\tcontainsHanCharacter(s) {\n\t\tconst hanRegex = /\\p{Script=Han}/u;\n\t\treturn hanRegex.test(s);\n\t},\n\t/**\n\t * Singleton Translator instance for {@link toSimplifiedChinese}.\n\t *\n\t * @type {Translator | null}\n\t */\n\tset translator(translator) {\n\t\tthis._translatorInstance = translator;\n\t},\n\t_translatorInstance: null,\n\t/**\n\t * Convert all Han characters to Simplified Chinese.\n\t *\n\t * Choosing Simplified Chinese makes the converted result more accurate,\n\t * as the conversion from SC to TC may have multiple possibilities,\n\t * while the conversion from TC to SC usually has only one possibility.\n\t *\n\t * @param {string} s\n\t * @returns {Promise<string>}\n\t */\n\tasync toSimplifiedChinese(s) {\n\t\t// create a singleton Translator instance\n\t\tif (!this._translatorInstance) this.translator = new Translator(\"zh\", true);\n\n\t\t// translate to Simplified Chinese\n\t\t// as Traditional Chinese differs between HK and TW, forcing to use OpenCC standard\n\t\treturn this._translatorInstance.convertChinese(s, \"t\", \"cn\");\n\t},\n\tremoveSongFeat(s) {\n\t\treturn (\n\t\t\ts\n\t\t\t\t.replace(/-\\s+(feat|with|prod).*/i, \"\")\n\t\t\t\t.replace(/(\\(|\\[)(feat|with|prod)\\.?\\s+.*(\\)|\\])$/i, \"\")\n\t\t\t\t.trim() || s\n\t\t);\n\t},\n\tremoveExtraInfo(s) {\n\t\treturn s.replace(/\\s-\\s.*/, \"\");\n\t},\n\tcapitalize(s) {\n\t\treturn s.replace(/^(\\w)/, ($1) => $1.toUpperCase());\n\t},\n\tdetectLanguage(lyrics) {\n\t\tif (!Array.isArray(lyrics)) return;\n\n\t\t// Should return IETF BCP 47 language tags.\n\t\t// This should detect the song's main language.\n\t\t// Remember there is a possibility of a song referencing something in another language and the lyrics show it in that native language!\n\t\tconst rawLyrics = lyrics[0].originalText ? lyrics.map((line) => line.originalText).join(\" \") : lyrics.map((line) => line.text).join(\" \");\n\n\t\tconst kanaRegex = /[\\u3001-\\u3003]|[\\u3005\\u3007]|[\\u301d-\\u301f]|[\\u3021-\\u3035]|[\\u3038-\\u303a]|[\\u3040-\\u30ff]|[\\uff66-\\uff9f]/gu;\n\t\tconst hangulRegex = /(\\S*[\\u3131-\\u314e|\\u314f-\\u3163|\\uac00-\\ud7a3]+\\S*)/g;\n\t\tconst simpRegex =\n\t\t\t/[万与丑专业丛东丝丢两严丧个丬丰临为丽举么义乌乐乔习乡书买乱争于亏云亘亚产亩亲亵亸亿仅从仑仓仪们价众优伙会伛伞伟传伤伥伦伧伪伫体余佣佥侠侣侥侦侧侨侩侪侬俣俦俨俩俪俭债倾偬偻偾偿傥傧储傩儿兑兖党兰关兴兹养兽冁内冈册写军农冢冯冲决况冻净凄凉凌减凑凛几凤凫凭凯击凼凿刍划刘则刚创删别刬刭刽刿剀剂剐剑剥剧劝办务劢动励劲劳势勋勐勚匀匦匮区医华协单卖卢卤卧卫却卺厂厅历厉压厌厍厕厢厣厦厨厩厮县参叆叇双发变叙叠叶号叹叽吁后吓吕吗吣吨听启吴呒呓呕呖呗员呙呛呜咏咔咙咛咝咤咴咸哌响哑哒哓哔哕哗哙哜哝哟唛唝唠唡唢唣唤唿啧啬啭啮啰啴啸喷喽喾嗫呵嗳嘘嘤嘱噜噼嚣嚯团园囱围囵国图圆圣圹场坂坏块坚坛坜坝坞坟坠垄垅垆垒垦垧垩垫垭垯垱垲垴埘埙埚埝埯堑堕塆墙壮声壳壶壸处备复够头夸夹夺奁奂奋奖奥妆妇妈妩妪妫姗姜娄娅娆娇娈娱娲娴婳婴婵婶媪嫒嫔嫱嬷孙学孪宁宝实宠审宪宫宽宾寝对寻导寿将尔尘尧尴尸尽层屃屉届属屡屦屿岁岂岖岗岘岙岚岛岭岳岽岿峃峄峡峣峤峥峦崂崃崄崭嵘嵚嵛嵝嵴巅巩巯币帅师帏帐帘帜带帧帮帱帻帼幂幞干并广庄庆庐庑库应庙庞废庼廪开异弃张弥弪弯弹强归当录彟彦彻径徕御忆忏忧忾怀态怂怃怄怅怆怜总怼怿恋恳恶恸恹恺恻恼恽悦悫悬悭悯惊惧惨惩惫惬惭惮惯愍愠愤愦愿慑慭憷懑懒懔戆戋戏戗战戬户扎扑扦执扩扪扫扬扰抚抛抟抠抡抢护报担拟拢拣拥拦拧拨择挂挚挛挜挝挞挟挠挡挢挣挤挥挦捞损捡换捣据捻掳掴掷掸掺掼揸揽揿搀搁搂搅携摄摅摆摇摈摊撄撑撵撷撸撺擞攒敌敛数斋斓斗斩断无旧时旷旸昙昼昽显晋晒晓晔晕晖暂暧札术朴机杀杂权条来杨杩杰极构枞枢枣枥枧枨枪枫枭柜柠柽栀栅标栈栉栊栋栌栎栏树栖样栾桊桠桡桢档桤桥桦桧桨桩梦梼梾检棂椁椟椠椤椭楼榄榇榈榉槚槛槟槠横樯樱橥橱橹橼檐檩欢欤欧歼殁殇残殒殓殚殡殴毁毂毕毙毡毵氇气氢氩氲汇汉污汤汹沓沟没沣沤沥沦沧沨沩沪沵泞泪泶泷泸泺泻泼泽泾洁洒洼浃浅浆浇浈浉浊测浍济浏浐浑浒浓浔浕涂涌涛涝涞涟涠涡涢涣涤润涧涨涩淀渊渌渍渎渐渑渔渖渗温游湾湿溃溅溆溇滗滚滞滟滠满滢滤滥滦滨滩滪漤潆潇潋潍潜潴澜濑濒灏灭灯灵灾灿炀炉炖炜炝点炼炽烁烂烃烛烟烦烧烨烩烫烬热焕焖焘煅煳熘爱爷牍牦牵牺犊犟状犷犸犹狈狍狝狞独狭狮狯狰狱狲猃猎猕猡猪猫猬献獭玑玙玚玛玮环现玱玺珉珏珐珑珰珲琎琏琐琼瑶瑷璇璎瓒瓮瓯电画畅畲畴疖疗疟疠疡疬疮疯疱疴痈痉痒痖痨痪痫痴瘅瘆瘗瘘瘪瘫瘾瘿癞癣癫癯皑皱皲盏盐监盖盗盘眍眦眬着睁睐睑瞒瞩矫矶矾矿砀码砖砗砚砜砺砻砾础硁硅硕硖硗硙硚确硷碍碛碜碱碹磙礼祎祢祯祷祸禀禄禅离秃秆种积称秽秾稆税稣稳穑穷窃窍窑窜窝窥窦窭竖竞笃笋笔笕笺笼笾筑筚筛筜筝筹签简箓箦箧箨箩箪箫篑篓篮篱簖籁籴类籼粜粝粤粪粮糁糇紧絷纟纠纡红纣纤纥约级纨纩纪纫纬纭纮纯纰纱纲纳纴纵纶纷纸纹纺纻纼纽纾线绀绁绂练组绅细织终绉绊绋绌绍绎经绐绑绒结绔绕绖绗绘给绚绛络绝绞统绠绡绢绣绤绥绦继绨绩绪绫绬续绮绯绰绱绲绳维绵绶绷绸绹绺绻综绽绾绿缀缁缂缃缄缅缆缇缈缉缊缋缌缍缎缏缐缑缒缓缔缕编缗缘缙缚缛缜缝缞缟缠缡缢缣缤缥缦缧缨缩缪缫缬缭缮缯缰缱缲缳缴缵罂网罗罚罢罴羁羟羡翘翙翚耢耧耸耻聂聋职聍联聩聪肃肠肤肷肾肿胀胁胆胜胧胨胪胫胶脉脍脏脐脑脓脔脚脱脶脸腊腌腘腭腻腼腽腾膑臜舆舣舰舱舻艰艳艹艺节芈芗芜芦苁苇苈苋苌苍苎苏苘苹茎茏茑茔茕茧荆荐荙荚荛荜荞荟荠荡荣荤荥荦荧荨荩荪荫荬荭荮药莅莜莱莲莳莴莶获莸莹莺莼萚萝萤营萦萧萨葱蒇蒉蒋蒌蓝蓟蓠蓣蓥蓦蔷蔹蔺蔼蕲蕴薮藁藓虏虑虚虫虬虮虽虾虿蚀蚁蚂蚕蚝蚬蛊蛎蛏蛮蛰蛱蛲蛳蛴蜕蜗蜡蝇蝈蝉蝎蝼蝾螀螨蟏衅衔补衬衮袄袅袆袜袭袯装裆裈裢裣裤裥褛褴襁襕见观觃规觅视觇览觉觊觋觌觍觎觏觐觑觞触觯詟誉誊讠计订讣认讥讦讧讨让讪讫训议讯记讱讲讳讴讵讶讷许讹论讻讼讽设访诀证诂诃评诅识诇诈诉诊诋诌词诎诏诐译诒诓诔试诖诗诘诙诚诛诜话诞诟诠诡询诣诤该详诧诨诩诪诫诬语诮误诰诱诲诳说诵诶请诸诹诺读诼诽课诿谀谁谂调谄谅谆谇谈谊谋谌谍谎谏谐谑谒谓谔谕谖谗谘谙谚谛谜谝谞谟谠谡谢谣谤谥谦谧谨谩谪谫谬谭谮谯谰谱谲谳谴谵谶谷豮贝贞负贠贡财责贤败账货质贩贪贫贬购贮贯贰贱贲贳贴贵贶贷贸费贺贻贼贽贾贿赀赁赂赃资赅赆赇赈赉赊赋赌赍赎赏赐赑赒赓赔赕赖赗赘赙赚赛赜赝赞赟赠赡赢赣赪赵赶趋趱趸跃跄跖跞践跶跷跸跹跻踊踌踪踬踯蹑蹒蹰蹿躏躜躯车轧轨轩轪轫转轭轮软轰轱轲轳轴轵轶轷轸轹轺轻轼载轾轿辀辁辂较辄辅辆辇辈辉辊辋辌辍辎辏辐辑辒输辔辕辖辗辘辙辚辞辩辫边辽达迁过迈运还这进远违连迟迩迳迹适选逊递逦逻遗遥邓邝邬邮邹邺邻郁郄郏郐郑郓郦郧郸酝酦酱酽酾酿释里鉅鉴銮錾钆钇针钉钊钋钌钍钎钏钐钑钒钓钔钕钖钗钘钙钚钛钝钞钟钠钡钢钣钤钥钦钧钨钩钪钫钬钭钮钯钰钱钲钳钴钵钶钷钸钹钺钻钼钽钾钿铀铁铂铃铄铅铆铈铉铊铋铍铎铏铐铑铒铕铗铘铙铚铛铜铝铞铟铠铡铢铣铤铥铦铧铨铪铫铬铭铮铯铰铱铲铳铴铵银铷铸铹铺铻铼铽链铿销锁锂锃锄锅锆锇锈锉锊锋锌锍锎锏锐锑锒锓锔锕锖锗错锚锜锞锟锠锡锢锣锤锥锦锨锩锫锬锭键锯锰锱锲锳锴锵锶锷锸锹锺锻锼锽锾锿镀镁镂镃镆镇镈镉镊镌镍镎镏镐镑镒镕镖镗镙镚镛镜镝镞镟镠镡镢镣镤镥镦镧镨镩镪镫镬镭镮镯镰镱镲镳镴镶长门闩闪闫闬闭问闯闰闱闲闳间闵闶闷闸闹闺闻闼闽闾闿阀阁阂阃阄阅阆阇阈阉阊阋阌阍阎阏阐阑阒阓阔阕阖阗阘阙阚阛队阳阴阵阶际陆陇陈陉陕陧陨险随隐隶隽难雏雠雳雾霁霉霭靓静靥鞑鞒鞯鞴韦韧韨韩韪韫韬韵页顶顷顸项顺须顼顽顾顿颀颁颂颃预颅领颇颈颉颊颋颌颍颎颏颐频颒颓颔颕颖颗题颙颚颛颜额颞颟颠颡颢颣颤颥颦颧风飏飐飑飒飓飔飕飖飗飘飙飚飞飨餍饤饥饦饧饨饩饪饫饬饭饮饯饰饱饲饳饴饵饶饷饸饹饺饻饼饽饾饿馀馁馂馃馄馅馆馇馈馉馊馋馌馍馎馏馐馑馒馓馔馕马驭驮驯驰驱驲驳驴驵驶驷驸驹驺驻驼驽驾驿骀骁骂骃骄骅骆骇骈骉骊骋验骍骎骏骐骑骒骓骔骕骖骗骘骙骚骛骜骝骞骟骠骡骢骣骤骥骦骧髅髋髌鬓魇魉鱼鱽鱾鱿鲀鲁鲂鲄鲅鲆鲇鲈鲉鲊鲋鲌鲍鲎鲏鲐鲑鲒鲓鲔鲕鲖鲗鲘鲙鲚鲛鲜鲝鲞鲟鲠鲡鲢鲣鲤鲥鲦鲧鲨鲩鲪鲫鲬鲭鲮鲯鲰鲱鲲鲳鲴鲵鲶鲷鲸鲹鲺鲻鲼鲽鲾鲿鳀鳁鳂鳃鳄鳅鳆鳇鳈鳉鳊鳋鳌鳍鳎鳏鳐鳑鳒鳓鳔鳕鳖鳗鳘鳙鳛鳜鳝鳞鳟鳠鳡鳢鳣鸟鸠鸡鸢鸣鸤鸥鸦鸧鸨鸩鸪鸫鸬鸭鸮鸯鸰鸱鸲鸳鸴鸵鸶鸷鸸鸹鸺鸻鸼鸽鸾鸿鹀鹁鹂鹃鹄鹅鹆鹇鹈鹉鹊鹋鹌鹍鹎鹏鹐鹑鹒鹓鹔鹕鹖鹗鹘鹚鹛鹜鹝鹞鹟鹠鹡鹢鹣鹤鹥鹦鹧鹨鹩鹪鹫鹬鹭鹯鹰鹱鹲鹳鹴鹾麦麸黄黉黡黩黪黾鼋鼌鼍鼗鼹齄齐齑齿龀龁龂龃龄龅龆龇龈龉龊龋龌龙龚龛龟志制咨只里系范松没尝尝闹面准钟别闲干尽脏拼]/gu;\n\t\tconst tradRegex =\n\t\t\t/[萬與醜專業叢東絲丟兩嚴喪個爿豐臨為麗舉麼義烏樂喬習鄉書買亂爭於虧雲亙亞產畝親褻嚲億僅從侖倉儀們價眾優夥會傴傘偉傳傷倀倫傖偽佇體餘傭僉俠侶僥偵側僑儈儕儂俁儔儼倆儷儉債傾傯僂僨償儻儐儲儺兒兌兗黨蘭關興茲養獸囅內岡冊寫軍農塚馮衝決況凍淨淒涼淩減湊凜幾鳳鳧憑凱擊氹鑿芻劃劉則剛創刪別剗剄劊劌剴劑剮劍剝劇勸辦務勱動勵勁勞勢勳猛勩勻匭匱區醫華協單賣盧鹵臥衛卻巹廠廳曆厲壓厭厙廁廂厴廈廚廄廝縣參靉靆雙發變敘疊葉號歎嘰籲後嚇呂嗎唚噸聽啟吳嘸囈嘔嚦唄員咼嗆嗚詠哢嚨嚀噝吒噅鹹呱響啞噠嘵嗶噦嘩噲嚌噥喲嘜嗊嘮啢嗩唕喚呼嘖嗇囀齧囉嘽嘯噴嘍嚳囁嗬噯噓嚶囑嚕劈囂謔團園囪圍圇國圖圓聖壙場阪壞塊堅壇壢壩塢墳墜壟壟壚壘墾坰堊墊埡墶壋塏堖塒塤堝墊垵塹墮壪牆壯聲殼壺壼處備複夠頭誇夾奪奩奐奮獎奧妝婦媽嫵嫗媯姍薑婁婭嬈嬌孌娛媧嫻嫿嬰嬋嬸媼嬡嬪嬙嬤孫學孿寧寶實寵審憲宮寬賓寢對尋導壽將爾塵堯尷屍盡層屭屜屆屬屢屨嶼歲豈嶇崗峴嶴嵐島嶺嶽崠巋嶨嶧峽嶢嶠崢巒嶗崍嶮嶄嶸嶔崳嶁脊巔鞏巰幣帥師幃帳簾幟帶幀幫幬幘幗冪襆幹並廣莊慶廬廡庫應廟龐廢廎廩開異棄張彌弳彎彈強歸當錄彠彥徹徑徠禦憶懺憂愾懷態慫憮慪悵愴憐總懟懌戀懇惡慟懨愷惻惱惲悅愨懸慳憫驚懼慘懲憊愜慚憚慣湣慍憤憒願懾憖怵懣懶懍戇戔戲戧戰戩戶紮撲扡執擴捫掃揚擾撫拋摶摳掄搶護報擔擬攏揀擁攔擰撥擇掛摯攣掗撾撻挾撓擋撟掙擠揮撏撈損撿換搗據撚擄摑擲撣摻摜摣攬撳攙擱摟攪攜攝攄擺搖擯攤攖撐攆擷擼攛擻攢敵斂數齋斕鬥斬斷無舊時曠暘曇晝曨顯晉曬曉曄暈暉暫曖劄術樸機殺雜權條來楊榪傑極構樅樞棗櫪梘棖槍楓梟櫃檸檉梔柵標棧櫛櫳棟櫨櫟欄樹棲樣欒棬椏橈楨檔榿橋樺檜槳樁夢檮棶檢欞槨櫝槧欏橢樓欖櫬櫚櫸檟檻檳櫧橫檣櫻櫫櫥櫓櫞簷檁歡歟歐殲歿殤殘殞殮殫殯毆毀轂畢斃氈毿氌氣氫氬氳彙漢汙湯洶遝溝沒灃漚瀝淪滄渢溈滬濔濘淚澩瀧瀘濼瀉潑澤涇潔灑窪浹淺漿澆湞溮濁測澮濟瀏滻渾滸濃潯濜塗湧濤澇淶漣潿渦溳渙滌潤澗漲澀澱淵淥漬瀆漸澠漁瀋滲溫遊灣濕潰濺漵漊潷滾滯灩灄滿瀅濾濫灤濱灘澦濫瀠瀟瀲濰潛瀦瀾瀨瀕灝滅燈靈災燦煬爐燉煒熗點煉熾爍爛烴燭煙煩燒燁燴燙燼熱煥燜燾煆糊溜愛爺牘犛牽犧犢強狀獷獁猶狽麅獮獰獨狹獅獪猙獄猻獫獵獼玀豬貓蝟獻獺璣璵瑒瑪瑋環現瑲璽瑉玨琺瓏璫琿璡璉瑣瓊瑤璦璿瓔瓚甕甌電畫暢佘疇癤療瘧癘瘍鬁瘡瘋皰屙癰痙癢瘂癆瘓癇癡癉瘮瘞瘺癟癱癮癭癩癬癲臒皚皺皸盞鹽監蓋盜盤瞘眥矓著睜睞瞼瞞矚矯磯礬礦碭碼磚硨硯碸礪礱礫礎硜矽碩硤磽磑礄確鹼礙磧磣堿镟滾禮禕禰禎禱禍稟祿禪離禿稈種積稱穢穠穭稅穌穩穡窮竊竅窯竄窩窺竇窶豎競篤筍筆筧箋籠籩築篳篩簹箏籌簽簡籙簀篋籜籮簞簫簣簍籃籬籪籟糴類秈糶糲粵糞糧糝餱緊縶糸糾紆紅紂纖紇約級紈纊紀紉緯紜紘純紕紗綱納紝縱綸紛紙紋紡紵紖紐紓線紺絏紱練組紳細織終縐絆紼絀紹繹經紿綁絨結絝繞絰絎繪給絢絳絡絕絞統綆綃絹繡綌綏絛繼綈績緒綾緓續綺緋綽緔緄繩維綿綬繃綢綯綹綣綜綻綰綠綴緇緙緗緘緬纜緹緲緝縕繢緦綞緞緶線緱縋緩締縷編緡緣縉縛縟縝縫縗縞纏縭縊縑繽縹縵縲纓縮繆繅纈繚繕繒韁繾繰繯繳纘罌網羅罰罷羆羈羥羨翹翽翬耮耬聳恥聶聾職聹聯聵聰肅腸膚膁腎腫脹脅膽勝朧腖臚脛膠脈膾髒臍腦膿臠腳脫腡臉臘醃膕齶膩靦膃騰臏臢輿艤艦艙艫艱豔艸藝節羋薌蕪蘆蓯葦藶莧萇蒼苧蘇檾蘋莖蘢蔦塋煢繭荊薦薘莢蕘蓽蕎薈薺蕩榮葷滎犖熒蕁藎蓀蔭蕒葒葤藥蒞蓧萊蓮蒔萵薟獲蕕瑩鶯蓴蘀蘿螢營縈蕭薩蔥蕆蕢蔣蔞藍薊蘺蕷鎣驀薔蘞藺藹蘄蘊藪槁蘚虜慮虛蟲虯蟣雖蝦蠆蝕蟻螞蠶蠔蜆蠱蠣蟶蠻蟄蛺蟯螄蠐蛻蝸蠟蠅蟈蟬蠍螻蠑螿蟎蠨釁銜補襯袞襖嫋褘襪襲襏裝襠褌褳襝褲襇褸襤繈襴見觀覎規覓視覘覽覺覬覡覿覥覦覯覲覷觴觸觶讋譽謄訁計訂訃認譏訐訌討讓訕訖訓議訊記訒講諱謳詎訝訥許訛論訩訟諷設訪訣證詁訶評詛識詗詐訴診詆謅詞詘詔詖譯詒誆誄試詿詩詰詼誠誅詵話誕詬詮詭詢詣諍該詳詫諢詡譸誡誣語誚誤誥誘誨誑說誦誒請諸諏諾讀諑誹課諉諛誰諗調諂諒諄誶談誼謀諶諜謊諫諧謔謁謂諤諭諼讒諮諳諺諦謎諞諝謨讜謖謝謠謗諡謙謐謹謾謫譾謬譚譖譙讕譜譎讞譴譫讖穀豶貝貞負貟貢財責賢敗賬貨質販貪貧貶購貯貫貳賤賁貰貼貴貺貸貿費賀貽賊贄賈賄貲賃賂贓資賅贐賕賑賚賒賦賭齎贖賞賜贔賙賡賠賧賴賵贅賻賺賽賾贗讚贇贈贍贏贛赬趙趕趨趲躉躍蹌蹠躒踐躂蹺蹕躚躋踴躊蹤躓躑躡蹣躕躥躪躦軀車軋軌軒軑軔轉軛輪軟轟軲軻轤軸軹軼軤軫轢軺輕軾載輊轎輈輇輅較輒輔輛輦輩輝輥輞輬輟輜輳輻輯轀輸轡轅轄輾轆轍轔辭辯辮邊遼達遷過邁運還這進遠違連遲邇逕跡適選遜遞邐邏遺遙鄧鄺鄔郵鄒鄴鄰鬱郤郟鄶鄭鄆酈鄖鄲醞醱醬釅釃釀釋裏钜鑒鑾鏨釓釔針釘釗釙釕釷釺釧釤鈒釩釣鍆釹鍚釵鈃鈣鈈鈦鈍鈔鍾鈉鋇鋼鈑鈐鑰欽鈞鎢鉤鈧鈁鈥鈄鈕鈀鈺錢鉦鉗鈷缽鈳鉕鈽鈸鉞鑽鉬鉭鉀鈿鈾鐵鉑鈴鑠鉛鉚鈰鉉鉈鉍鈹鐸鉶銬銠鉺銪鋏鋣鐃銍鐺銅鋁銱銦鎧鍘銖銑鋌銩銛鏵銓鉿銚鉻銘錚銫鉸銥鏟銃鐋銨銀銣鑄鐒鋪鋙錸鋱鏈鏗銷鎖鋰鋥鋤鍋鋯鋨鏽銼鋝鋒鋅鋶鐦鐧銳銻鋃鋟鋦錒錆鍺錯錨錡錁錕錩錫錮鑼錘錐錦鍁錈錇錟錠鍵鋸錳錙鍥鍈鍇鏘鍶鍔鍤鍬鍾鍛鎪鍠鍰鎄鍍鎂鏤鎡鏌鎮鎛鎘鑷鐫鎳鎿鎦鎬鎊鎰鎔鏢鏜鏍鏰鏞鏡鏑鏃鏇鏐鐔钁鐐鏷鑥鐓鑭鐠鑹鏹鐙鑊鐳鐶鐲鐮鐿鑔鑣鑞鑲長門閂閃閆閈閉問闖閏闈閑閎間閔閌悶閘鬧閨聞闥閩閭闓閥閣閡閫鬮閱閬闍閾閹閶鬩閿閽閻閼闡闌闃闠闊闋闔闐闒闕闞闤隊陽陰陣階際陸隴陳陘陝隉隕險隨隱隸雋難雛讎靂霧霽黴靄靚靜靨韃鞽韉韝韋韌韍韓韙韞韜韻頁頂頃頇項順須頊頑顧頓頎頒頌頏預顱領頗頸頡頰頲頜潁熲頦頤頻頮頹頷頴穎顆題顒顎顓顏額顳顢顛顙顥纇顫顬顰顴風颺颭颮颯颶颸颼颻飀飄飆飆飛饗饜飣饑飥餳飩餼飪飫飭飯飲餞飾飽飼飿飴餌饒餉餄餎餃餏餅餑餖餓餘餒餕餜餛餡館餷饋餶餿饞饁饃餺餾饈饉饅饊饌饢馬馭馱馴馳驅馹駁驢駔駛駟駙駒騶駐駝駑駕驛駘驍罵駰驕驊駱駭駢驫驪騁驗騂駸駿騏騎騍騅騌驌驂騙騭騤騷騖驁騮騫騸驃騾驄驏驟驥驦驤髏髖髕鬢魘魎魚魛魢魷魨魯魴魺鮁鮃鯰鱸鮋鮓鮒鮊鮑鱟鮍鮐鮭鮚鮳鮪鮞鮦鰂鮜鱠鱭鮫鮮鮺鯗鱘鯁鱺鰱鰹鯉鰣鰷鯀鯊鯇鮶鯽鯒鯖鯪鯕鯫鯡鯤鯧鯝鯢鯰鯛鯨鯵鯴鯔鱝鰈鰏鱨鯷鰮鰃鰓鱷鰍鰒鰉鰁鱂鯿鰠鼇鰭鰨鰥鰩鰟鰜鰳鰾鱈鱉鰻鰵鱅鰼鱖鱔鱗鱒鱯鱤鱧鱣鳥鳩雞鳶鳴鳲鷗鴉鶬鴇鴆鴣鶇鸕鴨鴞鴦鴒鴟鴝鴛鴬鴕鷥鷙鴯鴰鵂鴴鵃鴿鸞鴻鵐鵓鸝鵑鵠鵝鵒鷳鵜鵡鵲鶓鵪鶤鵯鵬鵮鶉鶊鵷鷫鶘鶡鶚鶻鶿鶥鶩鷊鷂鶲鶹鶺鷁鶼鶴鷖鸚鷓鷚鷯鷦鷲鷸鷺鸇鷹鸌鸏鸛鸘鹺麥麩黃黌黶黷黲黽黿鼂鼉鞀鼴齇齊齏齒齔齕齗齟齡齙齠齜齦齬齪齲齷龍龔龕龜誌製谘隻裡係範鬆冇嚐嘗鬨麵準鐘彆閒乾儘臟拚]/gu;\n\t\tconst hanziRegex = /\\p{Script=Han}/gu;\n\n\t\tconst cjkMatch = rawLyrics.match(\n\t\t\tnew RegExp(`${kanaRegex.source}|${hanziRegex.source}|${hangulRegex.source}|${/\\p{Unified_Ideograph}/gu.source}`, \"gu\")\n\t\t);\n\n\t\tif (!cjkMatch) return;\n\n\t\tconst kanaCount = cjkMatch.filter((glyph) => kanaRegex.test(glyph)).length;\n\t\tconst hanziCount = cjkMatch.filter((glyph) => hanziRegex.test(glyph)).length;\n\t\tconst simpCount = cjkMatch.filter((glyph) => simpRegex.test(glyph)).length;\n\t\tconst tradCount = cjkMatch.filter((glyph) => tradRegex.test(glyph)).length;\n\n\t\tconst kanaPercentage = kanaCount / cjkMatch.length;\n\t\tconst hanziPercentage = hanziCount / cjkMatch.length;\n\t\tconst simpPercentage = simpCount / cjkMatch.length;\n\t\tconst tradPercentage = tradCount / cjkMatch.length;\n\n\t\tif (cjkMatch.filter((glyph) => hangulRegex.test(glyph)).length !== 0) {\n\t\t\treturn \"ko\";\n\t\t}\n\n\t\tif (((kanaPercentage - hanziPercentage + 1) / 2) * 100 >= CONFIG.visual[\"ja-detect-threshold\"]) {\n\t\t\treturn \"ja\";\n\t\t}\n\n\t\treturn ((simpPercentage - tradPercentage + 1) / 2) * 100 >= CONFIG.visual[\"hans-detect-threshold\"] ? \"zh-hans\" : \"zh-hant\";\n\t},\n\tprocessTranslatedLyrics(translated, original) {\n\t\treturn original.map((lyric, index) => ({\n\t\t\tstartTime: lyric.startTime || 0,\n\t\t\ttext: this.rubyTextToReact(translated[index]),\n\t\t\toriginalText: lyric.text,\n\t\t}));\n\t},\n\t/** It seems that this function is not being used, but I'll keep it just in case it’s needed in the future.*/\n\tprocessTranslatedOriginalLyrics(lyrics, synced) {\n\t\tconst data = [];\n\t\tconst dataSouce = {};\n\n\t\tfor (const item of lyrics) {\n\t\t\tdataSouce[item.startTime] = { translate: item.text };\n\t\t}\n\n\t\tfor (const time in synced) {\n\t\t\tdataSouce[item.startTime] = {\n\t\t\t\t...dataSouce[item.startTime],\n\t\t\t\ttext: item.text,\n\t\t\t};\n\t\t}\n\n\t\tfor (const time in dataSouce) {\n\t\t\tconst item = dataSouce[time];\n\t\t\tconst lyric = {\n\t\t\t\tstartTime: time || 0,\n\t\t\t\ttext: this.rubyTextToOriginalReact(item.translate || item.text, item.text || item.translate),\n\t\t\t};\n\t\t\tdata.push(lyric);\n\t\t}\n\n\t\treturn data;\n\t},\n\trubyTextToOriginalReact(translated, syncedText) {\n\t\tconst react = Spicetify.React;\n\t\treturn react.createElement(\"p1\", null, [react.createElement(\"ruby\", {}, syncedText, react.createElement(\"rt\", null, translated))]);\n\t},\n\trubyTextToReact(s) {\n\t\tconst react = Spicetify.React;\n\t\tconst rubyElems = s.split(\"<ruby>\");\n\t\tconst reactChildren = [];\n\n\t\treactChildren.push(rubyElems[0]);\n\t\tfor (let i = 1; i < rubyElems.length; i++) {\n\t\t\tconst kanji = rubyElems[i].split(\"<rp>\")[0];\n\t\t\tconst furigana = rubyElems[i].split(\"<rt>\")[1].split(\"</rt>\")[0];\n\t\t\treactChildren.push(react.createElement(\"ruby\", null, kanji, react.createElement(\"rt\", null, furigana)));\n\n\t\t\treactChildren.push(rubyElems[i].split(\"</ruby>\")[1]);\n\t\t}\n\t\treturn react.createElement(\"p1\", null, reactChildren);\n\t},\n\tformatTime(timestamp) {\n\t\tif (Number.isNaN(timestamp)) return timestamp.toString();\n\t\tlet minutes = Math.trunc(timestamp / 60000);\n\t\tlet seconds = ((timestamp - minutes * 60000) / 1000).toFixed(2);\n\n\t\tif (minutes < 10) minutes = `0${minutes}`;\n\t\tif (seconds < 10) seconds = `0${seconds}`;\n\n\t\treturn `${minutes}:${seconds}`;\n\t},\n\tformatTextWithTimestamps(text, startTime = 0) {\n\t\tif (text.props?.children) {\n\t\t\treturn text.props.children\n\t\t\t\t.map((child) => {\n\t\t\t\t\tif (typeof child === \"string\") {\n\t\t\t\t\t\treturn child;\n\t\t\t\t\t}\n\t\t\t\t\tif (child.props?.children) {\n\t\t\t\t\t\treturn child.props?.children[0];\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.join(\"\");\n\t\t}\n\t\tif (Array.isArray(text)) {\n\t\t\tlet wordTime = startTime;\n\t\t\treturn text\n\t\t\t\t.map((word) => {\n\t\t\t\t\twordTime += word.time;\n\t\t\t\t\treturn `${word.word}<${this.formatTime(wordTime)}>`;\n\t\t\t\t})\n\t\t\t\t.join(\"\");\n\t\t}\n\t\treturn text;\n\t},\n\tconvertParsedToLRC(lyrics, isBelow) {\n\t\tlet original = \"\";\n\t\tlet conver = \"\";\n\n\t\tif (isBelow) {\n\t\t\tfor (const line of lyrics) {\n\t\t\t\toriginal += `[${this.formatTime(line.startTime)}]${this.formatTextWithTimestamps(line.originalText, line.startTime)}\\n`;\n\t\t\t\tconver += `[${this.formatTime(line.startTime)}]${this.formatTextWithTimestamps(line.text, line.startTime)}\\n`;\n\t\t\t}\n\t\t} else {\n\t\t\tfor (const line of lyrics) {\n\t\t\t\toriginal += `[${this.formatTime(line.startTime)}]${this.formatTextWithTimestamps(line.text, line.startTime)}\\n`;\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\toriginal,\n\t\t\tconver,\n\t\t};\n\t},\n\tconvertParsedToUnsynced(lyrics, isBelow) {\n\t\tlet original = \"\";\n\t\tlet conver = \"\";\n\n\t\tif (isBelow) {\n\t\t\tfor (const line of lyrics) {\n\t\t\t\tif (typeof line.originalText === \"object\") {\n\t\t\t\t\toriginal += `${line.originalText?.props?.children?.[0]}\\n`;\n\t\t\t\t} else {\n\t\t\t\t\toriginal += `${line.originalText}\\n`;\n\t\t\t\t}\n\n\t\t\t\tif (typeof line.text === \"object\") {\n\t\t\t\t\tconver += `${line.text?.props?.children?.[0]}\\n`;\n\t\t\t\t} else {\n\t\t\t\t\tconver += `${line.text}\\n`;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor (const line of lyrics) {\n\t\t\t\tif (typeof line.text === \"object\") {\n\t\t\t\t\toriginal += `${line.text?.props?.children?.[0]}\\n`;\n\t\t\t\t} else {\n\t\t\t\t\toriginal += `${line.text}\\n`;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\toriginal,\n\t\t\tconver,\n\t\t};\n\t},\n\tparseLocalLyrics(lyrics) {\n\t\t// Preprocess lyrics by removing [tags] and empty lines\n\t\tconst lines = lyrics\n\t\t\t.replaceAll(/\\[[a-zA-Z]+:.+\\]/g, \"\")\n\t\t\t.trim()\n\t\t\t.split(\"\\n\");\n\n\t\tconst syncedTimestamp = /\\[([0-9:.]+)\\]/;\n\t\tconst karaokeTimestamp = /<([0-9:.]+)>/;\n\n\t\tconst unsynced = [];\n\n\t\tconst isSynced = lines[0].match(syncedTimestamp);\n\t\tconst synced = isSynced ? [] : null;\n\n\t\tconst isKaraoke = lines[0].match(karaokeTimestamp);\n\t\tconst karaoke = isKaraoke ? [] : null;\n\n\t\tfunction timestampToMs(timestamp) {\n\t\t\tconst [minutes, seconds] = timestamp.replace(/\\[\\]<>/, \"\").split(\":\");\n\t\t\treturn Number(minutes) * 60 * 1000 + Number(seconds) * 1000;\n\t\t}\n\n\t\tfunction parseKaraokeLine(line, startTime) {\n\t\t\tlet wordTime = timestampToMs(startTime);\n\t\t\tconst karaokeLine = [];\n\t\t\tconst karaoke = line.matchAll(/(\\S+ ?)<([0-9:.]+)>/g);\n\t\t\tfor (const match of karaoke) {\n\t\t\t\tconst word = match[1];\n\t\t\t\tconst time = match[2];\n\t\t\t\tkaraokeLine.push({ word, time: timestampToMs(time) - wordTime });\n\t\t\t\twordTime = timestampToMs(time);\n\t\t\t}\n\t\t\treturn karaokeLine;\n\t\t}\n\n\t\tfor (const [i, line] of lines.entries()) {\n\t\t\tconst time = line.match(syncedTimestamp)?.[1];\n\t\t\tlet lyricContent = line.replace(syncedTimestamp, \"\").trim();\n\t\t\tconst lyric = lyricContent.replaceAll(/<([0-9:.]+)>/g, \"\").trim();\n\n\t\t\tif (line.trim() !== \"\") {\n\t\t\t\tif (isKaraoke) {\n\t\t\t\t\tif (!lyricContent.endsWith(\">\")) {\n\t\t\t\t\t\t// For some reason there are a variety of formats for karaoke lyrics, Wikipedia is also inconsisent in their examples\n\t\t\t\t\t\tconst endTime = lines[i + 1]?.match(syncedTimestamp)?.[1] || this.formatTime(Number(Spicetify.Player.data.item.metadata.duration));\n\t\t\t\t\t\tlyricContent += `<${endTime}>`;\n\t\t\t\t\t}\n\t\t\t\t\tconst karaokeLine = parseKaraokeLine(lyricContent, time);\n\t\t\t\t\tkaraoke.push({ text: karaokeLine, startTime: timestampToMs(time) });\n\t\t\t\t}\n\t\t\t\tisSynced && time && synced.push({ text: lyric || \"♪\", startTime: timestampToMs(time) });\n\t\t\t\tunsynced.push({ text: lyric || \"♪\" });\n\t\t\t}\n\t\t}\n\n\t\treturn { synced, unsynced, karaoke };\n\t},\n\tprocessLyrics(lyrics) {\n\t\treturn lyrics\n\t\t\t.replace(/　| /g, \"\") // Remove space\n\t\t\t.replace(/[!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~？！，。、《》【】「」]/g, \"\"); // Remove punctuation\n\t},\n};\n"
  },
  {
    "path": "CustomApps/lyrics-plus/index.js",
    "content": "// Run \"npm i @types/react\" to have this type package available in workspace\n/// <reference types=\"react\" />\n/// <reference path=\"../../globals.d.ts\" />\n\n/** @type {React} */\nconst react = Spicetify.React;\nconst { useState, useEffect, useCallback, useMemo, useRef } = react;\n/** @type {import(\"react\").ReactDOM} */\nconst spotifyVersion = Spicetify.Platform.version;\n\n// Define a function called \"render\" to specify app entry point\n// This function will be used to mount app to main view.\nfunction render() {\n\treturn react.createElement(LyricsContainer, null);\n}\n\nfunction getConfig(name, defaultVal = true) {\n\tconst value = localStorage.getItem(name);\n\treturn value ? value === \"true\" : defaultVal;\n}\n\nconst APP_NAME = \"lyrics-plus\";\nconst MUSIXMATCH_TRANSLATION_PREFIX_DEFAULT = \"musixmatchTranslation:\";\nconst MUSIXMATCH_TRANSLATION_PREFIX_GLOBAL_KEY = \"__lyricsPlusMusixmatchTranslationPrefix\";\nconst MUSIXMATCH_TRANSLATION_FETCH_MESSAGE = \"Fetching translation...\";\nconst MUSIXMATCH_TRANSLATION_FETCH_FAILED_MESSAGE = \"Failed to fetch translation, please try again in a few minutes\";\nconst MUSIXMATCH_TRANSLATION_PREFIX =\n\ttypeof window !== \"undefined\" && typeof window[MUSIXMATCH_TRANSLATION_PREFIX_GLOBAL_KEY] === \"string\"\n\t\t? window[MUSIXMATCH_TRANSLATION_PREFIX_GLOBAL_KEY]\n\t\t: MUSIXMATCH_TRANSLATION_PREFIX_DEFAULT;\n\nif (typeof window !== \"undefined\") {\n\twindow[MUSIXMATCH_TRANSLATION_PREFIX_GLOBAL_KEY] = MUSIXMATCH_TRANSLATION_PREFIX;\n}\n\nconst KARAOKE = 0;\nconst SYNCED = 1;\nconst UNSYNCED = 2;\nconst GENIUS = 3;\n\nconst CONFIG = {\n\tvisual: {\n\t\t\"playbar-button\": getConfig(\"lyrics-plus:visual:playbar-button\", false),\n\t\tcolorful: getConfig(\"lyrics-plus:visual:colorful\"),\n\t\tnoise: getConfig(\"lyrics-plus:visual:noise\"),\n\t\t\"background-color\": localStorage.getItem(\"lyrics-plus:visual:background-color\") || \"var(--spice-main)\",\n\t\t\"active-color\": localStorage.getItem(\"lyrics-plus:visual:active-color\") || \"var(--spice-text)\",\n\t\t\"inactive-color\": localStorage.getItem(\"lyrics-plus:visual:inactive-color\") || \"rgba(var(--spice-rgb-subtext),0.5)\",\n\t\t\"highlight-color\": localStorage.getItem(\"lyrics-plus:visual:highlight-color\") || \"var(--spice-button)\",\n\t\talignment: localStorage.getItem(\"lyrics-plus:visual:alignment\") || \"center\",\n\t\t\"lines-before\": localStorage.getItem(\"lyrics-plus:visual:lines-before\") || \"0\",\n\t\t\"lines-after\": localStorage.getItem(\"lyrics-plus:visual:lines-after\") || \"2\",\n\t\t\"font-size\": localStorage.getItem(\"lyrics-plus:visual:font-size\") || \"32\",\n\t\t\"translate:translated-lyrics-source\": localStorage.getItem(\"lyrics-plus:visual:translate:translated-lyrics-source\") || \"none\",\n\t\t\"translate:display-mode\": localStorage.getItem(\"lyrics-plus:visual:translate:display-mode\") || \"replace\",\n\t\t\"translate:detect-language-override\": localStorage.getItem(\"lyrics-plus:visual:translate:detect-language-override\") || \"off\",\n\t\t\"translation-mode:japanese\": localStorage.getItem(\"lyrics-plus:visual:translation-mode:japanese\") || \"furigana\",\n\t\t\"translation-mode:korean\": localStorage.getItem(\"lyrics-plus:visual:translation-mode:korean\") || \"romaja\",\n\t\t\"translation-mode:chinese\": localStorage.getItem(\"lyrics-plus:visual:translation-mode:chinese\") || \"cn\",\n\t\ttranslate: getConfig(\"lyrics-plus:visual:translate\", false),\n\t\t\"ja-detect-threshold\": localStorage.getItem(\"lyrics-plus:visual:ja-detect-threshold\") || \"40\",\n\t\t\"hans-detect-threshold\": localStorage.getItem(\"lyrics-plus:visual:hans-detect-threshold\") || \"40\",\n\t\t\"musixmatch-translation-language\": localStorage.getItem(\"lyrics-plus:visual:musixmatch-translation-language\") || \"none\",\n\t\t\"fade-blur\": getConfig(\"lyrics-plus:visual:fade-blur\"),\n\t\t\"fullscreen-key\": localStorage.getItem(\"lyrics-plus:visual:fullscreen-key\") || \"f12\",\n\t\t\"show-performers\": getConfig(\"lyrics-plus:visual:show-performers\", true),\n\t\t\"synced-compact\": getConfig(\"lyrics-plus:visual:synced-compact\"),\n\t\t\"dual-genius\": getConfig(\"lyrics-plus:visual:dual-genius\"),\n\t\t\"global-delay\": Number(localStorage.getItem(\"lyrics-plus:visual:global-delay\")) || 0,\n\t\tdelay: 0,\n\t},\n\tproviders: {\n\t\tlrclib: {\n\t\t\ton: getConfig(\"lyrics-plus:provider:lrclib:on\"),\n\t\t\tdesc: \"Lyrics sourced from lrclib.net. Supports both synced and unsynced lyrics. LRCLIB is a free and open-source lyrics provider.\",\n\t\t\tmodes: [SYNCED, UNSYNCED],\n\t\t},\n\t\tmusixmatch: {\n\t\t\ton: getConfig(\"lyrics-plus:provider:musixmatch:on\"),\n\t\t\tdesc: \"Fully compatible with Spotify. Requires a token that can be retrieved from the official Musixmatch app. If you have problems with retrieving lyrics, try refreshing the token by clicking <code>Refresh Token</code> button. You may need to be forced to use your own CORS Proxy to use this provider.\",\n\t\t\ttoken: localStorage.getItem(\"lyrics-plus:provider:musixmatch:token\") || \"21051986b9886beabe1ce01c3ce94c96319411f8f2c122676365e3\",\n\t\t\tmodes: [KARAOKE, SYNCED, UNSYNCED],\n\t\t},\n\t\tspotify: {\n\t\t\ton: getConfig(\"lyrics-plus:provider:spotify:on\"),\n\t\t\tdesc: \"Lyrics sourced from official Spotify API.\",\n\t\t\tmodes: [SYNCED, UNSYNCED],\n\t\t},\n\t\tnetease: {\n\t\t\ton: getConfig(\"lyrics-plus:provider:netease:on\", false),\n\t\t\tdesc: \"Crowdsourced lyrics provider ran by Chinese developers and users.\",\n\t\t\tmodes: [KARAOKE, SYNCED, UNSYNCED],\n\t\t},\n\t\tgenius: {\n\t\t\ton: spotifyVersion >= \"1.2.31\" ? false : getConfig(\"lyrics-plus:provider:genius:on\"),\n\t\t\tdesc: \"Provide unsynced lyrics with insights from artists themselves. Genius is disabled and cannot be used as a provider on <code>1.2.31</code> and higher.\",\n\t\t\tmodes: [GENIUS],\n\t\t},\n\t\tlocal: {\n\t\t\ton: getConfig(\"lyrics-plus:provider:local:on\"),\n\t\t\tdesc: \"Provide lyrics from cache/local files loaded from previous Spotify sessions.\",\n\t\t\tmodes: [KARAOKE, SYNCED, UNSYNCED],\n\t\t},\n\t},\n\tprovidersOrder: localStorage.getItem(\"lyrics-plus:services-order\"),\n\tmodes: [\"karaoke\", \"synced\", \"unsynced\", \"genius\"],\n\tlocked: localStorage.getItem(\"lyrics-plus:lock-mode\") || \"-1\",\n};\n\ntry {\n\tCONFIG.providersOrder = JSON.parse(CONFIG.providersOrder);\n\tif (!Array.isArray(CONFIG.providersOrder) || Object.keys(CONFIG.providers).length !== CONFIG.providersOrder.length) {\n\t\tthrow \"\";\n\t}\n} catch {\n\tCONFIG.providersOrder = Object.keys(CONFIG.providers);\n\tlocalStorage.setItem(\"lyrics-plus:services-order\", JSON.stringify(CONFIG.providersOrder));\n}\n\nCONFIG.locked = Number.parseInt(CONFIG.locked);\nCONFIG.visual[\"lines-before\"] = Number.parseInt(CONFIG.visual[\"lines-before\"]);\nCONFIG.visual[\"lines-after\"] = Number.parseInt(CONFIG.visual[\"lines-after\"]);\nCONFIG.visual[\"font-size\"] = Number.parseInt(CONFIG.visual[\"font-size\"]);\nCONFIG.visual[\"ja-detect-threshold\"] = Number.parseInt(CONFIG.visual[\"ja-detect-threshold\"]);\nCONFIG.visual[\"hans-detect-threshold\"] = Number.parseInt(CONFIG.visual[\"hans-detect-threshold\"]);\n\nif (CONFIG.visual[\"translate:translated-lyrics-source\"] === \"musixmatchTranslation\") {\n\tconst language = CONFIG.visual[\"musixmatch-translation-language\"];\n\tconst normalizedLanguage = language && language !== \"none\" ? language : \"none\";\n\tconst upgradedValue = normalizedLanguage !== \"none\" ? `${MUSIXMATCH_TRANSLATION_PREFIX}${normalizedLanguage}` : \"none\";\n\tCONFIG.visual[\"translate:translated-lyrics-source\"] = upgradedValue;\n\tlocalStorage.setItem(`${APP_NAME}:visual:translate:translated-lyrics-source`, upgradedValue);\n}\n\nif (typeof CONFIG.visual[\"translate:translated-lyrics-source\"] === \"string\") {\n\tconst sourceValue = CONFIG.visual[\"translate:translated-lyrics-source\"];\n\tif (sourceValue.startsWith(MUSIXMATCH_TRANSLATION_PREFIX)) {\n\t\tconst language = sourceValue.slice(MUSIXMATCH_TRANSLATION_PREFIX.length) || \"none\";\n\t\tif (CONFIG.visual[\"musixmatch-translation-language\"] !== language) {\n\t\t\tCONFIG.visual[\"musixmatch-translation-language\"] = language;\n\t\t\tlocalStorage.setItem(`${APP_NAME}:visual:musixmatch-translation-language`, language);\n\t\t}\n\t}\n}\n\nif (\n\tCONFIG.visual.translate &&\n\ttypeof CONFIG.visual[\"translate:translated-lyrics-source\"] === \"string\" &&\n\tCONFIG.visual[\"translate:translated-lyrics-source\"] !== \"none\"\n) {\n\tCONFIG.visual.translate = false;\n\tlocalStorage.setItem(`${APP_NAME}:visual:translate`, \"false\");\n}\n\nlet CACHE = {};\n\nconst emptyState = {\n\tkaraoke: null,\n\tsynced: null,\n\tunsynced: null,\n\tgenius: null,\n\tgenius2: null,\n\tcurrentLyrics: null,\n\tmusixmatchAvailableTranslations: null,\n\tmusixmatchTrackId: null,\n\tmusixmatchTranslationLanguage: null,\n};\n\nlet lyricContainerUpdate;\nlet reloadLyrics;\nlet refreshMusixmatchTranslation;\n\nconst fontSizeLimit = { min: 16, max: 256, step: 4 };\n\nconst thresholdSizeLimit = { min: 0, max: 100, step: 5 };\n\nfunction resolveTranslationSource(source) {\n\tif (typeof source !== \"string\") {\n\t\treturn { key: source, language: null };\n\t}\n\n\tif (source.startsWith(MUSIXMATCH_TRANSLATION_PREFIX)) {\n\t\tconst language = source.slice(MUSIXMATCH_TRANSLATION_PREFIX.length) || null;\n\t\treturn { key: \"musixmatchTranslation\", language };\n\t}\n\n\treturn { key: source, language: null };\n}\n\nclass LyricsContainer extends react.Component {\n\tconstructor() {\n\t\tsuper();\n\t\tthis.state = {\n\t\t\tkaraoke: null,\n\t\t\tsynced: null,\n\t\t\tunsynced: null,\n\t\t\tgenius: null,\n\t\t\tgenius2: null,\n\t\t\tcurrentLyrics: null,\n\t\t\tromaji: null,\n\t\t\tfurigana: null,\n\t\t\thiragana: null,\n\t\t\thangul: null,\n\t\t\tromaja: null,\n\t\t\tkatakana: null,\n\t\t\tcn: null,\n\t\t\thk: null,\n\t\t\ttw: null,\n\t\t\tmusixmatchTranslation: null,\n\t\t\tmusixmatchTranslationLanguage: null,\n\t\t\tmusixmatchAvailableTranslations: [],\n\t\t\tmusixmatchTrackId: null,\n\t\t\tneteaseTranslation: null,\n\t\t\turi: \"\",\n\t\t\tprovider: \"\",\n\t\t\tcolors: {\n\t\t\t\tbackground: \"\",\n\t\t\t\tinactive: \"\",\n\t\t\t},\n\t\t\ttempo: \"0.25s\",\n\t\t\texplicitMode: -1,\n\t\t\tlockMode: CONFIG.locked,\n\t\t\tmode: -1,\n\t\t\tisLoading: false,\n\t\t\tversionIndex: 0,\n\t\t\tversionIndex2: 0,\n\t\t\tisFullscreen: false,\n\t\t\tisFADMode: false,\n\t\t\tisCached: false,\n\t\t\tlanguage: null,\n\t\t};\n\t\tthis.currentTrackUri = \"\";\n\t\tthis.nextTrackUri = \"\";\n\t\tthis.availableModes = [];\n\t\tthis.styleVariables = {};\n\t\tthis.fullscreenContainer = document.createElement(\"div\");\n\t\tthis.fullscreenContainer.id = \"lyrics-fullscreen-container\";\n\t\tthis.mousetrap = null;\n\t\tthis.containerRef = react.createRef(null);\n\t\tthis.translator = null;\n\t\tthis.initMoustrap();\n\t\t// Cache last state\n\t\tthis.languageOverride = CONFIG.visual[\"translate:detect-language-override\"];\n\t\tthis.translate = CONFIG.visual.translate;\n\t\tthis.reRenderLyricsPage = false;\n\t\tthis.displayMode = null;\n\t\tthis.currentMusixmatchLanguage = CONFIG.visual[\"musixmatch-translation-language\"];\n\t\tthis._musixmatchTranslationRequestId = null;\n\t}\n\n\tinfoFromTrack(track) {\n\t\tconst meta = track?.metadata;\n\t\tif (!meta) {\n\t\t\treturn null;\n\t\t}\n\t\treturn {\n\t\t\tduration: Number(meta.duration),\n\t\t\talbum: meta.album_title,\n\t\t\tartist: meta.artist_name,\n\t\t\ttitle: meta.title,\n\t\t\turi: track.uri,\n\t\t\timage: meta.image_url,\n\t\t};\n\t}\n\n\tasync fetchColors(uri) {\n\t\tlet vibrant = 0;\n\t\ttry {\n\t\t\ttry {\n\t\t\t\tconst { fetchExtractedColorForTrackEntity } = Spicetify.GraphQL.Definitions;\n\t\t\t\tconst { data } = await Spicetify.GraphQL.Request(fetchExtractedColorForTrackEntity, { uri });\n\t\t\t\tconst { hex } = data.trackUnion.albumOfTrack.coverArt.extractedColors.colorDark;\n\t\t\t\tvibrant = Number.parseInt(hex.replace(\"#\", \"\"), 16);\n\t\t\t} catch {\n\t\t\t\tconst colors = await Spicetify.CosmosAsync.get(`https://spclient.wg.spotify.com/colorextractor/v1/extract-presets?uri=${uri}&format=json`);\n\t\t\t\tvibrant = colors.entries[0].color_swatches.find((color) => color.preset === \"VIBRANT_NON_ALARMING\").color;\n\t\t\t}\n\t\t} catch {\n\t\t\tvibrant = 8747370;\n\t\t}\n\n\t\tthis.setState({\n\t\t\tcolors: {\n\t\t\t\tbackground: Utils.convertIntToRGB(vibrant),\n\t\t\t\tinactive: Utils.convertIntToRGB(vibrant, 3),\n\t\t\t},\n\t\t});\n\t}\n\n\tasync fetchTempo(uri) {\n\t\tconst audio = await Spicetify.CosmosAsync.get(\n\t\t\t`https://spclient.wg.spotify.com/audio-attributes/v1/audio-features/${uri.split(\":\")[2]}?format=json`\n\t\t);\n\t\tlet tempo = audio.tempo;\n\n\t\tconst MIN_TEMPO = 60;\n\t\tconst MAX_TEMPO = 150;\n\t\tconst MAX_PERIOD = 0.4;\n\t\tif (!tempo) tempo = 105;\n\t\tif (tempo < MIN_TEMPO) tempo = MIN_TEMPO;\n\t\tif (tempo > MAX_TEMPO) tempo = MAX_TEMPO;\n\n\t\tlet period = MAX_PERIOD - ((tempo - MIN_TEMPO) / (MAX_TEMPO - MIN_TEMPO)) * MAX_PERIOD;\n\t\tperiod = Math.round(period * 100) / 100;\n\n\t\tthis.setState({\n\t\t\ttempo: `${String(period)}s`,\n\t\t});\n\t}\n\n\tasync refreshMusixmatchTranslation() {\n\t\tconst selectedLanguage = CONFIG.visual[\"musixmatch-translation-language\"] || \"none\";\n\t\tconst availableTranslations = this.state.musixmatchAvailableTranslations || [];\n\t\tconst trackId = this.state.musixmatchTrackId;\n\t\tconst currentUri = this.state.uri;\n\t\tconst currentRequestId = Symbol(\"musixmatchTranslationRequest\");\n\t\tthis._musixmatchTranslationRequestId = currentRequestId;\n\t\tconst isLatestRequest = () => this._musixmatchTranslationRequestId === currentRequestId;\n\t\tconst finishRequest = () => {\n\t\t\tif (isLatestRequest()) {\n\t\t\t\tthis._musixmatchTranslationRequestId = null;\n\t\t\t}\n\t\t};\n\n\t\tconst clearTranslation = () => {\n\t\t\tif (this.state.musixmatchTranslation !== null || this.state.musixmatchTranslationLanguage !== null) {\n\t\t\t\tthis.setState({\n\t\t\t\t\tmusixmatchTranslation: null,\n\t\t\t\t\tmusixmatchTranslationLanguage: null,\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (CACHE[currentUri]) {\n\t\t\t\tCACHE[currentUri].musixmatchTranslation = null;\n\t\t\t\tCACHE[currentUri].musixmatchTranslationLanguage = null;\n\t\t\t}\n\t\t};\n\n\t\tif (!trackId || !selectedLanguage || selectedLanguage === \"none\") {\n\t\t\tclearTranslation();\n\t\t\tfinishRequest();\n\t\t\treturn;\n\t\t}\n\n\t\tif (!availableTranslations.includes(selectedLanguage)) {\n\t\t\tclearTranslation();\n\t\t\tfinishRequest();\n\t\t\treturn;\n\t\t}\n\n\t\tconst baseLyrics = this.state.synced ?? this.state.unsynced;\n\t\tif (!baseLyrics) {\n\t\t\tfinishRequest();\n\t\t\treturn;\n\t\t}\n\n\t\tconst currentLanguage = selectedLanguage;\n\n\t\tSpicetify.showNotification(MUSIXMATCH_TRANSLATION_FETCH_MESSAGE, false, 1000);\n\n\t\tthis.setState({\n\t\t\tmusixmatchTranslation: null,\n\t\t\tmusixmatchTranslationLanguage: null,\n\t\t});\n\n\t\tlet translation;\n\t\ttry {\n\t\t\ttranslation = await ProviderMusixmatch.getTranslation(trackId);\n\t\t} catch (error) {\n\t\t\tconsole.error(error);\n\t\t\tif (isLatestRequest()) {\n\t\t\t\tSpicetify.showNotification(MUSIXMATCH_TRANSLATION_FETCH_FAILED_MESSAGE, true, 3000);\n\t\t\t\tif (CACHE[currentUri]) {\n\t\t\t\t\tCACHE[currentUri].musixmatchTranslation = null;\n\t\t\t\t\tCACHE[currentUri].musixmatchTranslationLanguage = null;\n\t\t\t\t}\n\t\t\t}\n\t\t\tfinishRequest();\n\t\t\treturn;\n\t\t}\n\n\t\tif (!translation) {\n\t\t\tif (isLatestRequest()) {\n\t\t\t\tSpicetify.showNotification(MUSIXMATCH_TRANSLATION_FETCH_FAILED_MESSAGE, true, 3000);\n\t\t\t\tif (CACHE[currentUri]) {\n\t\t\t\t\tCACHE[currentUri].musixmatchTranslation = null;\n\t\t\t\t\tCACHE[currentUri].musixmatchTranslationLanguage = null;\n\t\t\t\t}\n\t\t\t}\n\t\t\tfinishRequest();\n\t\t\treturn;\n\t\t}\n\n\t\tif (\n\t\t\tcurrentLanguage !== CONFIG.visual[\"musixmatch-translation-language\"] ||\n\t\t\ttrackId !== this.state.musixmatchTrackId ||\n\t\t\tcurrentUri !== this.state.uri ||\n\t\t\t!isLatestRequest()\n\t\t) {\n\t\t\tfinishRequest();\n\t\t\treturn;\n\t\t}\n\n\t\tconst latestBaseLyrics = this.state.synced ?? this.state.unsynced;\n\t\tif (!latestBaseLyrics) {\n\t\t\tfinishRequest();\n\t\t\treturn;\n\t\t}\n\n\t\tconst mappedTranslation = latestBaseLyrics.map((line) => {\n\t\t\tconst originalText = line.originalText ?? line.text;\n\t\t\tconst matched = translation.find((entry) => Utils.processLyrics(entry.matchedLine) === Utils.processLyrics(originalText));\n\n\t\t\treturn {\n\t\t\t\t...line,\n\t\t\t\ttext: matched?.translation ?? line.text,\n\t\t\t\toriginalText,\n\t\t\t};\n\t\t});\n\n\t\tif (!isLatestRequest()) {\n\t\t\tfinishRequest();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.setState({\n\t\t\tmusixmatchTranslation: mappedTranslation,\n\t\t\tmusixmatchTranslationLanguage: currentLanguage,\n\t\t});\n\t\tif (CACHE[currentUri]) {\n\t\t\tCACHE[currentUri].musixmatchTranslation = mappedTranslation;\n\t\t\tCACHE[currentUri].musixmatchTranslationLanguage = currentLanguage;\n\t\t}\n\t\tfinishRequest();\n\t}\n\n\tasync tryServices(trackInfo, mode = -1) {\n\t\tconst currentMode = CONFIG.modes[mode] || \"\";\n\t\tlet finalData = { ...emptyState, uri: trackInfo.uri };\n\t\tfor (const id of CONFIG.providersOrder) {\n\t\t\tconst service = CONFIG.providers[id];\n\t\t\tif (spotifyVersion >= \"1.2.31\" && id === \"genius\") continue;\n\t\t\tif (!service.on) continue;\n\t\t\tif (mode !== -1 && !service.modes.includes(mode)) continue;\n\n\t\t\tlet data;\n\t\t\ttry {\n\t\t\t\tdata = await Providers[id](trackInfo);\n\t\t\t} catch (e) {\n\t\t\t\tconsole.error(e);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (data.error || (!data.karaoke && !data.synced && !data.unsynced && !data.genius)) continue;\n\t\t\tif (mode === -1) {\n\t\t\t\tfinalData = data;\n\t\t\t\treturn finalData;\n\t\t\t}\n\n\t\t\tif (!data[currentMode]) {\n\t\t\t\tfor (const key in data) {\n\t\t\t\t\tif (!finalData[key]) {\n\t\t\t\t\t\tfinalData[key] = data[key];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfor (const key in data) {\n\t\t\t\tif (!finalData[key]) {\n\t\t\t\t\tfinalData[key] = data[key];\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (data.provider !== \"local\" && finalData.provider && finalData.provider !== data.provider) {\n\t\t\t\tconst styledMode = currentMode.charAt(0).toUpperCase() + currentMode.slice(1);\n\t\t\t\tfinalData.copyright = `${styledMode} lyrics provided by ${data.provider}\\n${finalData.copyright || \"\"}`.trim();\n\t\t\t}\n\n\t\t\tif (finalData.musixmatchTranslation && typeof finalData.musixmatchTranslation[0].startTime === \"undefined\" && finalData.synced) {\n\t\t\t\tfinalData.musixmatchTranslation = finalData.synced.map((line) => ({\n\t\t\t\t\t...line,\n\t\t\t\t\ttext:\n\t\t\t\t\t\tfinalData.musixmatchTranslation.find((l) => Utils.processLyrics(l.originalText) === Utils.processLyrics(line.text))?.text ?? line.text,\n\t\t\t\t}));\n\t\t\t}\n\n\t\t\treturn finalData;\n\t\t}\n\n\t\treturn finalData;\n\t}\n\n\tasync fetchLyrics(track, mode = -1, refresh = false) {\n\t\tconst info = this.infoFromTrack(track);\n\t\tif (!info) {\n\t\t\tthis.setState({ error: \"No track info\" });\n\t\t\treturn;\n\t\t}\n\n\t\tlet isCached = this.lyricsSaved(info.uri);\n\n\t\tif (CONFIG.visual.colorful) {\n\t\t\tthis.fetchColors(info.uri);\n\t\t}\n\n\t\tthis.fetchTempo(info.uri);\n\t\tthis.resetDelay();\n\n\t\tlet tempState;\n\t\t// if lyrics are cached\n\t\tif ((mode === -1 && CACHE[info.uri]) || CACHE[info.uri]?.[CONFIG.modes?.[mode]]) {\n\t\t\ttempState = { ...emptyState, ...CACHE[info.uri], isCached };\n\t\t\tif (CACHE[info.uri]?.mode) {\n\t\t\t\tthis.state.explicitMode = CACHE[info.uri]?.mode;\n\t\t\t\ttempState = { ...tempState, mode: CACHE[info.uri]?.mode };\n\t\t\t}\n\t\t} else {\n\t\t\tthis.setState({ ...emptyState, isLoading: true, isCached: false });\n\n\t\t\tconst resp = await this.tryServices(info, mode);\n\t\t\tif (resp.provider) {\n\t\t\t\t// Cache lyrics\n\t\t\t\tCACHE[resp.uri] = resp;\n\t\t\t}\n\n\t\t\t// This True when the user presses the Cache Lyrics button and saves it to localStorage.\n\t\t\tisCached = this.lyricsSaved(resp.uri);\n\n\t\t\t// In case user skips tracks too fast and multiple callbacks\n\t\t\t// set wrong lyrics to current track.\n\t\t\tif (resp.uri === this.currentTrackUri) {\n\t\t\t\ttempState = { ...emptyState, ...resp, isLoading: false, isCached };\n\t\t\t} else {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tconst selectedMusixmatchLanguage = CONFIG.visual[\"musixmatch-translation-language\"] || \"none\";\n\t\tconst shouldRefreshMusixmatchTranslation =\n\t\t\ttempState.musixmatchTrackId &&\n\t\t\tselectedMusixmatchLanguage !== \"none\" &&\n\t\t\tArray.isArray(tempState.musixmatchAvailableTranslations) &&\n\t\t\ttempState.musixmatchAvailableTranslations.includes(selectedMusixmatchLanguage) &&\n\t\t\t(tempState.musixmatchTranslationLanguage !== selectedMusixmatchLanguage || !tempState.musixmatchTranslation);\n\t\tif (\n\t\t\tselectedMusixmatchLanguage !== \"none\" &&\n\t\t\t(!Array.isArray(tempState.musixmatchAvailableTranslations) || !tempState.musixmatchAvailableTranslations.includes(selectedMusixmatchLanguage))\n\t\t) {\n\t\t\tif (\n\t\t\t\ttypeof CONFIG.visual[\"translate:translated-lyrics-source\"] === \"string\" &&\n\t\t\t\tCONFIG.visual[\"translate:translated-lyrics-source\"].startsWith(MUSIXMATCH_TRANSLATION_PREFIX)\n\t\t\t) {\n\t\t\t\tCONFIG.visual[\"translate:translated-lyrics-source\"] = \"none\";\n\t\t\t\tlocalStorage.setItem(`${APP_NAME}:visual:translate:translated-lyrics-source`, \"none\");\n\t\t\t}\n\t\t\tCONFIG.visual[\"musixmatch-translation-language\"] = \"none\";\n\t\t\tlocalStorage.setItem(`${APP_NAME}:visual:musixmatch-translation-language`, \"none\");\n\t\t}\n\t\tconst translationOverrides = shouldRefreshMusixmatchTranslation ? { musixmatchTranslation: null, musixmatchTranslationLanguage: null } : {};\n\n\t\tlet finalMode = mode;\n\t\tif (mode === -1) {\n\t\t\tif (this.state.explicitMode !== -1) {\n\t\t\t\tfinalMode = this.state.explicitMode;\n\t\t\t} else if (this.state.lockMode !== -1) {\n\t\t\t\tfinalMode = this.state.lockMode;\n\t\t\t} else {\n\t\t\t\t// Auto switch\n\t\t\t\tif (tempState.karaoke) {\n\t\t\t\t\tfinalMode = KARAOKE;\n\t\t\t\t} else if (tempState.synced) {\n\t\t\t\t\tfinalMode = SYNCED;\n\t\t\t\t} else if (tempState.unsynced) {\n\t\t\t\t\tfinalMode = UNSYNCED;\n\t\t\t\t} else if (tempState.genius) {\n\t\t\t\t\tfinalMode = GENIUS;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.lyricsSource(tempState, finalMode);\n\n\t\t// if song changed one time\n\t\tif (tempState.uri !== this.state.uri || refresh) {\n\t\t\t// when a song starts for the first time and language-override is selected, the lyrics are converted to the specified language.\n\t\t\t// however, when switching it off again, the detected language needs to be known, so defaultLanguage has been introduced.\n\t\t\tconst defaultLanguage = Utils.detectLanguage(this.state.currentLyrics);\n\t\t\tconst language =\n\t\t\t\tCONFIG.visual[\"translate:detect-language-override\"] !== \"off\" ? CONFIG.visual[\"translate:detect-language-override\"] : defaultLanguage;\n\t\t\tconst friendlyLanguage = language && new Intl.DisplayNames([\"en\"], { type: \"language\" }).of(language.split(\"-\")[0])?.toLowerCase();\n\t\t\tconst targetConvert = CONFIG.visual[`translation-mode:${friendlyLanguage}`];\n\n\t\t\tconst isMemorey = CACHE[tempState.uri]?.[targetConvert];\n\t\t\tif (CONFIG.visual.translate && defaultLanguage && !isMemorey) {\n\t\t\t\tthis.translateLyrics(language, this.state.currentLyrics, targetConvert).then((translated) => {\n\t\t\t\t\tconst res = { [targetConvert]: translated };\n\t\t\t\t\t// Cache translated lyrics\n\t\t\t\t\tCACHE[tempState.uri] = { ...CACHE[tempState.uri], ...res };\n\t\t\t\t\tthis.setState({ ...res });\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// reset and apply\n\t\t\tthis.setState(\n\t\t\t\t{\n\t\t\t\t\tfurigana: null,\n\t\t\t\t\tromaji: null,\n\t\t\t\t\thiragana: null,\n\t\t\t\t\tkatakana: null,\n\t\t\t\t\thangul: null,\n\t\t\t\t\tromaja: null,\n\t\t\t\t\tcn: null,\n\t\t\t\t\thk: null,\n\t\t\t\t\ttw: null,\n\t\t\t\t\tneteaseTranslation: null,\n\t\t\t\t\t...tempState,\n\t\t\t\t\t...translationOverrides,\n\t\t\t\t\tlanguage: defaultLanguage,\n\t\t\t\t},\n\t\t\t\t() => {\n\t\t\t\t\tthis.currentMusixmatchLanguage = CONFIG.visual[\"musixmatch-translation-language\"];\n\t\t\t\t\tif (shouldRefreshMusixmatchTranslation) {\n\t\t\t\t\t\tthis.refreshMusixmatchTranslation();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.setState({ ...tempState, ...translationOverrides }, () => {\n\t\t\tthis.currentMusixmatchLanguage = CONFIG.visual[\"musixmatch-translation-language\"];\n\t\t\tif (shouldRefreshMusixmatchTranslation) {\n\t\t\t\tthis.refreshMusixmatchTranslation();\n\t\t\t}\n\t\t});\n\t}\n\n\tlyricsSource(lyricsState, mode) {\n\t\tif (!lyricsState) return;\n\n\t\tconst lang = this.provideLanguageCode(this.state.currentLyrics);\n\t\tconst friendlyLanguage = lang && new Intl.DisplayNames([\"en\"], { type: \"language\" }).of(lang.split(\"-\")[0])?.toLowerCase();\n\n\t\tif (!this.displayMode) {\n\t\t\tthis.displayMode = CONFIG.visual[`translation-mode:${friendlyLanguage}`];\n\t\t}\n\n\t\t// get original Lyrics\n\t\tconst lyrics = lyricsState[CONFIG.modes[mode]];\n\t\tconst translationSourceConfig = resolveTranslationSource(CONFIG.visual[\"translate:translated-lyrics-source\"]);\n\n\t\tif (translationSourceConfig.language) {\n\t\t\tconst translationLanguageKey = `${APP_NAME}:visual:musixmatch-translation-language`;\n\t\t\tconst storedLanguage = localStorage.getItem(translationLanguageKey);\n\n\t\t\tif (storedLanguage !== translationSourceConfig.language) {\n\t\t\t\tlocalStorage.setItem(translationLanguageKey, translationSourceConfig.language);\n\t\t\t}\n\n\t\t\tif (CONFIG.visual[\"musixmatch-translation-language\"] !== translationSourceConfig.language) {\n\t\t\t\tCONFIG.visual[\"musixmatch-translation-language\"] = translationSourceConfig.language;\n\t\t\t}\n\t\t}\n\n\t\tif (CONFIG.visual.translate) {\n\t\t\tthis.state.currentLyrics = lyricsState[CONFIG.visual[`translation-mode:${friendlyLanguage}`]] ?? lyrics;\n\t\t} else {\n\t\t\tthis.state.currentLyrics = lyricsState[translationSourceConfig.key] ?? lyrics;\n\t\t}\n\n\t\t// Convert Mode re-fresh\n\t\tif (\n\t\t\tthis.translate !== CONFIG.visual.translate ||\n\t\t\tthis.languageOverride !== CONFIG.visual[\"translate:detect-language-override\"] ||\n\t\t\tthis.displayMode !== CONFIG.visual[`translation-mode:${friendlyLanguage}`]\n\t\t) {\n\t\t\tthis.translate = CONFIG.visual.translate;\n\t\t\tthis.languageOverride = CONFIG.visual[\"translate:detect-language-override\"];\n\t\t\tthis.displayMode = CONFIG.visual[`translation-mode:${friendlyLanguage}`];\n\n\t\t\tif (CONFIG.visual.translate) {\n\t\t\t\tconst targetConvert = CONFIG.visual[`translation-mode:${friendlyLanguage}`];\n\t\t\t\tconst isCached = CACHE[lyricsState.uri]?.[targetConvert];\n\n\t\t\t\tif (!isCached) {\n\t\t\t\t\tthis.translateLyrics(lang, lyrics, targetConvert).then((translated) => {\n\t\t\t\t\t\tconst res = { [targetConvert]: translated };\n\t\t\t\t\t\t// Cache translated lyrics\n\t\t\t\t\t\tCACHE[lyricsState.uri] = { ...CACHE[lyricsState.uri], ...res };\n\t\t\t\t\t\tthis.setState({ ...this.state, ...res });\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst resetCache = { furigana: null, romaji: null, hiragana: null, katakana: null, hangul: null, romaja: null, cn: null, hk: null, tw: null };\n\t\t\t\tCACHE[lyricsState.uri] = { ...CACHE[lyricsState.uri], ...resetCache };\n\t\t\t}\n\t\t}\n\t}\n\n\tprovideLanguageCode(lyrics) {\n\t\tif (!lyrics) return;\n\n\t\tif (CONFIG.visual[\"translate:detect-language-override\"] !== \"off\") {\n\t\t\treturn CONFIG.visual[\"translate:detect-language-override\"];\n\t\t}\n\t\tif (this.state.language) {\n\t\t\treturn this.state.language;\n\t\t}\n\t\treturn Utils.detectLanguage(lyrics);\n\t}\n\n\tasync translateLyrics(language, lyrics, targetConvert) {\n\t\tif (!language) return;\n\n\t\tSpicetify.showNotification(\"Converting...\", false, 1000);\n\t\tif (!this.translator) {\n\t\t\tthis.translator = new Translator(language);\n\t\t}\n\t\tawait this.translator.awaitFinished(language);\n\n\t\tlet result;\n\t\ttry {\n\t\t\tif (language === \"ja\") {\n\t\t\t\t// Japanese\n\t\t\t\tconst map = {\n\t\t\t\t\tromaji: { target: \"romaji\", mode: \"spaced\" },\n\t\t\t\t\tfurigana: { target: \"hiragana\", mode: \"furigana\" },\n\t\t\t\t\thiragana: { target: \"hiragana\", mode: \"normal\" },\n\t\t\t\t\tkatakana: { target: \"katakana\", mode: \"normal\" },\n\t\t\t\t};\n\n\t\t\t\tresult = await Promise.all(\n\t\t\t\t\tlyrics.map(async (lyric) => await this.translator.romajifyText(lyric.text, map[targetConvert].target, map[targetConvert].mode))\n\t\t\t\t);\n\t\t\t} else if (language === \"ko\") {\n\t\t\t\t// Korean\n\t\t\t\tresult = await Promise.all(lyrics.map(async (lyric) => await this.translator.convertToRomaja(lyric.text, \"romaji\")));\n\t\t\t} else if (language === \"zh-hans\") {\n\t\t\t\t// Chinese (Simplified)\n\t\t\t\tconst map = {\n\t\t\t\t\tcn: { from: \"cn\", target: \"cn\" },\n\t\t\t\t\ttw: { from: \"cn\", target: \"tw\" },\n\t\t\t\t\thk: { from: \"cn\", target: \"hk\" },\n\t\t\t\t};\n\n\t\t\t\t// prevent conversion between the same language.\n\t\t\t\tif (targetConvert === \"cn\") {\n\t\t\t\t\tSpicetify.showNotification(\"No conversion is needed\", false, 1000);\n\t\t\t\t\treturn lyrics;\n\t\t\t\t}\n\n\t\t\t\tresult = await Promise.all(\n\t\t\t\t\tlyrics.map(async (lyric) => await this.translator.convertChinese(lyric.text, map[targetConvert].from, map[targetConvert].target))\n\t\t\t\t);\n\t\t\t} else if (language === \"zh-hant\") {\n\t\t\t\t// Chinese (Traditional)\n\t\t\t\tconst map = {\n\t\t\t\t\tcn: { from: \"t\", target: \"cn\" },\n\t\t\t\t\thk: { from: \"t\", target: \"hk\" },\n\t\t\t\t\ttw: { from: \"t\", target: \"tw\" },\n\t\t\t\t};\n\n\t\t\t\t// prevent conversion between the same language.\n\t\t\t\tif (targetConvert === \"tw\") {\n\t\t\t\t\tSpicetify.showNotification(\"No conversion is needed\", false, 1000);\n\t\t\t\t\treturn lyrics;\n\t\t\t\t}\n\n\t\t\t\tresult = await Promise.all(\n\t\t\t\t\tlyrics.map(async (lyric) => await this.translator.convertChinese(lyric.text, map[targetConvert].from, map[targetConvert].target))\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst res = Utils.processTranslatedLyrics(result, lyrics);\n\t\t\tSpicetify.showNotification(\"Converting...\", false, 0);\n\t\t\treturn res;\n\t\t} catch (error) {\n\t\t\tSpicetify.showNotification(\"Convert Error!\", true);\n\t\t\tconsole.error(error);\n\t\t}\n\t}\n\n\tresetDelay() {\n\t\tCONFIG.visual.delay = Number(localStorage.getItem(`lyrics-delay:${Spicetify.Player.data.item.uri}`)) || 0;\n\t}\n\n\tasync onVersionChange(items, index) {\n\t\tif (this.state.mode === GENIUS) {\n\t\t\tthis.setState({\n\t\t\t\t...emptyLine,\n\t\t\t\tgenius2: this.state.genius2,\n\t\t\t\tisLoading: true,\n\t\t\t});\n\t\t\tconst lyrics = await ProviderGenius.fetchLyricsVersion(items, index);\n\t\t\tthis.setState({\n\t\t\t\tgenius: lyrics,\n\t\t\t\tversionIndex: index,\n\t\t\t\tisLoading: false,\n\t\t\t});\n\t\t}\n\t}\n\n\tasync onVersionChange2(items, index) {\n\t\tif (this.state.mode === GENIUS) {\n\t\t\tthis.setState({\n\t\t\t\t...emptyLine,\n\t\t\t\tgenius: this.state.genius,\n\t\t\t\tisLoading: true,\n\t\t\t});\n\t\t\tconst lyrics = await ProviderGenius.fetchLyricsVersion(items, index);\n\t\t\tthis.setState({\n\t\t\t\tgenius2: lyrics,\n\t\t\t\tversionIndex2: index,\n\t\t\t\tisLoading: false,\n\t\t\t});\n\t\t}\n\t}\n\n\tsaveLocalLyrics(uri, lyrics) {\n\t\tif (lyrics.genius) {\n\t\t\tlyrics.unsynced = lyrics.genius.split(\"<br>\").map((lyc) => {\n\t\t\t\treturn {\n\t\t\t\t\ttext: lyc.replace(/<[^>]*>/g, \"\"),\n\t\t\t\t};\n\t\t\t});\n\t\t\tlyrics.genius = null;\n\t\t}\n\n\t\tconst localLyrics = JSON.parse(localStorage.getItem(`${APP_NAME}:local-lyrics`)) || {};\n\t\tlocalLyrics[uri] = lyrics;\n\t\tlocalStorage.setItem(`${APP_NAME}:local-lyrics`, JSON.stringify(localLyrics));\n\t\tthis.setState({ isCached: true });\n\t}\n\n\tdeleteLocalLyrics(uri) {\n\t\tconst localLyrics = JSON.parse(localStorage.getItem(`${APP_NAME}:local-lyrics`)) || {};\n\t\tdelete localLyrics[uri];\n\t\tlocalStorage.setItem(`${APP_NAME}:local-lyrics`, JSON.stringify(localLyrics));\n\t\tconsole.log(localLyrics);\n\t\tthis.setState({ isCached: false });\n\t}\n\n\tlyricsSaved(uri) {\n\t\tconst localLyrics = JSON.parse(localStorage.getItem(`${APP_NAME}:local-lyrics`)) || {};\n\t\treturn !!localLyrics[uri];\n\t}\n\n\tprocessLyricsFromFile(event) {\n\t\tconst file = event.target.files;\n\t\tif (!file.length) return;\n\t\tconst reader = new FileReader();\n\n\t\tif (file[0].size > 1024 * 1024) {\n\t\t\tSpicetify.showNotification(\"File too large\", true);\n\t\t\treturn;\n\t\t}\n\n\t\treader.onload = (e) => {\n\t\t\ttry {\n\t\t\t\tconst localLyrics = Utils.parseLocalLyrics(e.target.result);\n\t\t\t\tconst parsedKeys = Object.keys(localLyrics)\n\t\t\t\t\t.filter((key) => localLyrics[key])\n\t\t\t\t\t.map((key) => key[0].toUpperCase() + key.slice(1))\n\t\t\t\t\t.map((key) => `<strong>${key}</strong>`);\n\n\t\t\t\tif (!parsedKeys.length) {\n\t\t\t\t\tSpicetify.showNotification(\"Nothing to load\", true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tthis.setState({ ...localLyrics, provider: \"local\" });\n\t\t\t\tCACHE[this.currentTrackUri] = { ...localLyrics, provider: \"local\", uri: this.currentTrackUri };\n\t\t\t\tthis.saveLocalLyrics(this.currentTrackUri, localLyrics);\n\n\t\t\t\tSpicetify.showNotification(`Loaded ${parsedKeys.join(\", \")} lyrics from file`);\n\t\t\t} catch (e) {\n\t\t\t\tconsole.error(e);\n\t\t\t\tSpicetify.showNotification(\"Failed to load lyrics\", true);\n\t\t\t}\n\t\t};\n\n\t\treader.onerror = (e) => {\n\t\t\tconsole.error(e);\n\t\t\tSpicetify.showNotification(\"Failed to read file\", true);\n\t\t};\n\n\t\treader.readAsText(file[0]);\n\t\tevent.target.value = \"\";\n\t}\n\tinitMoustrap() {\n\t\tif (!this.mousetrap && Spicetify.Mousetrap) {\n\t\t\tthis.mousetrap = new Spicetify.Mousetrap();\n\t\t}\n\t}\n\n\tcomponentDidMount() {\n\t\tthis.onQueueChange = async ({ data: queue }) => {\n\t\t\tthis.state.explicitMode = this.state.lockMode;\n\t\t\tthis.currentTrackUri = queue.current.uri;\n\t\t\tthis.fetchLyrics(queue.current, this.state.explicitMode);\n\t\t\tthis.viewPort.scrollTo(0, 0);\n\n\t\t\t// Fetch next track\n\t\t\tconst nextTrack = queue.queued?.[0] || queue.nextUp?.[0];\n\t\t\tconst nextInfo = this.infoFromTrack(nextTrack);\n\t\t\t// Debounce next track fetch\n\t\t\tif (!nextInfo || nextInfo.uri === this.nextTrackUri) return;\n\t\t\tthis.nextTrackUri = nextInfo.uri;\n\t\t\tthis.tryServices(nextInfo, this.state.explicitMode).then((resp) => {\n\t\t\t\tif (resp.provider) {\n\t\t\t\t\t// Cache lyrics\n\t\t\t\t\tCACHE[resp.uri] = resp;\n\t\t\t\t}\n\t\t\t});\n\t\t};\n\n\t\tif (Spicetify.Player?.data?.item) {\n\t\t\tthis.state.explicitMode = this.state.lockMode;\n\t\t\tthis.currentTrackUri = Spicetify.Player.data.item.uri;\n\t\t\tthis.fetchLyrics(Spicetify.Player.data.item, this.state.explicitMode);\n\t\t}\n\n\t\tthis.updateVisualOnConfigChange();\n\t\tUtils.addQueueListener(this.onQueueChange);\n\n\t\tlyricContainerUpdate = () => {\n\t\t\tthis.reRenderLyricsPage = !this.reRenderLyricsPage;\n\t\t\tthis.updateVisualOnConfigChange();\n\t\t\tthis.forceUpdate();\n\n\t\t\tif (this.currentMusixmatchLanguage !== CONFIG.visual[\"musixmatch-translation-language\"]) {\n\t\t\t\tthis.currentMusixmatchLanguage = CONFIG.visual[\"musixmatch-translation-language\"];\n\t\t\t\tthis.refreshMusixmatchTranslation();\n\t\t\t}\n\t\t};\n\n\t\trefreshMusixmatchTranslation = this.refreshMusixmatchTranslation.bind(this);\n\n\t\treloadLyrics = () => {\n\t\t\tCACHE = {};\n\t\t\tthis.updateVisualOnConfigChange();\n\t\t\tthis.forceUpdate();\n\t\t\tthis.fetchLyrics(Spicetify.Player.data.item, this.state.explicitMode, true);\n\t\t};\n\n\t\tthis.viewPort =\n\t\t\tdocument.querySelector(\".Root__main-view .os-viewport\") ?? document.querySelector(\".Root__main-view .main-view-container__scroll-node\");\n\n\t\tthis.configButton = new Spicetify.Menu.Item(\"Lyrics Plus config\", false, openConfig, \"lyrics\");\n\t\tthis.configButton.register();\n\n\t\tthis.onFontSizeChange = (event) => {\n\t\t\tif (!event.ctrlKey) return;\n\t\t\tconst dir = event.deltaY < 0 ? 1 : -1;\n\t\t\tlet temp = CONFIG.visual[\"font-size\"] + dir * fontSizeLimit.step;\n\t\t\tif (temp < fontSizeLimit.min) {\n\t\t\t\ttemp = fontSizeLimit.min;\n\t\t\t} else if (temp > fontSizeLimit.max) {\n\t\t\t\ttemp = fontSizeLimit.max;\n\t\t\t}\n\t\t\tCONFIG.visual[\"font-size\"] = temp;\n\t\t\tlocalStorage.setItem(\"lyrics-plus:visual:font-size\", temp);\n\t\t\tlyricContainerUpdate();\n\t\t};\n\n\t\tthis.toggleFullscreen = () => {\n\t\t\tconst isEnabled = !this.state.isFullscreen;\n\t\t\tif (isEnabled) {\n\t\t\t\tdocument.body.append(this.fullscreenContainer);\n\t\t\t\tdocument.documentElement.requestFullscreen();\n\t\t\t\tthis.mousetrap.bind(\"esc\", this.toggleFullscreen);\n\t\t\t} else {\n\t\t\t\tthis.fullscreenContainer.remove();\n\t\t\t\tdocument.exitFullscreen();\n\t\t\t\tthis.mousetrap.unbind(\"esc\");\n\t\t\t}\n\n\t\t\tthis.setState({\n\t\t\t\tisFullscreen: isEnabled,\n\t\t\t});\n\t\t};\n\t\tthis.mousetrap.reset();\n\t\tthis.mousetrap.bind(CONFIG.visual[\"fullscreen-key\"], this.toggleFullscreen);\n\t\twindow.addEventListener(\"fad-request\", lyricContainerUpdate);\n\t}\n\n\tcomponentWillUnmount() {\n\t\tUtils.removeQueueListener(this.onQueueChange);\n\t\tthis.configButton.deregister();\n\t\tthis.mousetrap.reset();\n\t\twindow.removeEventListener(\"fad-request\", lyricContainerUpdate);\n\t\trefreshMusixmatchTranslation = null;\n\t}\n\n\tupdateVisualOnConfigChange() {\n\t\tthis.availableModes = CONFIG.modes.filter((_, id) => {\n\t\t\treturn Object.values(CONFIG.providers).some((p) => p.on && p.modes.includes(id));\n\t\t});\n\n\t\tif (!CONFIG.visual.colorful) {\n\t\t\tthis.styleVariables = {\n\t\t\t\t\"--lyrics-color-active\": CONFIG.visual[\"active-color\"],\n\t\t\t\t\"--lyrics-color-inactive\": CONFIG.visual[\"inactive-color\"],\n\t\t\t\t\"--lyrics-color-background\": CONFIG.visual[\"background-color\"],\n\t\t\t\t\"--lyrics-highlight-background\": CONFIG.visual[\"highlight-color\"],\n\t\t\t\t\"--lyrics-background-noise\": CONFIG.visual.noise ? \"var(--background-noise)\" : \"unset\",\n\t\t\t};\n\t\t}\n\n\t\tthis.styleVariables = {\n\t\t\t...this.styleVariables,\n\t\t\t\"--lyrics-align-text\": CONFIG.visual.alignment,\n\t\t\t\"--lyrics-font-size\": `${CONFIG.visual[\"font-size\"]}px`,\n\t\t\t\"--animation-tempo\": this.state.tempo,\n\t\t};\n\n\t\tthis.mousetrap.reset();\n\t\tthis.mousetrap.bind(CONFIG.visual[\"fullscreen-key\"], this.toggleFullscreen);\n\t}\n\n\trender() {\n\t\tconst fadLyricsContainer = document.getElementById(\"fad-lyrics-plus-container\");\n\t\tthis.state.isFADMode = !!fadLyricsContainer;\n\n\t\tif (this.state.isFADMode) {\n\t\t\t// Text colors will be set by FAD extension\n\t\t\tthis.styleVariables = {};\n\t\t} else if (CONFIG.visual.colorful) {\n\t\t\tthis.styleVariables = {\n\t\t\t\t\"--lyrics-color-active\": \"white\",\n\t\t\t\t\"--lyrics-color-inactive\": this.state.colors.inactive,\n\t\t\t\t\"--lyrics-color-background\": this.state.colors.background || \"transparent\",\n\t\t\t\t\"--lyrics-highlight-background\": this.state.colors.inactive,\n\t\t\t\t\"--lyrics-background-noise\": CONFIG.visual.noise ? \"var(--background-noise)\" : \"unset\",\n\t\t\t};\n\t\t}\n\n\t\tthis.styleVariables = {\n\t\t\t...this.styleVariables,\n\t\t\t\"--lyrics-align-text\": CONFIG.visual.alignment,\n\t\t\t\"--lyrics-font-size\": `${CONFIG.visual[\"font-size\"]}px`,\n\t\t\t\"--animation-tempo\": this.state.tempo,\n\t\t};\n\n\t\tlet mode = -1;\n\t\tif (this.state.explicitMode !== -1) {\n\t\t\tmode = this.state.explicitMode;\n\t\t} else if (this.state.lockMode !== -1) {\n\t\t\tmode = this.state.lockMode;\n\t\t} else {\n\t\t\t// Auto switch\n\t\t\tif (this.state.karaoke) {\n\t\t\t\tmode = KARAOKE;\n\t\t\t} else if (this.state.synced) {\n\t\t\t\tmode = SYNCED;\n\t\t\t} else if (this.state.unsynced) {\n\t\t\t\tmode = UNSYNCED;\n\t\t\t} else if (this.state.genius) {\n\t\t\t\tmode = GENIUS;\n\t\t\t}\n\t\t}\n\n\t\tlet activeItem;\n\t\tlet showTranslationButton;\n\n\t\tthis.lyricsSource(this.state, mode);\n\t\tconst lang = this.provideLanguageCode(this.state.currentLyrics);\n\t\tconst friendlyLanguage = lang && new Intl.DisplayNames([\"en\"], { type: \"language\" }).of(lang.split(\"-\")[0])?.toLowerCase();\n\t\tconst hasMusixmatchLanguages = Array.isArray(this.state.musixmatchAvailableTranslations) && this.state.musixmatchAvailableTranslations.length > 0;\n\t\tconst hasTranslation = this.state.neteaseTranslation !== null || this.state.musixmatchTranslation !== null || hasMusixmatchLanguages;\n\t\tconst hasPerformer = !!this.state.currentLyrics?.some((line) => line.performer);\n\n\t\tif (mode !== -1) {\n\t\t\tshowTranslationButton = (friendlyLanguage || hasTranslation) && (mode === SYNCED || mode === UNSYNCED);\n\n\t\t\tif (mode === KARAOKE && this.state.karaoke) {\n\t\t\t\tactiveItem = react.createElement(CONFIG.visual[\"synced-compact\"] ? SyncedLyricsPage : SyncedExpandedLyricsPage, {\n\t\t\t\t\tisKara: true,\n\t\t\t\t\ttrackUri: this.state.uri,\n\t\t\t\t\tlyrics: this.state.karaoke,\n\t\t\t\t\tprovider: this.state.provider,\n\t\t\t\t\tcopyright: this.state.copyright,\n\t\t\t\t\treRenderLyricsPage: this.reRenderLyricsPage,\n\t\t\t\t});\n\t\t\t} else if (mode === SYNCED && this.state.synced) {\n\t\t\t\tactiveItem = react.createElement(CONFIG.visual[\"synced-compact\"] ? SyncedLyricsPage : SyncedExpandedLyricsPage, {\n\t\t\t\t\ttrackUri: this.state.uri,\n\t\t\t\t\tlyrics: this.state.currentLyrics,\n\t\t\t\t\tprovider: this.state.provider,\n\t\t\t\t\tcopyright: this.state.copyright,\n\t\t\t\t\treRenderLyricsPage: this.reRenderLyricsPage,\n\t\t\t\t});\n\t\t\t} else if (mode === UNSYNCED && this.state.unsynced) {\n\t\t\t\tactiveItem = react.createElement(UnsyncedLyricsPage, {\n\t\t\t\t\ttrackUri: this.state.uri,\n\t\t\t\t\tlyrics: this.state.currentLyrics,\n\t\t\t\t\tprovider: this.state.provider,\n\t\t\t\t\tcopyright: this.state.copyright,\n\t\t\t\t\treRenderLyricsPage: this.reRenderLyricsPage,\n\t\t\t\t});\n\t\t\t} else if (mode === GENIUS && this.state.genius) {\n\t\t\t\tactiveItem = react.createElement(GeniusPage, {\n\t\t\t\t\tisSplitted: CONFIG.visual[\"dual-genius\"],\n\t\t\t\t\ttrackUri: this.state.uri,\n\t\t\t\t\tlyrics: this.state.genius,\n\t\t\t\t\tprovider: this.state.provider,\n\t\t\t\t\tcopyright: this.state.copyright,\n\t\t\t\t\tversions: this.state.versions,\n\t\t\t\t\tversionIndex: this.state.versionIndex,\n\t\t\t\t\tonVersionChange: this.onVersionChange.bind(this),\n\t\t\t\t\tlyrics2: this.state.genius2,\n\t\t\t\t\tversionIndex2: this.state.versionIndex2,\n\t\t\t\t\tonVersionChange2: this.onVersionChange2.bind(this),\n\t\t\t\t\treRenderLyricsPage: this.reRenderLyricsPage,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tif (!activeItem) {\n\t\t\tactiveItem = react.createElement(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tclassName: \"lyrics-lyricsContainer-LyricsUnavailablePage\",\n\t\t\t\t},\n\t\t\t\treact.createElement(\n\t\t\t\t\t\"span\",\n\t\t\t\t\t{\n\t\t\t\t\t\tclassName: \"lyrics-lyricsContainer-LyricsUnavailableMessage\",\n\t\t\t\t\t},\n\t\t\t\t\tthis.state.isLoading ? LoadingIcon : \"(• _ • )\"\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\tthis.state.mode = mode;\n\n\t\tconst out = react.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tclassName: `lyrics-lyricsContainer-LyricsContainer${CONFIG.visual[\"fade-blur\"] ? \" blur-enabled\" : \"\"}${\n\t\t\t\t\tfadLyricsContainer ? \" fad-enabled\" : \"\"\n\t\t\t\t}`,\n\t\t\t\tstyle: this.styleVariables,\n\t\t\t\tref: (el) => {\n\t\t\t\t\tif (!el) return;\n\t\t\t\t\tel.onmousewheel = this.onFontSizeChange;\n\t\t\t\t},\n\t\t\t},\n\t\t\treact.createElement(\"div\", {\n\t\t\t\tclassName: \"lyrics-lyricsContainer-LyricsBackground\",\n\t\t\t}),\n\t\t\treact.createElement(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tclassName: \"lyrics-config-button-container\",\n\t\t\t\t},\n\t\t\t\tshowTranslationButton &&\n\t\t\t\t\treact.createElement(TranslationMenu, {\n\t\t\t\t\t\tfriendlyLanguage,\n\t\t\t\t\t\thasTranslation: {\n\t\t\t\t\t\t\tmusixmatch: this.state.musixmatchTranslation !== null,\n\t\t\t\t\t\t\tnetease: this.state.neteaseTranslation !== null,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmusixmatchLanguages: this.state.musixmatchAvailableTranslations || [],\n\t\t\t\t\t\tmusixmatchSelectedLanguage: this.state.musixmatchTranslationLanguage || CONFIG.visual[\"musixmatch-translation-language\"],\n\t\t\t\t\t}),\n\t\t\t\treact.createElement(AdjustmentsMenu, { mode, hasPerformer }),\n\t\t\t\treact.createElement(\n\t\t\t\t\tSpicetify.ReactComponent.TooltipWrapper,\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: this.state.isCached ? \"Lyrics cached\" : \"Cache lyrics\",\n\t\t\t\t\t},\n\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\"button\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tclassName: \"lyrics-config-button\",\n\t\t\t\t\t\t\tonClick: () => {\n\t\t\t\t\t\t\t\tconst { synced, unsynced, karaoke, genius } = this.state;\n\t\t\t\t\t\t\t\tif (!synced && !unsynced && !karaoke && !genius) {\n\t\t\t\t\t\t\t\t\tSpicetify.showNotification(\"No lyrics to cache\", true);\n\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tif (this.state.isCached) {\n\t\t\t\t\t\t\t\t\tthis.deleteLocalLyrics(this.currentTrackUri);\n\t\t\t\t\t\t\t\t\tSpicetify.showNotification(\"Delete lyrics cache\");\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tthis.saveLocalLyrics(this.currentTrackUri, { synced, unsynced, karaoke, genius });\n\t\t\t\t\t\t\t\t\tSpicetify.showNotification(\"Lyrics cached\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\treact.createElement(\"svg\", {\n\t\t\t\t\t\t\twidth: 16,\n\t\t\t\t\t\t\theight: 16,\n\t\t\t\t\t\t\tviewBox: \"0 0 16 16\",\n\t\t\t\t\t\t\tfill: \"currentColor\",\n\t\t\t\t\t\t\tdangerouslySetInnerHTML: {\n\t\t\t\t\t\t\t\t__html: Spicetify.SVGIcons[this.state.isCached ? \"downloaded\" : \"download\"],\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t})\n\t\t\t\t\t)\n\t\t\t\t),\n\t\t\t\treact.createElement(\n\t\t\t\t\tSpicetify.ReactComponent.TooltipWrapper,\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: \"Load lyrics from file\",\n\t\t\t\t\t},\n\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\"button\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tclassName: \"lyrics-config-button\",\n\t\t\t\t\t\t\tonClick: () => {\n\t\t\t\t\t\t\t\tdocument.getElementById(\"lyrics-file-input\").click();\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\treact.createElement(\"input\", {\n\t\t\t\t\t\t\ttype: \"file\",\n\t\t\t\t\t\t\tid: \"lyrics-file-input\",\n\t\t\t\t\t\t\taccept: \".lrc,.txt\",\n\t\t\t\t\t\t\tonChange: this.processLyricsFromFile.bind(this),\n\t\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\t\tdisplay: \"none\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}),\n\t\t\t\t\t\treact.createElement(\"svg\", {\n\t\t\t\t\t\t\twidth: 16,\n\t\t\t\t\t\t\theight: 16,\n\t\t\t\t\t\t\tviewBox: \"0 0 16 16\",\n\t\t\t\t\t\t\tfill: \"currentColor\",\n\t\t\t\t\t\t\tdangerouslySetInnerHTML: {\n\t\t\t\t\t\t\t\t__html: Spicetify.SVGIcons[\"plus-alt\"],\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t})\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t),\n\t\t\tactiveItem,\n\t\t\t!!document.querySelector(\".main-topBar-topbarContentWrapper\") &&\n\t\t\t\treact.createElement(TopBarContent, {\n\t\t\t\t\tlinks: this.availableModes,\n\t\t\t\t\tactiveLink: CONFIG.modes[mode],\n\t\t\t\t\tlockLink: CONFIG.modes[this.state.lockMode],\n\t\t\t\t\tswitchCallback: (label) => {\n\t\t\t\t\t\tconst mode = CONFIG.modes.findIndex((a) => a === label);\n\t\t\t\t\t\tif (mode !== this.state.mode) {\n\t\t\t\t\t\t\t// If explicitMode is not set, moving the topBar will apply the default mode value for the selected song.\n\t\t\t\t\t\t\tconst info = this.infoFromTrack(Spicetify.Player.data.item);\n\t\t\t\t\t\t\tif (info?.uri && CACHE[info?.uri]) {\n\t\t\t\t\t\t\t\tCACHE[info.uri].mode = mode;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tthis.setState({ explicitMode: mode });\n\t\t\t\t\t\t\tthis.state.provider !== \"local\" && this.fetchLyrics(Spicetify.Player.data.item, mode);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\tlockCallback: (label) => {\n\t\t\t\t\t\tlet mode = CONFIG.modes.findIndex((a) => a === label);\n\t\t\t\t\t\tif (mode === this.state.lockMode) {\n\t\t\t\t\t\t\tmode = -1;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.setState({ explicitMode: mode, lockMode: mode });\n\t\t\t\t\t\tthis.fetchLyrics(Spicetify.Player.data.item, mode);\n\t\t\t\t\t\tCONFIG.locked = mode;\n\t\t\t\t\t\tlocalStorage.setItem(\"lyrics-plus:lock-mode\", mode);\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t);\n\n\t\tif (this.state.isFullscreen) return Spicetify.ReactDOM.createPortal(out, this.fullscreenContainer);\n\t\tif (fadLyricsContainer) return Spicetify.ReactDOM.createPortal(out, fadLyricsContainer);\n\t\treturn out;\n\t}\n}\n"
  },
  {
    "path": "CustomApps/lyrics-plus/manifest.json",
    "content": "{\n\t\"name\": {\n\t\t\"ms\": \"Lyrics\",\n\t\t\"gu\": \"Lyrics\",\n\t\t\"ko\": \"Lyrics\",\n\t\t\"pa-IN\": \"Lyrics\",\n\t\t\"az\": \"Lyrics\",\n\t\t\"ru\": \"Текст\",\n\t\t\"uk\": \"Lyrics\",\n\t\t\"nb\": \"Lyrics\",\n\t\t\"sv\": \"Låttext\",\n\t\t\"sw\": \"Lyrics\",\n\t\t\"ur\": \"Lyrics\",\n\t\t\"bho\": \"Lyrics\",\n\t\t\"pa-PK\": \"Lyrics\",\n\t\t\"te\": \"Lyrics\",\n\t\t\"ro\": \"Lyrics\",\n\t\t\"vi\": \"Lời bài hát\",\n\t\t\"am\": \"Lyrics\",\n\t\t\"bn\": \"Lyrics\",\n\t\t\"en\": \"Lyrics\",\n\t\t\"id\": \"Lirik\",\n\t\t\"bg\": \"Lyrics\",\n\t\t\"da\": \"Lyrics\",\n\t\t\"es-419\": \"Letras\",\n\t\t\"mr\": \"Lyrics\",\n\t\t\"ml\": \"Lyrics\",\n\t\t\"th\": \"เนื้อเพลง\",\n\t\t\"tr\": \"Şarkı Sözleri\",\n\t\t\"is\": \"Lyrics\",\n\t\t\"fa\": \"Lyrics\",\n\t\t\"or\": \"Lyrics\",\n\t\t\"he\": \"Lyrics\",\n\t\t\"hi\": \"Lyrics\",\n\t\t\"zh-TW\": \"歌詞\",\n\t\t\"sr\": \"Lyrics\",\n\t\t\"pt-BR\": \"Letra\",\n\t\t\"zu\": \"Lyrics\",\n\t\t\"nl\": \"Songteksten\",\n\t\t\"es\": \"Letra\",\n\t\t\"lt\": \"Lyrics\",\n\t\t\"ja\": \"歌詞\",\n\t\t\"st\": \"Lyrics\",\n\t\t\"it\": \"Lyrics\",\n\t\t\"el\": \"Στίχοι\",\n\t\t\"pt-PT\": \"Lyrics\",\n\t\t\"kn\": \"Lyrics\",\n\t\t\"de\": \"Songtext\",\n\t\t\"fr\": \"Paroles\",\n\t\t\"ne\": \"Lyrics\",\n\t\t\"ar\": \"الكلمات\",\n\t\t\"af\": \"Lyrics\",\n\t\t\"et\": \"Lyrics\",\n\t\t\"pl\": \"Tekst\",\n\t\t\"ta\": \"Lyrics\",\n\t\t\"sl\": \"Lyrics\",\n\t\t\"pk\": \"Lyrics\",\n\t\t\"hr\": \"Lyrics\",\n\t\t\"sk\": \"Lyrics\",\n\t\t\"fi\": \"Sanat\",\n\t\t\"lv\": \"Lyrics\",\n\t\t\"fil\": \"Lyrics\",\n\t\t\"fr-CA\": \"Paroles\",\n\t\t\"cs\": \"Text\",\n\t\t\"zh-CN\": \"歌词\",\n\t\t\"hu\": \"Dalszöveg\"\n\t},\n\t\"icon\": \"<svg viewBox=\\\"0 0 256 256\\\" fill=\\\"currentColor\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"1\\\"><path d=\\\"m224.9832,74.42656q0,17.80336 -8.28326,33.3279t-22.6169,25.4232t-31.90855,12.31993l-88.73891,102.6898q-4.89793,5.69708 -12.53293,5.69708q-4.46576,0 -8.35529,-2.1364l-15.41406,-8.83047q-5.47415,-3.13339 -7.41892,-9.11532t0.5042,-11.67901l54.74154,-121.34772q-5.18604,-12.81842 -5.18604,-26.34898q0,-29.6248 21.32039,-50.70398t51.28418,-21.07918t51.28418,21.07918t21.32039,50.70398zm-158.46235,167.92132l83.26476,-96.28059q-18.72737,-0.71213 -34.50157,-10.0411t-25.13789,-24.85349l-51.57229,114.65366q-1.15245,2.56368 -0.28811,5.19858t3.3133,4.05917l15.55812,8.97289q2.30491,1.28184 4.96996,0.85456t4.39373,-2.56368zm85.85778,-105.11106q26.21832,0 44.87366,-18.44428t18.65534,-44.36598t-18.65534,-44.36598t-44.87366,-18.44428t-44.87366,18.44428t-18.65534,44.36598t18.65534,44.36598t44.87366,18.44428z\\\"/></svg>\",\n\t\"active-icon\": \"<svg viewBox=\\\"0 0 256 256\\\" fill=\\\"currentColor\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"1\\\"><path d=\\\"M 224.983 74.427 C 224.983 86.295 222.222 97.405 216.7 107.754 C 211.178 118.104 203.639 126.579 194.083 133.178 C 184.527 139.777 173.891 143.883 162.174 145.498 L 73.436 248.187 C 70.17 251.985 65.993 253.884 60.903 253.884 C 57.925 253.884 55.14 253.172 52.547 251.748 L 37.133 242.918 C 33.484 240.829 31.011 237.79 29.714 233.802 C 28.418 229.814 28.586 225.921 30.219 222.123 L 84.96 100.776 C 81.503 92.23 79.774 83.447 79.774 74.427 C 79.774 54.677 86.881 37.775 101.094 23.723 C 115.308 9.67 132.403 2.643 152.379 2.643 C 172.355 2.643 189.449 9.67 203.663 23.723 C 217.876 37.775 224.983 54.677 224.983 74.427 Z M 152.379 137.237 C 169.858 137.237 184.815 131.089 197.252 118.793 C 209.689 106.496 215.908 91.708 215.908 74.427 C 215.908 57.145 209.689 42.357 197.252 30.061 C 184.815 17.764 169.858 11.616 152.379 11.616 C 134.9 11.616 119.942 17.764 107.505 30.061 C 95.068 42.357 88.85 57.145 88.85 74.427 C 88.85 91.708 95.068 106.496 107.505 118.793 C 119.942 131.089 134.9 137.237 152.379 137.237 Z\\\"/></svg>\",\n\t\"subfiles\": [\n\t\t\"ProviderNetease.js\",\n\t\t\"ProviderMusixmatch.js\",\n\t\t\"ProviderGenius.js\",\n\t\t\"ProviderLRCLIB.js\",\n\t\t\"Providers.js\",\n\t\t\"Pages.js\",\n\t\t\"OptionsMenu.js\",\n\t\t\"TabBar.js\",\n\t\t\"Utils.js\",\n\t\t\"Settings.js\",\n\t\t\"Translator.js\"\n\t],\n\t\"subfiles_extension\": [\"PlaybarButton.js\"]\n}\n"
  },
  {
    "path": "CustomApps/lyrics-plus/style.css",
    "content": "/*!\n * Bootstrap v3.3.7 (http://getbootstrap.com)\n * Copyright 2011-2016 Twitter,\n Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n/*!\n * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=35378cd201a131f69c68a64bc4438544)\n * Config saved to config.json and https://gist.github.com/35378cd201a131f69c68a64bc4438544\n */\n@media (min-width: 768px) {\n\t.container {\n\t\twidth: 750px;\n\t}\n}\n\n@media (min-width: 992px) {\n\t.container {\n\t\twidth: 970px;\n\t}\n}\n\n@media (min-width: 1200px) {\n\t.container {\n\t\twidth: 1170px;\n\t}\n}\n\n@media (min-width: 1500px) {\n\t.container {\n\t\twidth: 1450px;\n\t}\n}\n\n.row {\n\tmargin-left: -16px;\n\tmargin-right: -16px;\n}\n\n.container:after,\n.row:after {\n\tclear: both;\n}\n\n.hide {\n\tdisplay: none !important;\n}\n\n.show {\n\tdisplay: block !important;\n}\n\n.hidden {\n\tdisplay: none !important;\n}\n\n.lyrics-lyricsContainer-LyricsContainer {\n\tdisplay: grid;\n\tgrid-template-rows: 1fr;\n\tposition: absolute;\n\theight: 100%;\n\twidth: 100%;\n\ttop: 0;\n}\n\n.lyrics-lyricsContainer-Loading {\n\talign-self: center;\n\tgrid-area: 1 / 1 / -1 / -1;\n}\n\n.lyrics-lyricsContainer-LyricsUnavailablePage {\n\talign-items: center;\n\tcolor: var(--lyrics-color-inactive);\n\tdisplay: flex;\n\tgrid-area: 1 / 1 / -1 / -1;\n\theight: 100%;\n\tjustify-content: center;\n\tpadding: 20px;\n\tfont-size: 88px;\n\tletter-spacing: 0.1em;\n\tfont-weight: 700;\n}\n\n.lyrics-lyricsContainer-UnsyncedLyricsPage {\n\tgrid-area: 1 / 1 / -1 / -1;\n\tgrid-template-rows: 1fr 20px;\n\tuser-select: text;\n\ttext-align: var(--lyrics-align-text);\n}\n\n.lyrics-lyricsContainer-LyricsUnsyncedPadding {\n\tdisplay: flex;\n\t/* 2 padding blocks & 1 line height & Provider block */\n\theight: calc(50vh - 91px - 8px - var(--lyrics-font-size));\n}\n.lyrics-lyricsContainer-UnsyncedLyricsPage:has(.lyrics-versionSelector, .lyrics-lyricsContainer-LyricsLine:nth-child(4))\n\t.lyrics-lyricsContainer-LyricsUnsyncedPadding {\n\theight: 10vh;\n}\n\n.lyrics-lyricsContainer-SyncedLyricsPage {\n\tdisplay: grid;\n\tgrid-area: 1 / 1 / -1 / -1;\n\tgrid-template-rows: 1fr 30px;\n\toverflow: hidden;\n\ttext-align: var(--lyrics-align-text);\n\tuser-select: text;\n}\n\n.lyrics-lyricsContainer-LyricsBackground {\n\tbackground-color: var(--lyrics-color-background);\n\tbackground-image: var(--lyrics-background-noise);\n\tgrid-area: 1 / 1 / -1 / -1;\n\ttransition: background-color 0.25s ease-out;\n}\n\n.lyrics-lyricsContainer-Provider {\n\talign-self: end;\n\tcolor: var(--lyrics-color-inactive);\n\tgrid-area: 2 / 1 / -1 / -1;\n\tjustify-self: stretch;\n\theight: 25px;\n\toverflow: hidden;\n\tbackground: linear-gradient(0deg, var(--lyrics-color-background) 30%, transparent);\n\tz-index: 1;\n\tpadding: 60px 20px 30px;\n\tpointer-events: none;\n}\n\n.lyrics-lyricsContainer-SyncedLyrics {\n\t--lyrics-line-height: calc(4px + var(--lyrics-font-size));\n\tgrid-area: 1 / 1 / -2 / -1;\n\theight: 0;\n}\n\n.lyrics-lyricsContainer-SyncedLyrics .lyrics-lyricsContainer-LyricsLine {\n\ttransform: translateY(calc(var(--position-index) * var(--lyrics-line-height) + var(--offset)));\n\ttransform-origin: var(--lyrics-align-text);\n\ttransition-timing-function: cubic-bezier(0, 0, 0.58, 1);\n\ttransition-duration: calc(var(--animation-index) * var(--animation-tempo) + 0.1s);\n\ttransition-property: transform, color, opacity;\n}\n\n.lyrics-lyricsContainer-LyricsContainer.blur-enabled .lyrics-lyricsContainer-SyncedLyrics .lyrics-lyricsContainer-LyricsLine {\n\tfilter: blur(calc(var(--blur-index) * 1.5px));\n}\n\n.lyrics-lyricsContainer-UnsyncedLyricsPage .lyrics-lyricsContainer-LyricsLine a {\n\tcolor: var(--lyrics-color-active);\n}\n\n.lyrics-lyricsContainer-UnsyncedLyricsPage .lyrics-lyricsContainer-LyricsLine {\n\tcolor: var(--lyrics-color-inactive);\n\ttransition: color 0.25s cubic-bezier(0, 0, 0.58, 1);\n}\n\n.lyrics-lyricsContainer-UnsyncedLyricsPage .lyrics-lyricsContainer-LyricsLine-active {\n\tcolor: var(--lyrics-color-active);\n}\n\n.lyrics-lyricsContainer-UnsyncedLyricsPage .lyrics-lyricsContainer-LyricsLine:hover {\n\tcolor: var(--lyrics-color-active);\n}\n\n.lyrics-lyricsContainer-SyncedLyrics .lyrics-lyricsContainer-LyricsLine {\n\tcolor: var(--lyrics-color-inactive);\n}\n\n.lyrics-lyricsContainer-SyncedLyrics .lyrics-lyricsContainer-LyricsLine:hover {\n\tcolor: var(--lyrics-color-active);\n}\n\n.lyrics-lyricsContainer-SyncedLyrics .lyrics-lyricsContainer-LyricsLine.lyrics-lyricsContainer-LyricsLine-active {\n\tcolor: var(--lyrics-color-active);\n\topacity: 1;\n\ttransform: translateY(calc(var(--position-index) * var(--lyrics-line-height) + var(--offset))) scale(1.1);\n\tfilter: none !important;\n}\n\n.lyrics-lyricsContainer-SyncedLyrics > .lyrics-lyricsContainer-LyricsLine-paddingLine {\n\topacity: 0;\n\tpointer-events: none;\n}\n\n.lyrics-lyricsContainer-LyricsLine,\n.lyrics-versionSelector {\n\tmargin-left: 100px;\n\tmargin-right: 100px;\n}\n\n@media (min-width: 1024px) {\n\t.lyrics-lyricsContainer-LyricsLine,\n\t.lyrics-versionSelector {\n\t\tmargin-left: 150px;\n\t\tmargin-right: 150px;\n\t}\n}\n\n@media (min-width: 1280px) {\n\t.lyrics-lyricsContainer-LyricsLine,\n\t.lyrics-versionSelector {\n\t\tmargin-left: 200px;\n\t\tmargin-right: 200px;\n\t}\n}\n\n.lyrics-lyricsContainer-UnsyncedLyricsPage .lyrics-lyricsContainer-LyricsLine {\n\tfont-size: var(--lyrics-font-size);\n\tfont-weight: 700;\n\tletter-spacing: -0.04em;\n\tline-height: calc(12px + var(--lyrics-font-size));\n}\n\n.lyrics-lyricsContainer-SyncedLyrics .lyrics-lyricsContainer-LyricsLine {\n\tfont-size: var(--lyrics-font-size);\n\tfont-weight: 700;\n\tletter-spacing: -0.04em;\n\tline-height: var(--lyrics-line-height);\n}\n\n@media (min-width: 1280px) {\n\t.lyrics-lyricsContainer-SyncedLyrics .lyrics-lyricsContainer-LyricsLine {\n\t\tfont-weight: 900;\n\t}\n}\n\n.lyrics-tabBar-headerItem {\n\t-webkit-app-region: no-drag;\n\tdisplay: inline-block;\n\tpointer-events: auto;\n}\n\n.lyrics-tabBar-headerItemLink {\n\tmargin: 0 8px 0 0;\n}\n\n.lyrics-tabBar-active {\n\tbackground-color: var(--spice-tab-active);\n\tborder-radius: 4px;\n}\n\n.lyrics-tabBar-headerItemLink {\n\tborder-radius: 4px;\n\tcolor: var(--spice-text);\n\tdisplay: inline-block;\n\tmargin: 0 8px;\n\tpadding: 8px 16px;\n\tposition: relative;\n\ttext-decoration: none !important;\n\tcursor: pointer;\n}\n\n.lyrics-tabBar-headerItemLink .main-type-mestoBold {\n\ttext-transform: capitalize;\n}\n\n.lyrics-tabBar-headerItemLink-locked::before {\n\tcontent: \"• \";\n}\n\n.lyrics-tabBar-nav {\n\t-webkit-app-region: drag;\n\tpointer-events: none;\n\twidth: 100%;\n}\n\n.lyrics-tabBar-header {\n\tdisplay: flex;\n\tflex-direction: row;\n\tflex-wrap: nowrap;\n\talign-items: center;\n\tjustify-content: flex-start;\n}\n\n.lyrics-tabBar-headerItem .optionsMenu-dropBox {\n\tcolor: var(--spice-text);\n\tborder: 0;\n\tmax-width: 150px;\n\theight: 42px;\n\tpadding: 0 30px 0 12px;\n\tbackground-color: initial;\n\tcursor: pointer;\n\tappearance: none;\n}\n\n.lyrics-tabBar-headerItem .optionsMenu-dropBox svg {\n\tposition: absolute;\n\tmargin-left: 8px;\n}\n\n#lyrics-plus-config-container option {\n\tbackground-color: var(--spice-button);\n}\n\ndiv.lyrics-tabBar-headerItemLink {\n\tpadding: 0;\n}\n\n.lyrics-tabBar-header button.switch {\n\tmargin-inline-end: 12px;\n\tmargin-inline-start: 0;\n}\n\n.lyrics-lyricsContainer-Karaoke-WordActive {\n\tcolor: var(--lyrics-color-active) !important;\n\tbackground-position: top left !important;\n}\n\n.lyrics-lyricsContainer-LyricsLine:hover .lyrics-lyricsContainer-Karaoke-Word {\n\tbackground-position: top left;\n}\n\n.lyrics-lyricsContainer-Karaoke-Word {\n\tcolor: var(--lyrics-color-inactive);\n\tbackground-image: linear-gradient(\n\t\tto right,\n\t\tvar(--lyrics-color-active),\n\t\tvar(--lyrics-color-active) 45%,\n\t\tvar(--lyrics-color-inactive) 55%,\n\t\tvar(--lyrics-color-inactive)\n\t);\n\t-webkit-background-clip: text;\n\t-webkit-text-fill-color: transparent;\n\tbackground-size: 225% 100%;\n\tbackground-position: top left 100%;\n\ttransition-property: color, background-position;\n\ttransition-duration: calc(var(--word-duration) + 0.05s);\n\ttransition-timing-function: linear;\n}\n\n.lyrics-lyricsContainer-LyricsLine a {\n\tbackground-color: transparent;\n\ttransition: background-color 0.25s cubic-bezier(0, 0, 0, 1);\n}\n\n.lyrics-lyricsContainer-LyricsLine a.fetched {\n\tbackground-color: var(--lyrics-highlight-background);\n}\n\n.lyrics-lyricsContainer-LyricsLine a,\n.lyrics-lyricsContainer-LyricsLine a:hover {\n\ttext-decoration: none !important;\n}\n\n.lyrics-lyricsContainer-LyricsLine a:hover {\n\tborder-bottom: 2px solid var(--lyrics-color-active);\n}\n\n.lyrics-Genius-noteTextContainer {\n\tfont-size: 18px;\n\tfont-weight: 400;\n\tletter-spacing: normal;\n\tline-height: 24px;\n\ttext-transform: none;\n\n\tpadding: 25px;\n\tbackground-color: var(--lyrics-color-active);\n\tborder-radius: 3px;\n\tcolor: var(--lyrics-highlight-background);\n\tbox-shadow: 0 10px 15px 5px rgb(0, 0, 0, 0.2);\n\tcursor: default;\n\ttext-align: left;\n}\n\n.lyrics-Genius-divider {\n\t/* border-bottom: 3px solid var(--lyrics-color-active); */\n\tline-height: 0;\n\tmargin-left: var(--link-left);\n}\n\n.lyrics-Searchbar {\n\tposition: sticky;\n\twidth: 300px;\n\theight: 40px;\n\tbottom: 10px;\n\tdisplay: flex;\n\tbackground-color: var(--lyrics-color-active) !important;\n\tcolor: var(--lyrics-highlight-background);\n\tmargin-left: 10px;\n\tborder-radius: 3px;\n\tbox-shadow: 0 10px 15px 5px rgb(0, 0, 0, 0.2);\n}\n\n.lyrics-Searchbar input {\n\twidth: 300px;\n\theight: 40px;\n\tbottom: 10px;\n\tborder: 0;\n\tcolor: var(--lyrics-highlight-background) !important;\n\tpadding: 0 36px;\n}\n\n.lyrics-Searchbar svg {\n\tposition: absolute;\n\tleft: 0;\n\theight: 40px;\n\tmargin-left: 10px;\n}\n\n.lyrics-Searchbar span {\n\tposition: relative;\n\tright: 0;\n\tline-height: 40px;\n\tmargin-right: 10px;\n\tfont-weight: 400;\n\tfont-size: 16px;\n\tletter-spacing: 0.2em;\n}\n\n.lyrics-Searchbar-highlight {\n\tposition: fixed;\n\twidth: 100%;\n\theight: var(--search-highlight-height);\n\tleft: 0;\n\ttop: var(--search-highlight-top);\n\tbackground-color: var(--lyrics-highlight-background);\n\topacity: 0.5;\n\tpointer-events: none;\n}\n\n.lyrics-versionSelector {\n\tmax-width: 500px;\n\tborder-radius: 4px;\n\tdisplay: inline-block;\n\tposition: relative;\n\tcursor: pointer;\n\tbox-shadow: 0 10px 15px 5px rgb(0, 0, 0, 0.2);\n\tbackground-color: var(--lyrics-highlight-background);\n\tmargin-bottom: 75px;\n}\n\n.lyrics-versionSelector select {\n\tborder: 0;\n\tborder-radius: 4px;\n\tmax-width: 500px;\n\theight: 42px;\n\tpadding: 0 30px 0 12px;\n\tcursor: pointer;\n\tappearance: none;\n\tfont-size: 18px;\n\tbackground-color: var(--lyrics-color-active);\n\tcolor: var(--lyrics-highlight-background);\n}\n.lyrics-versionSelector option {\n\tbackground-color: var(--lyrics-color-active);\n}\n\n.lyrics-versionSelector svg {\n\tposition: absolute;\n\theight: 42px;\n\tright: 10px;\n\tpointer-events: none;\n\tfill: var(--lyrics-highlight-background);\n}\n\n/** Setting menu */\n.lyrics-tooltip-wrapper .setting-row::after,\n#lyrics-plus-config-container .setting-row::after {\n\tcontent: \"\";\n\tdisplay: table;\n\tclear: both;\n}\n.lyrics-tooltip-wrapper .setting-row .col,\n#lyrics-plus-config-container .setting-row .col {\n\tpadding: 16px 0 4px;\n\talign-items: center;\n}\n.lyrics-tooltip-wrapper .setting-row .col.description,\n#lyrics-plus-config-container .setting-row .col.description {\n\tfloat: left;\n\tpadding-right: 15px;\n\tcursor: default;\n}\n.lyrics-tooltip-wrapper .setting-row .col.action,\n#lyrics-plus-config-container .setting-row .col.action {\n\tfloat: right;\n\tdisplay: flex;\n\tjustify-content: flex-end;\n\talign-items: center;\n}\n.lyrics-tooltip-wrapper button.switch,\n#lyrics-plus-config-container button.switch {\n\talign-items: center;\n\tborder: 0px;\n\tborder-radius: 50%;\n\tbackground-color: rgba(var(--spice-rgb-shadow), 0.7);\n\tcolor: var(--spice-text);\n\tcursor: pointer;\n\tmargin-inline-start: 12px;\n\tpadding: 8px;\n\twidth: 32px;\n\theight: 32px;\n}\n.lyrics-tooltip-wrapper button.switch.disabled,\n.lyrics-tooltip-wrapper button.switch[disabled],\n#lyrics-plus-config-container button.switch.disabled,\n#lyrics-plus-config-container button.switch[disabled] {\n\tcolor: rgba(var(--spice-rgb-text), 0.3);\n}\n.lyrics-tooltip-wrapper button.switch.small,\n#lyrics-plus-config-container button.switch.small {\n\twidth: 22px;\n\theight: 22px;\n\tpadding: 3px;\n}\n\n.lyrics-tooltip-wrapper input,\n#lyrics-plus-config-container input {\n\twidth: 100%;\n\tmargin-top: 10px;\n\tpadding: 0 5px;\n\theight: 32px;\n\tborder: 0;\n\tcolor: var(--spice-text);\n\tbackground-color: initial;\n\tborder-bottom: 1px solid var(--spice-text);\n}\n.lyrics-tooltip-wrapper .col.action .adjust-value,\n#lyrics-plus-config-container .col.action .adjust-value {\n\tmargin-inline-start: 12px;\n\tmin-width: 22px;\n\ttext-align: center;\n}\n\n.lyrics-tooltip-wrapper .col.action span,\n#lyrics-plus-config-container .col.action span {\n\tfont-size: 14px;\n\topacity: 0.8;\n}\n.lyrics-tooltip-wrapper .col.action .btn,\n#lyrics-plus-config-container .col.action .btn {\n\tfont-weight: 700;\n\tbackground-color: transparent;\n\tborder-radius: 500px;\n\ttransition-duration: 33ms;\n\ttransition-property: background-color, border-color, color, box-shadow, filter, transform;\n\tpadding-inline: 15px;\n\tborder: 1px solid #727272;\n\tcolor: var(--spice-text);\n\tmin-block-size: 32px;\n\tcursor: pointer;\n}\n\n.lyrics-tooltip-wrapper .col.action .btn:hover,\n#lyrics-plus-config-container .col.action .btn:hover {\n\ttransform: scale(1.04);\n\tborder-color: var(--spice-text);\n}\n\n.lyrics-tooltip-wrapper .col.action .btn:disabled,\n#lyrics-plus-config-container .col.action .btn:disabled {\n\topacity: 0.5;\n\tcursor: not-allowed;\n}\n.lyrics-tooltip-wrapper .col.action .main-dropDown-dropDown,\n.lyrics-tooltip-wrapper .col.action input,\n#lyrics-plus-config-container .col.action .main-dropDown-dropDown,\n#lyrics-plus-config-container .col.action input {\n\twidth: 150px;\n}\n\n#lyrics-fullscreen-container {\n\tposition: fixed;\n\twidth: 100vw;\n\theight: 100vh;\n\tcursor: default;\n\tleft: 0;\n\ttop: 0;\n}\n\n#lyrics-fullscreen-container .lyrics-lyricsContainer-LyricsContainer {\n\theight: 100vh;\n\tmargin-bottom: 0;\n\tmargin-top: 0;\n\toverflow-y: auto;\n}\n\n#lyrics-fullscreen-container .lyrics-lyricsContainer-LyricsContainer::-webkit-scrollbar {\n\tbackground-color: var(--lyrics-color-background);\n}\n\n.lyrics-lyricsContainer-LyricsContainer.fad-enabled {\n\theight: 100vh;\n\tmargin-top: 0;\n\tmargin-bottom: 0;\n\toverflow-y: scroll;\n}\n\n.lyrics-lyricsContainer-LyricsContainer.fad-enabled .lyrics-lyricsContainer-LyricsLine {\n\tmargin-left: 100px;\n\tmargin-right: 100px;\n}\n\n.lyrics-lyricsContainer-LyricsContainer.fad-enabled .lyrics-lyricsContainer-LyricsBackground,\n.lyrics-lyricsContainer-LyricsContainer.fad-enabled .lyrics-lyricsContainer-Provider {\n\tdisplay: none;\n}\n\n.lyrics-lyricsContainer-LyricsContainer.fad-enabled .lyrics-lyricsContainer-SyncedLyricsPage {\n\twidth: 100%;\n}\n\n.lyrics-lyricsContainer-LyricsContainer.fad-enabled .lyrics-config-button-container {\n\topacity: 0;\n\ttransition: opacity 0.2s cubic-bezier(0, 0, 0.58, 1);\n}\n\n.lyrics-lyricsContainer-LyricsContainer.fad-enabled:hover .lyrics-config-button-container {\n\topacity: 1;\n}\n\n.lyrics-idling-indicator {\n\tdisplay: inline-block;\n\topacity: 1;\n\ttransition: opacity 0.2s cubic-bezier(0, 0, 0.58, 1);\n}\n\n.lyrics-idling-indicator-hidden {\n\topacity: 0;\n}\n\n.lyrics-idling-indicator__circle {\n\tbackground-color: var(--lyrics-color-active);\n\tborder-radius: 50%;\n\tdisplay: inline-block;\n\topacity: 0.5;\n\tmargin-right: calc(var(--lyrics-font-size) / 4);\n\n\ttransform-origin: center;\n\ttransition-timing-function: linear;\n\ttransition-duration: var(--indicator-delay);\n\ttransition-property: transform, opacity;\n\theight: var(--lyrics-font-size);\n\twidth: var(--lyrics-font-size);\n\ttransform: scale(0.5);\n}\n.lyrics-idling-indicator__circle.active {\n\topacity: 1;\n\ttransform: scale(0.7);\n}\n\n.lyrics-config-button-container {\n\t-webkit-margin-end: 32px;\n\t-webkit-box-pack: end;\n\tpointer-events: none;\n\tbottom: 32px;\n\tdisplay: flex;\n\tjustify-content: flex-end;\n\tmargin: -52px 0 0;\n\tmargin-inline-end: 32px;\n\tposition: sticky;\n\tz-index: 2;\n}\n\n.lyrics-config-button-container > * {\n\tpointer-events: auto;\n}\n\n@-webkit-keyframes spin {\n\t0% {\n\t\t-webkit-transform: rotate(0deg);\n\t}\n\t100% {\n\t\t-webkit-transform: rotate(360deg);\n\t}\n}\n\n.lyrics-config-button {\n\talign-items: center;\n\tbackground-color: rgba(0, 0, 0, 0.5);\n\tborder: 0;\n\tmargin: 5px;\n\tborder-radius: 4px;\n\tcolor: #eee;\n\tcursor: pointer;\n\tdisplay: flex;\n\tgap: 8px;\n\tjustify-content: center;\n\tpadding: 12px;\n\theight: 40px;\n\twidth: 40px;\n}\n\n.lyrics-config-button-container .main-contextMenu-menu {\n\tcolor: var(--spice-text);\n\tpadding: 12px 12px 6px;\n}\n\n.lyrics-lyricsContainer-UnsyncedLyricsPage .split {\n\tdisplay: flex;\n}\n.lyrics-lyricsContainer-UnsyncedLyricsPage .split > div {\n\tflex: 50%;\n}\n.lyrics-lyricsContainer-UnsyncedLyricsPage .split > div > div:not(.lyrics-versionSelector) {\n\tmargin-left: 0;\n\tmargin-right: 0;\n}\n.split .lyrics-lyricsContainer-LyricsLine {\n\tpadding-left: 50px;\n\tpadding-right: 50px;\n}\n.split .lyrics-versionSelector {\n\tmargin-right: 50px;\n\tmargin-left: 50px;\n}\n.split .lyrics-versionSelector select {\n\twidth: 100%;\n}\n.main-content-view {\n\theight: 100%;\n}\n\n@media (min-width: 1024px) {\n\t.split .lyrics-lyricsContainer-LyricsLine {\n\t\tpadding-left: 75px;\n\t\tpadding-right: 75px;\n\t}\n\t.split .lyrics-versionSelector {\n\t\tmargin-right: 75px;\n\t\tmargin-left: 75px;\n\t}\n}\n\n@media (min-width: 1280px) {\n\t.split .lyrics-lyricsContainer-LyricsLine {\n\t\tpadding-left: 100px;\n\t\tpadding-right: 100px;\n\t}\n\t.split .lyrics-versionSelector {\n\t\tmargin-right: 100px;\n\t\tmargin-left: 100px;\n\t}\n}\n\n.lyrics-lyricsContainer-Performer {\n\tdisplay: block;\n\tfont-size: 0.6em;\n\topacity: 0.7;\n\tline-height: 1.2em;\n\t/* color: var(--lyrics-color-inactive); */\n}\n"
  },
  {
    "path": "CustomApps/new-releases/Card.js",
    "content": "function DraggableComponent({ uri, title, children }) {\n\tconst dragHandler = Spicetify.ReactHook.DragHandler?.([uri], title);\n\treturn dragHandler\n\t\t? react.cloneElement(children, {\n\t\t\t\tonDragStart: dragHandler,\n\t\t\t\tdraggable: \"true\",\n\t\t\t})\n\t\t: children;\n}\n\nclass Card extends react.Component {\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tObject.assign(this, props);\n\t\tthis.href = URI.fromString(this.uri).toURLPath(true);\n\t\tthis.artistHref = URI.fromString(this.artist.uri).toURLPath(true);\n\t\tconst uriType = Spicetify.URI.fromString(this.uri)?.type;\n\t\tswitch (uriType) {\n\t\t\tcase Spicetify.URI.Type.ALBUM:\n\t\t\tcase Spicetify.URI.Type.TRACK:\n\t\t\t\tthis.menuType = Spicetify.ReactComponent.AlbumMenu;\n\t\t\t\tbreak;\n\t\t}\n\t\tthis.menuType = this.menuType || \"div\";\n\t}\n\n\tplay(event) {\n\t\tSpicetify.Player.playUri(this.uri, this.context);\n\t\tevent.stopPropagation();\n\t}\n\n\tcloseButtonClicked(event) {\n\t\tevent.stopPropagation();\n\n\t\tremoveCards(this.props.uri);\n\n\t\tSpicetify.Snackbar.enqueueCustomSnackbar\n\t\t\t? Spicetify.Snackbar.enqueueCustomSnackbar(\"dismissed-release\", {\n\t\t\t\t\tkeyPrefix: \"dismissed-release\",\n\t\t\t\t\tchildren: Spicetify.ReactComponent.Snackbar.wrapper({\n\t\t\t\t\t\tchildren: Spicetify.ReactComponent.Snackbar.simpleLayout({\n\t\t\t\t\t\t\tleading: Spicetify.ReactComponent.Snackbar.styledImage({\n\t\t\t\t\t\t\t\tsrc: this.props.imageURL,\n\t\t\t\t\t\t\t\timageHeight: \"24px\",\n\t\t\t\t\t\t\t\timageWidth: \"24px\",\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\tcenter: Spicetify.React.createElement(\"div\", {\n\t\t\t\t\t\t\t\tdangerouslySetInnerHTML: {\n\t\t\t\t\t\t\t\t\t__html: `Dismissed <b>${this.title}</b>.`,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\ttrailing: Spicetify.ReactComponent.Snackbar.ctaText({\n\t\t\t\t\t\t\t\tctaText: \"Undo\",\n\t\t\t\t\t\t\t\tonCtaClick: () => removeCards(this.props.uri, \"undo\"),\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t}),\n\t\t\t\t\t}),\n\t\t\t\t})\n\t\t\t: Spicetify.showNotification(`Dismissed <b>${this.title}</b> from <br>${this.artist.name}</b>`);\n\t}\n\n\trender() {\n\t\tconst detail = [];\n\t\tthis.visual.type && detail.push(this.type);\n\t\tif (this.visual.count && this.trackCount) {\n\t\t\tdetail.push(Spicetify.Locale.get(\"tracklist-header.songs-counter\", this.trackCount));\n\t\t}\n\n\t\treturn react.createElement(\n\t\t\tSpicetify.ReactComponent.RightClickMenu || \"div\",\n\t\t\t{\n\t\t\t\tmenu: react.createElement(this.menuType, { uri: this.uri }),\n\t\t\t},\n\t\t\treact.createElement(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tclassName: \"main-card-card\",\n\t\t\t\t\tonClick: (event) => {\n\t\t\t\t\t\tHistory.push(this.href);\n\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\treact.createElement(\n\t\t\t\t\tDraggableComponent,\n\t\t\t\t\t{\n\t\t\t\t\t\turi: this.uri,\n\t\t\t\t\t\ttitle: this.title,\n\t\t\t\t\t},\n\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tclassName: \"main-card-draggable\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tclassName: \"main-card-imageContainer\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tclassName: \"main-cardImage-imageWrapper\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t\t\t\t{},\n\t\t\t\t\t\t\t\t\treact.createElement(\"img\", {\n\t\t\t\t\t\t\t\t\t\t\"aria-hidden\": \"false\",\n\t\t\t\t\t\t\t\t\t\tdraggable: \"false\",\n\t\t\t\t\t\t\t\t\t\tloading: \"lazy\",\n\t\t\t\t\t\t\t\t\t\tsrc: this.imageURL,\n\t\t\t\t\t\t\t\t\t\tclassName: \"main-image-image main-cardImage-image\",\n\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tclassName: \"main-card-PlayButtonContainer\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tclassName: \"main-playButton-PlayButton main-playButton-primary\",\n\t\t\t\t\t\t\t\t\t\t\"aria-label\": Spicetify.Locale.get(\"play\"),\n\t\t\t\t\t\t\t\t\t\tstyle: { \"--size\": \"40px\" },\n\t\t\t\t\t\t\t\t\t\tonClick: this.play.bind(this),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\t\t\"button\",\n\t\t\t\t\t\t\t\t\t\tnull,\n\t\t\t\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\t\t\t\"span\",\n\t\t\t\t\t\t\t\t\t\t\tnull,\n\t\t\t\t\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\t\t\t\t\"svg\",\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\theight: \"24\",\n\t\t\t\t\t\t\t\t\t\t\t\t\trole: \"img\",\n\t\t\t\t\t\t\t\t\t\t\t\t\twidth: \"24\",\n\t\t\t\t\t\t\t\t\t\t\t\t\tviewBox: \"0 0 24 24\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\"aria-hidden\": \"true\",\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\treact.createElement(\"polygon\", {\n\t\t\t\t\t\t\t\t\t\t\t\t\tpoints: \"21.57 12 5.98 3 5.98 21 21.57 12\",\n\t\t\t\t\t\t\t\t\t\t\t\t\tfill: \"currentColor\",\n\t\t\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\tSpicetify.ReactComponent.TooltipWrapper,\n\t\t\t\t\t\t\t\t{ label: \"Dismiss\" },\n\t\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\t\"button\",\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tclassName: \"main-card-closeButton\",\n\t\t\t\t\t\t\t\t\t\tonClick: this.closeButtonClicked.bind(this),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\t\t\"svg\",\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\twidth: \"16\",\n\t\t\t\t\t\t\t\t\t\t\theight: \"16\",\n\t\t\t\t\t\t\t\t\t\t\tviewBox: \"0 0 16 16\",\n\t\t\t\t\t\t\t\t\t\t\txmlns: \"http://www.w3.org/2000/svg\",\n\t\t\t\t\t\t\t\t\t\t\tclassName: \"main-card-closeButton-svg\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\treact.createElement(\"path\", {\n\t\t\t\t\t\t\t\t\t\t\td: \"M2.47 2.47a.75.75 0 0 1 1.06 0L8 6.94l4.47-4.47a.75.75 0 1 1 1.06 1.06L9.06 8l4.47 4.47a.75.75 0 1 1-1.06 1.06L8 9.06l-4.47 4.47a.75.75 0 0 1-1.06-1.06L6.94 8 2.47 3.53a.75.75 0 0 1 0-1.06Z\",\n\t\t\t\t\t\t\t\t\t\t\tfill: \"var(--spice-text)\",\n\t\t\t\t\t\t\t\t\t\t\tfillRule: \"evenodd\",\n\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t),\n\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tclassName: \"main-card-cardMetadata\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\"a\",\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tdraggable: \"false\",\n\t\t\t\t\t\t\t\t\ttitle: this.title,\n\t\t\t\t\t\t\t\t\tclassName: \"main-cardHeader-link\",\n\t\t\t\t\t\t\t\t\tdir: \"auto\",\n\t\t\t\t\t\t\t\t\thref: this.href,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tclassName: \"main-cardHeader-text main-type-balladBold\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tthis.title\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tdetail.length > 0 &&\n\t\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tclassName: \"main-cardSubHeader-root main-type-mestoBold new-releases-cardSubHeader\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\treact.createElement(\"span\", null, detail.join(\" • \"))\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\tDraggableComponent,\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\turi: this.artist.uri,\n\t\t\t\t\t\t\t\t\ttitle: this.artist.name,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\t\"a\",\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tclassName: \"main-cardSubHeader-root main-type-mesto new-releases-cardSubHeader\",\n\t\t\t\t\t\t\t\t\t\thref: this.artistHref,\n\t\t\t\t\t\t\t\t\t\tonClick: (event) => {\n\t\t\t\t\t\t\t\t\t\t\tHistory.push(this.artistHref);\n\t\t\t\t\t\t\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\treact.createElement(\"span\", null, this.artist.name)\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t),\n\t\t\t\t\t\treact.createElement(\"div\", {\n\t\t\t\t\t\t\tclassName: \"main-card-cardLink\",\n\t\t\t\t\t\t})\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "CustomApps/new-releases/Icons.js",
    "content": "const LoadingIcon = react.createElement(\n\t\"svg\",\n\t{\n\t\twidth: \"100px\",\n\t\theight: \"100px\",\n\t\tviewBox: \"0 0 100 100\",\n\t\tpreserveAspectRatio: \"xMidYMid\",\n\t},\n\treact.createElement(\n\t\t\"circle\",\n\t\t{\n\t\t\tcx: \"50\",\n\t\t\tcy: \"50\",\n\t\t\tr: \"0\",\n\t\t\tfill: \"none\",\n\t\t\tstroke: \"currentColor\",\n\t\t\t\"stroke-width\": \"2\",\n\t\t},\n\t\treact.createElement(\"animate\", {\n\t\t\tattributeName: \"r\",\n\t\t\trepeatCount: \"indefinite\",\n\t\t\tdur: \"1s\",\n\t\t\tvalues: \"0;40\",\n\t\t\tkeyTimes: \"0;1\",\n\t\t\tkeySplines: \"0 0.2 0.8 1\",\n\t\t\tcalcMode: \"spline\",\n\t\t\tbegin: \"0s\",\n\t\t}),\n\t\treact.createElement(\"animate\", {\n\t\t\tattributeName: \"opacity\",\n\t\t\trepeatCount: \"indefinite\",\n\t\t\tdur: \"1s\",\n\t\t\tvalues: \"1;0\",\n\t\t\tkeyTimes: \"0;1\",\n\t\t\tkeySplines: \"0.2 0 0.8 1\",\n\t\t\tcalcMode: \"spline\",\n\t\t\tbegin: \"0s\",\n\t\t})\n\t),\n\treact.createElement(\n\t\t\"circle\",\n\t\t{\n\t\t\tcx: \"50\",\n\t\t\tcy: \"50\",\n\t\t\tr: \"0\",\n\t\t\tfill: \"none\",\n\t\t\tstroke: \"currentColor\",\n\t\t\t\"stroke-width\": \"2\",\n\t\t},\n\t\treact.createElement(\"animate\", {\n\t\t\tattributeName: \"r\",\n\t\t\trepeatCount: \"indefinite\",\n\t\t\tdur: \"1s\",\n\t\t\tvalues: \"0;40\",\n\t\t\tkeyTimes: \"0;1\",\n\t\t\tkeySplines: \"0 0.2 0.8 1\",\n\t\t\tcalcMode: \"spline\",\n\t\t\tbegin: \"-0.5s\",\n\t\t}),\n\t\treact.createElement(\"animate\", {\n\t\t\tattributeName: \"opacity\",\n\t\t\trepeatCount: \"indefinite\",\n\t\t\tdur: \"1s\",\n\t\t\tvalues: \"1;0\",\n\t\t\tkeyTimes: \"0;1\",\n\t\t\tkeySplines: \"0.2 0 0.8 1\",\n\t\t\tcalcMode: \"spline\",\n\t\t\tbegin: \"-0.5s\",\n\t\t})\n\t)\n);\n\nclass LoadMoreIcon extends react.Component {\n\trender() {\n\t\treturn react.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tonClick: this.props.onClick,\n\t\t\t},\n\t\t\treact.createElement(\n\t\t\t\t\"p\",\n\t\t\t\t{\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\tfontSize: 100,\n\t\t\t\t\t\tlineHeight: \"65px\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"»\"\n\t\t\t),\n\t\t\treact.createElement(\n\t\t\t\t\"span\",\n\t\t\t\t{\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\tfontSize: 20,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"Load more\"\n\t\t\t)\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "CustomApps/new-releases/Settings.js",
    "content": "const ButtonSVG = ({ icon, active = true, onClick }) => {\n\treturn react.createElement(\n\t\t\"button\",\n\t\t{\n\t\t\tclassName: `switch${active ? \"\" : \" disabled\"}`,\n\t\t\tonClick,\n\t\t},\n\t\treact.createElement(\"svg\", {\n\t\t\twidth: 16,\n\t\t\theight: 16,\n\t\t\tviewBox: \"0 0 16 16\",\n\t\t\tfill: \"currentColor\",\n\t\t\tdangerouslySetInnerHTML: {\n\t\t\t\t__html: icon,\n\t\t\t},\n\t\t})\n\t);\n};\n\nconst ButtonText = ({ text, active = true, onClick }) => {\n\treturn react.createElement(\n\t\t\"button\",\n\t\t{\n\t\t\tclassName: `text${active ? \"\" : \" disabled\"}`,\n\t\t\tonClick,\n\t\t},\n\t\ttext\n\t);\n};\n\nconst ConfigSlider = ({ name, defaultValue, onChange = () => {} }) => {\n\tconst [active, setActive] = useState(defaultValue);\n\n\tconst toggleState = useCallback(() => {\n\t\tconst state = !active;\n\t\tsetActive(state);\n\t\tonChange(state);\n\t}, [active]);\n\n\treturn react.createElement(\n\t\t\"div\",\n\t\t{\n\t\t\tclassName: \"setting-row\",\n\t\t},\n\t\treact.createElement(\n\t\t\t\"label\",\n\t\t\t{\n\t\t\t\tclassName: \"col description\",\n\t\t\t},\n\t\t\tname\n\t\t),\n\t\treact.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tclassName: \"col action\",\n\t\t\t},\n\t\t\treact.createElement(ButtonSVG, {\n\t\t\t\ticon: Spicetify.SVGIcons.check,\n\t\t\t\tactive,\n\t\t\t\tonClick: toggleState,\n\t\t\t})\n\t\t)\n\t);\n};\n\nconst ConfigSelection = ({ name, defaultValue, options, onChange = () => {} }) => {\n\tconst [value, setValue] = useState(defaultValue);\n\n\tconst setValueCallback = useCallback(\n\t\t(event) => {\n\t\t\tconst value = event.target.value;\n\t\t\tsetValue(value);\n\t\t\tonChange(value);\n\t\t},\n\t\t[value]\n\t);\n\n\treturn react.createElement(\n\t\t\"div\",\n\t\t{\n\t\t\tclassName: \"setting-row\",\n\t\t},\n\t\treact.createElement(\n\t\t\t\"label\",\n\t\t\t{\n\t\t\t\tclassName: \"col description\",\n\t\t\t},\n\t\t\tname\n\t\t),\n\t\treact.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tclassName: \"col action\",\n\t\t\t},\n\t\t\treact.createElement(\n\t\t\t\t\"select\",\n\t\t\t\t{\n\t\t\t\t\tvalue,\n\t\t\t\t\tonChange: setValueCallback,\n\t\t\t\t},\n\t\t\t\tObject.keys(options).map((item) =>\n\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\"option\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvalue: item,\n\t\t\t\t\t\t},\n\t\t\t\t\t\toptions[item]\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t)\n\t\t)\n\t);\n};\n\nconst ConfigInput = ({ name, defaultValue, onChange = () => {} }) => {\n\tconst [value, setValue] = useState(defaultValue);\n\n\tconst setValueCallback = useCallback(\n\t\t(event) => {\n\t\t\tconst value = event.target.value;\n\t\t\tsetValue(value);\n\t\t\tonChange(value);\n\t\t},\n\t\t[value]\n\t);\n\n\treturn react.createElement(\n\t\t\"div\",\n\t\t{\n\t\t\tclassName: \"setting-row\",\n\t\t},\n\t\treact.createElement(\n\t\t\t\"label\",\n\t\t\t{\n\t\t\t\tclassName: \"col description\",\n\t\t\t},\n\t\t\tname\n\t\t),\n\t\treact.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tclassName: \"col action\",\n\t\t\t},\n\t\t\treact.createElement(\"input\", {\n\t\t\t\tvalue,\n\t\t\t\tonChange: setValueCallback,\n\t\t\t})\n\t\t)\n\t);\n};\n\nconst OptionList = ({ items, onChange }) => {\n\tconst [_, setItems] = useState(items);\n\treturn items.map((item) => {\n\t\tif (!item.when()) {\n\t\t\treturn;\n\t\t}\n\t\treturn react.createElement(item.type, {\n\t\t\tname: item.desc,\n\t\t\tdefaultValue: item.defaultValue,\n\t\t\toptions: item.options,\n\t\t\tonChange: (value) => {\n\t\t\t\tonChange(item.key, value);\n\t\t\t\tsetItems([...items]);\n\t\t\t},\n\t\t});\n\t});\n};\n\nfunction openConfig() {\n\tconst configContainer = react.createElement(\n\t\t\"div\",\n\t\t{\n\t\t\tid: `${APP_NAME}-config-container`,\n\t\t},\n\t\treact.createElement(OptionList, {\n\t\t\titems: [\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Time range\",\n\t\t\t\t\tkey: \"range\",\n\t\t\t\t\tdefaultValue: CONFIG.range,\n\t\t\t\t\ttype: ConfigSelection,\n\t\t\t\t\toptions: {\n\t\t\t\t\t\t30: \"30 days\",\n\t\t\t\t\t\t60: \"60 days\",\n\t\t\t\t\t\t90: \"90 days\",\n\t\t\t\t\t\t120: \"120 days\",\n\t\t\t\t\t},\n\t\t\t\t\twhen: () => true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Date locale\",\n\t\t\t\t\tkey: \"locale\",\n\t\t\t\t\tdefaultValue: CONFIG.locale,\n\t\t\t\t\ttype: ConfigInput,\n\t\t\t\t\twhen: () => true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Relative date\",\n\t\t\t\t\tkey: \"relative\",\n\t\t\t\t\tdefaultValue: CONFIG.relative,\n\t\t\t\t\ttype: ConfigSlider,\n\t\t\t\t\twhen: () => true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Show type\",\n\t\t\t\t\tkey: \"visual:type\",\n\t\t\t\t\tdefaultValue: CONFIG.visual.type,\n\t\t\t\t\ttype: ConfigSlider,\n\t\t\t\t\twhen: () => true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Show track count\",\n\t\t\t\t\tkey: \"visual:count\",\n\t\t\t\t\tdefaultValue: CONFIG.visual.count,\n\t\t\t\t\ttype: ConfigSlider,\n\t\t\t\t\twhen: () => true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Fetch new podcast\",\n\t\t\t\t\tkey: \"podcast\",\n\t\t\t\t\tdefaultValue: CONFIG.podcast,\n\t\t\t\t\ttype: ConfigSlider,\n\t\t\t\t\twhen: () => true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: \"Fetch new music\",\n\t\t\t\t\tkey: \"music\",\n\t\t\t\t\tdefaultValue: CONFIG.music,\n\t\t\t\t\ttype: ConfigSlider,\n\t\t\t\t\twhen: () => true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: Spicetify.Locale.get(\"artist.albums\"),\n\t\t\t\t\tkey: \"album\",\n\t\t\t\t\tdefaultValue: CONFIG.album,\n\t\t\t\t\ttype: ConfigSlider,\n\t\t\t\t\twhen: () => CONFIG.music,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tdesc: Spicetify.Locale.get(\"artist.singles\"),\n\t\t\t\t\tkey: \"single-ep\",\n\t\t\t\t\tdefaultValue: CONFIG[\"single-ep\"],\n\t\t\t\t\ttype: ConfigSlider,\n\t\t\t\t\twhen: () => CONFIG.music,\n\t\t\t\t},\n\t\t\t\t/* {\n\t\t\t\t\tdesc: Spicetify.Locale.get(\"artist.appears-on\"),\n\t\t\t\t\tkey: \"appears-on\",\n\t\t\t\t\tdefaultValue: CONFIG[\"appears-on\"],\n\t\t\t\t\ttype: ConfigSlider,\n\t\t\t\t\twhen: () => CONFIG[\"music\"]\n\t\t\t\t}, */\n\t\t\t\t{\n\t\t\t\t\tdesc: Spicetify.Locale.get(\"artist.compilations\"),\n\t\t\t\t\tkey: \"compilations\",\n\t\t\t\t\tdefaultValue: CONFIG.compilations,\n\t\t\t\t\ttype: ConfigSlider,\n\t\t\t\t\twhen: () => CONFIG.music,\n\t\t\t\t},\n\t\t\t],\n\t\t\tonChange: (name, value) => {\n\t\t\t\tconst subs = name.split(\":\");\n\t\t\t\tif (subs.length > 1) {\n\t\t\t\t\tCONFIG[subs[0]][subs[1]] = value;\n\t\t\t\t\tgridUpdatePostsVisual();\n\t\t\t\t} else {\n\t\t\t\t\tCONFIG[name] = value;\n\t\t\t\t}\n\t\t\t\tlocalStorage.setItem(`${APP_NAME}:${name}`, value);\n\t\t\t},\n\t\t}),\n\t\treact.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tclassName: \"setting-row\",\n\t\t\t},\n\t\t\treact.createElement(\n\t\t\t\t\"label\",\n\t\t\t\t{\n\t\t\t\t\tclassName: \"col description\",\n\t\t\t\t},\n\t\t\t\t\"Dismissed releases\"\n\t\t\t),\n\t\t\treact.createElement(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tclassName: \"col action\",\n\t\t\t\t},\n\t\t\t\treact.createElement(ButtonText, {\n\t\t\t\t\ttext: Spicetify.Locale.get(\"equalizer.reset\"),\n\t\t\t\t\tonClick: removeCards.bind(this, null, \"reset\"),\n\t\t\t\t})\n\t\t\t)\n\t\t)\n\t);\n\n\tSpicetify.PopupModal.display({\n\t\ttitle: Spicetify.Locale.get(\"new_releases\"),\n\t\tcontent: configContainer,\n\t});\n}\n"
  },
  {
    "path": "CustomApps/new-releases/index.js",
    "content": "// Run \"npm i @types/react\" to have this type package available in workspace\n/// <reference types=\"react\" />\n\n/** @type {React} */\nconst {\n\tURI,\n\tReact: react,\n\tReact: { useState, useEffect, useCallback },\n\tReactDOM: reactDOM,\n\tPlatform: { History },\n\tCosmosAsync,\n} = Spicetify;\n\n// Define a function called \"render\" to specify app entry point\n// This function will be used to mount app to main view.\nfunction render() {\n\treturn react.createElement(Grid);\n}\n\nfunction getConfig(name, defaultVal = true) {\n\tconst value = localStorage.getItem(name);\n\treturn value ? value === \"true\" : defaultVal;\n}\n\nconst APP_NAME = \"new-releases\";\n\nconst CONFIG = {\n\tvisual: {\n\t\ttype: getConfig(\"new-releases:visual:type\", true),\n\t\tcount: getConfig(\"new-releases:visual:count\", true),\n\t},\n\tpodcast: getConfig(\"new-releases:podcast\", false),\n\tmusic: getConfig(\"new-releases:music\", true),\n\talbum: getConfig(\"new-releases:album\", true),\n\t\"single-ep\": getConfig(\"new-releases:single-ep\", true),\n\t// [\"appears-on\"]: getConfig(\"new-releases:appears-on\", false),\n\tcompilations: getConfig(\"new-releases:compilations\", false),\n\trange: localStorage.getItem(\"new-releases:range\") || \"30\",\n\tlocale: localStorage.getItem(\"new-releases:locale\") || navigator.language,\n\trelative: getConfig(\"new-releases:relative\", false),\n};\n\nlet dismissed;\ntry {\n\tdismissed = JSON.parse(Spicetify.LocalStorage.get(\"new-releases:dismissed\"));\n\tif (!Array.isArray(dismissed)) throw \"\";\n} catch {\n\tdismissed = [];\n}\n\nlet gridList = [];\nlet lastScroll = 0;\n\nlet gridUpdatePostsVisual;\nlet removeCards;\n\nlet today = Date.now();\nCONFIG.range = Number.parseInt(CONFIG.range) || 30;\nconst DAY_DIVIDER = 24 * 3600 * 1000;\nlet limitInMs = CONFIG.range * DAY_DIVIDER;\nconst dateFormat = {\n\tyear: \"numeric\",\n\tmonth: \"short\",\n\tday: \"2-digit\",\n};\nconst relativeDateFormat = {\n\tnumeric: \"auto\",\n};\nlet separatedByDate = {};\nlet dateList = [];\n\nclass Grid extends react.Component {\n\tviewportSelector = document.querySelector(\"#main .os-viewport\") ? \"#main .os-viewport\" : \"#main .main-view-container__scroll-node\";\n\n\tconstructor() {\n\t\tsuper();\n\t\tthis.state = {\n\t\t\tcards: [],\n\t\t\trest: true,\n\t\t};\n\t}\n\n\tupdatePostsVisual() {\n\t\tgridList = [];\n\t\tfor (const date of dateList) {\n\t\t\tif (separatedByDate[date].every((card) => dismissed.includes(card.props.uri))) continue;\n\n\t\t\tgridList.push(\n\t\t\t\treact.createElement(\n\t\t\t\t\t\"div\",\n\t\t\t\t\t{\n\t\t\t\t\t\tclassName: \"new-releases-header\",\n\t\t\t\t\t},\n\t\t\t\t\treact.createElement(\"h2\", null, date)\n\t\t\t\t),\n\t\t\t\treact.createElement(\n\t\t\t\t\t\"div\",\n\t\t\t\t\t{\n\t\t\t\t\t\tclassName: \"main-gridContainer-gridContainer \",\n\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\t\"--min-container-width\": \"180px\",\n\t\t\t\t\t\t\t\"--column-count\": \"auto-fill\",\n\t\t\t\t\t\t\t\"--grid-gap\": \"18px\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tseparatedByDate[date]\n\t\t\t\t\t\t.filter((card) => !dismissed.includes(card.props.uri))\n\t\t\t\t\t\t.map((card) => react.createElement(Card, { ...card.props, key: card.props.uri }))\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t\tthis.setState({ cards: [...gridList] });\n\t}\n\n\tremoveCards(id, type) {\n\t\tswitch (type) {\n\t\t\tcase \"reset\":\n\t\t\t\tSpicetify.showNotification(\"Reset dismissed releases\");\n\t\t\t\tdismissed = [];\n\t\t\t\tbreak;\n\t\t\tcase \"undo\":\n\t\t\t\tif (!dismissed[0]) Spicetify.showNotification(\"Nothing to undo\", true);\n\t\t\t\telse Spicetify.showNotification(\"Undone dismissal\");\n\t\t\t\tdismissed = id ? dismissed.filter((item) => item !== id) : dismissed.slice(0, -1);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tdismissed.push(id);\n\t\t\t\tbreak;\n\t\t}\n\t\tSpicetify.LocalStorage.set(\"new-releases:dismissed\", JSON.stringify(dismissed));\n\t\tthis.updatePostsVisual();\n\t}\n\n\tasync reload() {\n\t\tgridList = [];\n\t\tseparatedByDate = {};\n\t\tdateList = [];\n\n\t\ttoday = Date.now();\n\t\tCONFIG.range = Number.parseInt(CONFIG.range) || 30;\n\t\tlimitInMs = CONFIG.range * DAY_DIVIDER;\n\n\t\tthis.setState({ rest: false });\n\t\tlet items = [];\n\t\tif (CONFIG.music) {\n\t\t\tconst tracks = await fetchTracks();\n\t\t\titems.push(...tracks.flat());\n\t\t}\n\t\tif (CONFIG.podcast) {\n\t\t\tconst episodes = await fetchPodcasts();\n\t\t\titems.push(...episodes);\n\t\t}\n\n\t\titems = items.filter(Boolean).sort((a, b) => b.time - a.time);\n\n\t\tlet timeFormat;\n\t\tif (CONFIG.relative) {\n\t\t\ttimeFormat = new Intl.RelativeTimeFormat(CONFIG.locale, relativeDateFormat);\n\t\t} else {\n\t\t\ttimeFormat = new Intl.DateTimeFormat(CONFIG.locale, dateFormat);\n\t\t}\n\n\t\tfor (const track of items) {\n\t\t\ttrack.visual = CONFIG.visual;\n\t\t\tlet dateStr;\n\t\t\tif (CONFIG.relative) {\n\t\t\t\tconst days = Math.ceil((track.time - today) / DAY_DIVIDER);\n\t\t\t\tdateStr = timeFormat.format(days, \"day\");\n\t\t\t} else {\n\t\t\t\tdateStr = timeFormat.format(track.time);\n\t\t\t}\n\t\t\tif (!separatedByDate[dateStr]) {\n\t\t\t\tdateList.push(dateStr);\n\t\t\t\tseparatedByDate[dateStr] = [];\n\t\t\t}\n\t\t\tseparatedByDate[dateStr].push(react.createElement(Card, { ...track, key: track.uri }));\n\t\t}\n\n\t\tfor (const date of dateList) {\n\t\t\tif (separatedByDate[date].every((card) => dismissed.includes(card.props.uri))) continue;\n\n\t\t\tgridList.push(\n\t\t\t\treact.createElement(\n\t\t\t\t\t\"div\",\n\t\t\t\t\t{\n\t\t\t\t\t\tclassName: \"new-releases-header\",\n\t\t\t\t\t},\n\t\t\t\t\treact.createElement(\"h2\", null, date)\n\t\t\t\t),\n\t\t\t\treact.createElement(\n\t\t\t\t\t\"div\",\n\t\t\t\t\t{\n\t\t\t\t\t\tclassName: \"main-gridContainer-gridContainer\",\n\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\t\"--min-container-width\": \"180px\",\n\t\t\t\t\t\t\t\"--column-count\": \"auto-fill\",\n\t\t\t\t\t\t\t\"--grid-gap\": \"18px\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tseparatedByDate[date].filter((card) => !dismissed.includes(card.props.uri))\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\tthis.setState({ rest: true });\n\t}\n\n\tasync componentDidMount() {\n\t\tgridUpdatePostsVisual = this.updatePostsVisual.bind(this);\n\t\tremoveCards = this.removeCards.bind(this);\n\n\t\tthis.configButton = new Spicetify.Menu.Item(\n\t\t\t\"New Releases config\",\n\t\t\tfalse,\n\t\t\topenConfig,\n\t\t\t'<svg viewBox=\"0 0 256 256\" width=\"16\" height=\"16\" fill=\"currentColor\" style=\"stroke: currentcolor; stroke-width: 10px;\"><path d=\"M229.84861,182.11168q2.38762,2.80284 2.73874,6.3064t-0.98314,6.37647t-4.21345,4.83491t-6.53085,1.96199l-184.83001,0q-2.94942,0 -5.40726,-1.26128t-3.93255,-3.36341t-2.17695,-4.62469t0,-5.25533t2.52807,-4.97505l18.82008,-21.86218l0,-76.37749q0,-16.81706 6.53085,-32.02249t17.62627,-26.27666t26.33406,-17.58784t32.09245,-6.51661l2.52807,0q16.5729,0.56057 31.53065,7.70782t25.5616,18.77905t16.78358,27.18758t6.17973,32.2327l0,72.87393l18.82008,21.86218zm-193.81871,7.70782l184.83001,0l-21.62904,-25.22559l0,-77.21834q0,-14.71493 -5.40726,-28.16858t-14.60663,-23.40374t-21.90994,-16.04628t-26.61496,-6.51661l-0.63202,0l-0.56179,0l-0.49157,0l-0.56179,0q-14.32573,0 -27.45765,5.60569t-22.61218,15.06528t-15.0982,22.56289t-5.61793,27.3978l0,80.7219l-21.62904,25.22559zm116.01033,41.2018q0,9.66981 -6.95219,16.60685t-16.71335,6.93704t-16.64313,-6.93704t-6.88197,-16.60685l0,-5.88597l11.79766,0l0,5.88597q0,4.90498 3.44098,8.33846t8.35668,3.43348t8.35668,-3.43348t3.44098,-8.33846l0,-5.88597l11.79766,0l0,5.88597z\"/></svg>'\n\t\t);\n\t\tthis.configButton.register();\n\n\t\tconst viewPort = document.querySelector(this.viewportSelector);\n\n\t\tif (gridList.length) {\n\t\t\t// Already loaded\n\t\t\tif (lastScroll > 0) {\n\t\t\t\tviewPort.scrollTo(0, lastScroll);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tthis.reload();\n\t}\n\n\tcomponentWillUnmount() {\n\t\tconst viewPort = document.querySelector(this.viewportSelector);\n\t\tlastScroll = viewPort.scrollTop;\n\t\tthis.configButton.deregister();\n\t}\n\n\trender() {\n\t\tconst expFeatures = JSON.parse(localStorage.getItem(\"spicetify-exp-features\") || \"{}\");\n\t\tconst isGlobalNav = expFeatures?.enableGlobalNavBar?.value !== \"control\";\n\t\tconst version = Spicetify.Platform.version.split(\".\").map((i) => Number.parseInt(i));\n\n\t\tconst tabBarMargin = {\n\t\t\tmarginTop: isGlobalNav || (version[0] === 1 && version[1] === 2 && version[2] >= 45) ? \"60px\" : \"0px\",\n\t\t};\n\t\treturn react.createElement(\n\t\t\t\"section\",\n\t\t\t{\n\t\t\t\tclassName: \"contentSpacing\",\n\t\t\t},\n\t\t\treact.createElement(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tclassName: \"new-releases-header\",\n\t\t\t\t\tstyle: tabBarMargin,\n\t\t\t\t},\n\t\t\t\treact.createElement(\"h1\", null, Spicetify.Locale.get(\"new_releases\")),\n\t\t\t\treact.createElement(\n\t\t\t\t\t\"div\",\n\t\t\t\t\t{\n\t\t\t\t\t\tclassName: \"new-releases-controls-container\",\n\t\t\t\t\t},\n\t\t\t\t\treact.createElement(ButtonText, {\n\t\t\t\t\t\ttext: Spicetify.Locale.get(\"playlist.extender.refresh\"),\n\t\t\t\t\t\tonClick: this.reload.bind(this),\n\t\t\t\t\t}),\n\t\t\t\t\treact.createElement(ButtonText, {\n\t\t\t\t\t\ttext: \"undo\", // no locale for this\n\t\t\t\t\t\tonClick: this.removeCards.bind(this, null, \"undo\"),\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t\t),\n\t\t\tthis.state.rest ? gridList : LoadingIcon\n\t\t);\n\t}\n}\n\nasync function getArtistList() {\n\tconst config = {\n\t\tfilters: [\"1\"],\n\t\tsortOrder: [\"0\"],\n\t\ttextFilter: \"\",\n\t\toffset: 0,\n\t\tlimit: 50000,\n\t};\n\tconst artists = await Spicetify.Platform.LibraryAPI.getContents(config);\n\tcount(true);\n\treturn artists.items ?? [];\n}\n\nasync function getArtistEverything(artist) {\n\tconst { data, errors } = await Spicetify.GraphQL.Request(\n\t\t{\n\t\t\tname: \"queryArtistDiscographyAll\",\n\t\t\toperation: \"query\",\n\t\t\tsha256Hash: \"9380995a9d4663cbcb5113fef3c6aabf70ae6d407ba61793fd01e2a1dd6929b0\",\n\t\t\tvalue: null,\n\t\t},\n\t\t{\n\t\t\turi: artist.uri,\n\t\t\toffset: 0,\n\t\t\t// Limit 100 since GraphQL has resource limit\n\t\t\tlimit: 100,\n\t\t}\n\t);\n\tif (errors) throw errors;\n\n\tconst releases = data?.artistUnion.discography.all.items.flatMap((r) => r.releases.items);\n\tconst items = [];\n\tconst types = [\n\t\t[CONFIG.album, releases.filter((r) => r.type === \"ALBUM\"), Spicetify.Locale.get(\"album\")],\n\t\t// Appears on has a separate GraphQL query but does not provide enough information (release date), which requires recursively making requests for each album\n\t\t// [CONFIG[\"appears-on\"], releases.appears_on?.releases, Spicetify.Locale.get(\"artist.appears-on\")],\n\t\t[CONFIG.compilations, releases.filter((r) => r.type === \"COMPILATION\"), Spicetify.Locale.get(\"compilation\")],\n\t\t[\n\t\t\tCONFIG[\"single-ep\"],\n\t\t\treleases.filter((r) => r.type === \"SINGLE\" || r.type === \"EP\"),\n\t\t\t`${Spicetify.Locale.get(\"single\")}/${Spicetify.Locale.get(\"ep\")}`,\n\t\t],\n\t];\n\tfor (const type of types) {\n\t\tif (type[0] && type[1]) {\n\t\t\tfor (const item of type[1]) {\n\t\t\t\tconst meta = metaFromTrack(artist, item);\n\t\t\t\tif (!meta) continue;\n\t\t\t\tmeta.type = type[2];\n\t\t\t\titems.push(meta);\n\t\t\t}\n\t\t}\n\t}\n\treturn items;\n}\n\nasync function getPodcastList() {\n\tconst body = await Spicetify.Platform.LibraryAPI.getShows({ limit: 50000 });\n\treturn body.items ?? [];\n}\n\nasync function getPodcastRelease(uri) {\n\tconst body = await Spicetify.Platform.ShowAPI.getContents(uri, { limit: 50000 });\n\treturn body.items;\n}\n\nfunction metaFromTrack(artist, track) {\n\tconst time = Date.parse(track.date.isoString);\n\tif (today - time < limitInMs) {\n\t\treturn {\n\t\t\turi: track.uri,\n\t\t\ttitle: track.name,\n\t\t\tartist: {\n\t\t\t\tname: artist.name,\n\t\t\t\turi: artist.uri,\n\t\t\t},\n\t\t\timageURL: track.coverArt.sources.reduce((prev, curr) => (prev.width > curr.width ? prev : curr)).url,\n\t\t\ttime,\n\t\t\ttrackCount: track.tracks.totalCount,\n\t\t};\n\t}\n\treturn null;\n}\n\nconst count = (() => {\n\tlet counter = 0;\n\treturn (reset = false) => {\n\t\tif (reset) counter = 0;\n\t\telse counter++;\n\t};\n})();\n\nasync function fetchTracks() {\n\tconst artistList = await getArtistList();\n\tSpicetify.showNotification(`Fetching releases from ${artistList.length} artists`);\n\n\tconst requests = artistList.map(async (obj) => {\n\t\treturn await getArtistEverything(obj).catch((err) => {\n\t\t\tconsole.debug(\"Could not fetch all releases\", err);\n\t\t\tconsole.debug(`Missing releases from ${count()} artists`);\n\t\t});\n\t});\n\n\treturn await Promise.all(requests);\n}\n\nasync function fetchPodcasts() {\n\tconst items = [];\n\tconst itemTypeStr = Spicetify.Locale.get(\"card.tag.episode\");\n\tfor (const podcast of await getPodcastList()) {\n\t\tconst tracks = await getPodcastRelease(podcast.uri);\n\t\tif (!tracks) continue;\n\n\t\tfor (const track of tracks) {\n\t\t\tconst time = new Date(track.releaseDate.isoString);\n\n\t\t\tif (today - time.getTime() > limitInMs) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\titems.push({\n\t\t\t\turi: track.uri,\n\t\t\t\ttitle: track.name,\n\t\t\t\tartist: {\n\t\t\t\t\tname: podcast.name,\n\t\t\t\t\turi: podcast.uri,\n\t\t\t\t},\n\t\t\t\timageURL: track.coverArt.reduce((prev, curr) => (prev.width > curr.width ? prev : curr)).url,\n\t\t\t\ttime,\n\t\t\t\ttype: itemTypeStr,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn items;\n}\n"
  },
  {
    "path": "CustomApps/new-releases/manifest.json",
    "content": "{\n\t\"name\": {\n\t\t\"ms\": \"Keluaran Baharu\",\n\t\t\"gu\": \"નવા રિલીઝ\",\n\t\t\"ko\": \"최신 음악\",\n\t\t\"pa-IN\": \"ਨਵੇਂ ਰਿਲੀਜ਼\",\n\t\t\"az\": \"Yeni buraxılışlar\",\n\t\t\"ru\": \"Новые релизы\",\n\t\t\"uk\": \"Нові релізи\",\n\t\t\"nb\": \"Nye utgivelser\",\n\t\t\"sv\": \"Nya releaser\",\n\t\t\"sw\": \"Matoleo Mapya\",\n\t\t\"ur\": \"نئی ریلیزز\",\n\t\t\"bho\": \"नवका रिलीज़\",\n\t\t\"pa-PK\": \"نویں ریلیزاں\",\n\t\t\"te\": \"క్రొత్త రిలీజ్‌లు\",\n\t\t\"ro\": \"Lansări noi\",\n\t\t\"vi\": \"Mới Phát Hành\",\n\t\t\"am\": \"አዳዲስ የተለቀቁ\",\n\t\t\"bn\": \"নতুন রিলিজ\",\n\t\t\"en\": \"New Releases\",\n\t\t\"id\": \"Rilis Terbaru\",\n\t\t\"bg\": \"Нови издания\",\n\t\t\"da\": \"Nye udgivelser\",\n\t\t\"es-419\": \"Nuevos Lanzamientos\",\n\t\t\"mr\": \"नवीन रिलीझ\",\n\t\t\"ml\": \"പുതിയ റിലീസുകള്‍\",\n\t\t\"th\": \"ออกใหม่ล่าสุด\",\n\t\t\"tr\": \"Yeni Çıkanlar\",\n\t\t\"is\": \"Nýjar útgáfur\",\n\t\t\"fa\": \"تولیدات جدید\",\n\t\t\"or\": \"ନୂଆ ରିଲିଜଗୁଡ଼ିକ\",\n\t\t\"he\": \"מה חדש?\",\n\t\t\"hi\": \"नई रिलीज़\",\n\t\t\"zh-TW\": \"最新發行\",\n\t\t\"sr\": \"Nova izdanja\",\n\t\t\"pt-BR\": \"Novos lançamentos\",\n\t\t\"zu\": \"Ezisanda Kukhishwa\",\n\t\t\"nl\": \"Nieuwe releases\",\n\t\t\"es\": \"Novedades\",\n\t\t\"lt\": \"Nauji leidimai\",\n\t\t\"ja\": \"ニューリリース\",\n\t\t\"st\": \"Nova izdanja\",\n\t\t\"it\": \"Nuove uscite\",\n\t\t\"el\": \"Νέες κυκλοφορίες\",\n\t\t\"pt-PT\": \"Novos lançamentos\",\n\t\t\"kn\": \"ಹೊಸ ಬಿಡುಗಡೆಗಳು\",\n\t\t\"de\": \"Neuerscheinungen\",\n\t\t\"fr\": \"Nouveautés\",\n\t\t\"ne\": \"नयाँ रिलिजहरू\",\n\t\t\"ar\": \"الإصدارات الجديدة\",\n\t\t\"af\": \"Nuwe vrystellings\",\n\t\t\"et\": \"Uus muusika\",\n\t\t\"pl\": \"Nowe wydania\",\n\t\t\"ta\": \"புதிய வெளியீடுகள்\",\n\t\t\"sl\": \"Nove izdaje\",\n\t\t\"pk\": \"New Releases\",\n\t\t\"hr\": \"Nova izdanja\",\n\t\t\"sk\": \"Novinky\",\n\t\t\"fi\": \"Uudet julkaisut\",\n\t\t\"lv\": \"Jaunumi\",\n\t\t\"fil\": \"Mga Bagong Release\",\n\t\t\"fr-CA\": \"Nouveautés\",\n\t\t\"cs\": \"Čerstvé novinky\",\n\t\t\"zh-CN\": \"新歌热播\",\n\t\t\"hu\": \"Újdonságok\"\n\t},\n\t\"icon\": \"<svg viewBox=\\\"0 0 256 256\\\" fill=\\\"currentColor\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"1\\\"><path d=\\\"M229.84861,182.11168q2.38762,2.80284 2.73874,6.3064t-0.98314,6.37647t-4.21345,4.83491t-6.53085,1.96199l-184.83001,0q-2.94942,0 -5.40726,-1.26128t-3.93255,-3.36341t-2.17695,-4.62469t0,-5.25533t2.52807,-4.97505l18.82008,-21.86218l0,-76.37749q0,-16.81706 6.53085,-32.02249t17.62627,-26.27666t26.33406,-17.58784t32.09245,-6.51661l2.52807,0q16.5729,0.56057 31.53065,7.70782t25.5616,18.77905t16.78358,27.18758t6.17973,32.2327l0,72.87393l18.82008,21.86218zm-193.81871,7.70782l184.83001,0l-21.62904,-25.22559l0,-77.21834q0,-14.71493 -5.40726,-28.16858t-14.60663,-23.40374t-21.90994,-16.04628t-26.61496,-6.51661l-0.63202,0l-0.56179,0l-0.49157,0l-0.56179,0q-14.32573,0 -27.45765,5.60569t-22.61218,15.06528t-15.0982,22.56289t-5.61793,27.3978l0,80.7219l-21.62904,25.22559zm116.01033,41.2018q0,9.66981 -6.95219,16.60685t-16.71335,6.93704t-16.64313,-6.93704t-6.88197,-16.60685l0,-5.88597l11.79766,0l0,5.88597q0,4.90498 3.44098,8.33846t8.35668,3.43348t8.35668,-3.43348t3.44098,-8.33846l0,-5.88597l11.79766,0l0,5.88597z\\\"/></svg>\",\n\t\"active-icon\": \"<svg viewBox=\\\"0 0 256 256\\\" fill=\\\"currentColor\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"1\\\"><path d=\\\"M229.849 182.112 C 231.44 183.98 232.353 186.082 232.587 188.418 C 232.821 190.754 232.494 192.879 231.604 194.795 C 230.715 196.71 229.31 198.321 227.391 199.629 C 225.471 200.937 223.294 201.591 220.86 201.591 L 36.03 201.591 C 34.064 201.591 32.261 201.171 30.623 200.33 C 28.984 199.489 27.673 198.368 26.69 196.967 C 25.707 195.565 24.981 194.024 24.513 192.342 C 24.045 190.66 24.045 188.909 24.513 187.087 C 24.981 185.265 25.824 183.607 27.041 182.112 L 45.861 160.25 L 45.861 83.872 C 45.861 72.661 48.038 61.986 52.392 51.85 C 56.746 41.713 62.621 32.954 70.018 25.573 C 77.415 18.192 86.193 12.329 96.352 7.985 C 106.512 3.641 117.209 1.468 128.445 1.468 L 130.973 1.468 C 142.022 1.842 152.532 4.411 162.504 9.176 C 172.475 13.941 180.996 20.201 188.065 27.955 C 195.134 35.71 200.729 44.772 204.849 55.143 C 208.969 65.513 211.029 76.258 211.029 87.376 L 211.029 160.25 L 229.849 182.112 Z M 152.04 231.021 C 152.04 237.468 149.723 243.003 145.088 247.628 C 140.453 252.253 134.882 254.565 128.375 254.565 C 121.867 254.565 116.32 252.253 111.732 247.628 C 107.144 243.003 104.85 237.468 104.85 231.021 L 104.85 225.135 L 116.647 225.135 L 116.647 231.021 C 116.647 234.291 117.794 237.071 120.088 239.36 C 122.382 241.649 125.168 242.793 128.445 242.793 C 131.722 242.793 134.508 241.649 136.802 239.36 C 139.096 237.071 140.243 234.291 140.243 231.021 L 140.243 225.135 L 152.04 225.135 L 152.04 231.021 z\\\"/></svg>\",\n\t\"subfiles\": [\"Card.js\", \"Icons.js\", \"Settings.js\"]\n}\n"
  },
  {
    "path": "CustomApps/new-releases/style.css",
    "content": ".setting-row::after {\n\tcontent: \"\";\n\tdisplay: table;\n\tclear: both;\n}\n.setting-row .col {\n\tdisplay: flex;\n\tpadding: 10px 0;\n\talign-items: center;\n}\n.setting-row .col.description {\n\tfloat: left;\n\tpadding-right: 15px;\n\tcursor: default;\n}\n.setting-row .col.action {\n\tfloat: right;\n\ttext-align: right;\n}\nbutton.switch {\n\talign-items: center;\n\tborder: 0px;\n\tborder-radius: 50%;\n\tbackground-color: rgba(var(--spice-rgb-shadow), 0.7);\n\tcolor: var(--spice-text);\n\tcursor: pointer;\n\tdisplay: flex;\n\tmargin-inline-start: 12px;\n\tpadding: 8px;\n}\nbutton.switch.disabled,\nbutton.switch[disabled] {\n\tcolor: rgba(var(--spice-rgb-text), 0.3);\n}\nbutton.switch.small {\n\twidth: 22px;\n\theight: 22px;\n\tpadding: 6px;\n}\nbutton.text {\n\tfont-size: 12px;\n\tline-height: 16px;\n\tfont-weight: 700;\n\tletter-spacing: 0.1em;\n\ttext-transform: uppercase;\n\ttext-align: center;\n\tcolor: var(--spice-text);\n\tbackground-color: initial;\n\tpadding: 7px 15px;\n\tborder: 1px solid var(--spice-text);\n\t-webkit-box-sizing: border-box;\n\tbox-sizing: border-box;\n\tborder-radius: 4px;\n\tmargin-inline-start: 12px;\n}\n#new-releases-config-container input {\n\twidth: 100%;\n\tmargin-top: 10px;\n\tpadding: 0 5px;\n\theight: 32px;\n\tborder: 0;\n\tcolor: var(--spice-text);\n\tbackground-color: initial;\n\tborder-bottom: 1px solid var(--spice-text);\n}\n\noption {\n\tbackground-color: var(--spice-button);\n}\n\n.new-releases-header {\n\t-webkit-box-pack: justify;\n\t-webkit-box-align: center;\n\talign-content: space-between;\n\talign-items: center;\n\tcolor: var(--spice-text);\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tmargin: 16px 0;\n\ttext-transform: capitalize;\n}\n\n.new-releases-controls-container {\n\tposition: relative;\n\talign-items: center;\n\tdisplay: flex;\n}\n\n.new-releases-cardSubHeader {\n\tcolor: var(--spice-subtext);\n}\n.new-releases-header + .main-gridContainer-gridContainer .main-card-card .main-playButton-PlayButton button {\n\t-webkit-tap-highlight-color: transparent;\n\tbackground-color: transparent;\n\tborder: 0px;\n\tborder-radius: 500px;\n\tdisplay: inline-block;\n\tposition: relative;\n\ttouch-action: manipulation;\n\ttransition-duration: 33ms;\n\ttransition-property: background-color, border-color, color, box-shadow, filter, transform;\n\tuser-select: none;\n\tvertical-align: middle;\n\ttransform: translate3d(0px, 0px, 0px);\n\tpadding: 0px;\n\tmin-inline-size: 0px;\n\talign-self: center;\n}\n.new-releases-header + .main-gridContainer-gridContainer .main-card-card .main-playButton-PlayButton span {\n\t-webkit-tap-highlight-color: transparent;\n\tposition: relative;\n\tbackground-color: var(--spice-button-active);\n\tcolor: var(--spice-sidebar);\n\tdisplay: flex;\n\tborder-radius: 500px;\n\tfont-size: inherit;\n\tmin-block-size: 48px;\n\t-webkit-box-align: center;\n\talign-items: center;\n\t-webkit-box-pack: center;\n\tjustify-content: center;\n\tinline-size: 48px;\n\tblock-size: 48px;\n}\n.new-releases-header + .main-gridContainer-gridContainer .main-card-card .main-playButton-PlayButton span:hover {\n\ttransform: scale(1.04);\n}\n\n.main-card-closeButton {\n\tpointer-events: all;\n\tposition: absolute !important;\n\ttop: 8px;\n\tright: 8px;\n\t-webkit-box-align: center;\n\t-ms-flex-align: center;\n\t-webkit-box-pack: center;\n\t-ms-flex-pack: center;\n\talign-items: center;\n\tbackground-color: rgba(var(--spice-rgb-shadow), 0.7);\n\tborder: 0;\n\tborder-radius: 500px;\n\tcolor: var(--spice-sidebar);\n\tdisplay: flex;\n\theight: 28px;\n\tjustify-content: center;\n\t-webkit-transform: scale(1);\n\ttransform: scale(1);\n\t-webkit-transform-origin: center;\n\ttransform-origin: center;\n\twidth: 28px;\n\tvisibility: hidden;\n\topacity: 0;\n\ttransition:\n\t\tvisibility 0s,\n\t\topacity 0.3s ease;\n}\n\n.main-card-closeButton:active {\n\ttransform: scale(1) !important;\n}\n.main-card-closeButton:hover {\n\ttransform: scale(1.1);\n}\n\n.main-card-card:hover .main-card-closeButton {\n\tvisibility: visible;\n\topacity: 1;\n\ttransition:\n\t\tvisibility 0s,\n\t\topacity 0.3s ease;\n}\n\n.new-releases-header + .main-gridContainer-gridContainer {\n\tgrid-template-columns: repeat(var(--column-count), minmax(var(--min-container-width), 1fr)) !important;\n}\n"
  },
  {
    "path": "CustomApps/reddit/Card.js",
    "content": "class Card extends react.Component {\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tObject.assign(this, props);\n\t\tconst uriObj = URI.fromString(this.uri);\n\t\tthis.href = uriObj.toURLPath(true);\n\n\t\tthis.uriType = uriObj.type;\n\t\tswitch (this.uriType) {\n\t\t\tcase URI.Type.ALBUM:\n\t\t\tcase URI.Type.TRACK:\n\t\t\t\tthis.menuType = Spicetify.ReactComponent.AlbumMenu;\n\t\t\t\tbreak;\n\t\t\tcase URI.Type.ARTIST:\n\t\t\t\tthis.menuType = Spicetify.ReactComponent.ArtistMenu;\n\t\t\t\tbreak;\n\t\t\tcase URI.Type.PLAYLIST:\n\t\t\tcase URI.Type.PLAYLIST_V2:\n\t\t\t\tthis.menuType = Spicetify.ReactComponent.PlaylistMenu;\n\t\t\t\tbreak;\n\t\t\tcase URI.Type.SHOW:\n\t\t\t\tthis.menuType = Spicetify.ReactComponent.PodcastShowMenu;\n\t\t\t\tbreak;\n\t\t}\n\t\tthis.menuType = this.menuType || \"div\";\n\t}\n\n\tplay(event) {\n\t\tSpicetify.Player.playUri(this.uri, this.context);\n\t\tevent.stopPropagation();\n\t}\n\n\tgetSubtitle() {\n\t\tlet subtitle;\n\t\tif ((this.uriType === URI.Type.ALBUM || this.uriType === URI.Type.TRACK) && Array.isArray(this.subtitle)) {\n\t\t\tsubtitle = this.subtitle.map((artist) => {\n\t\t\t\tconst artistHref = URI.fromString(artist.uri).toURLPath(true);\n\t\t\t\treturn react.createElement(\n\t\t\t\t\t\"a\",\n\t\t\t\t\t{\n\t\t\t\t\t\thref: artistHref,\n\t\t\t\t\t\tonClick: (event) => {\n\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t\t\t\tHistory.push(artistHref);\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\treact.createElement(\"span\", null, artist.name)\n\t\t\t\t);\n\t\t\t});\n\t\t\t// Insert commas between elements\n\t\t\tsubtitle = subtitle.flatMap((el, i, arr) => (arr.length - 1 !== i ? [el, \", \"] : el));\n\t\t} else {\n\t\t\tsubtitle = react.createElement(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tclassName: `${this.visual.longDescription ? \"reddit-longDescription \" : \"\"}main-cardSubHeader-root main-type-mesto reddit-cardSubHeader`,\n\t\t\t\t\tas: \"div\",\n\t\t\t\t},\n\t\t\t\treact.createElement(\"span\", null, this.subtitle)\n\t\t\t);\n\t\t}\n\t\treturn react.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tclassName: \"reddit-cardSubHeader main-type-mesto\",\n\t\t\t},\n\t\t\tsubtitle\n\t\t);\n\t}\n\n\tgetFollowers() {\n\t\tif (this.visual.followers && (this.uriType === URI.Type.PLAYLIST || this.uriType === URI.Type.PLAYLIST_V2)) {\n\t\t\treturn react.createElement(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tclassName: \"main-cardSubHeader-root main-type-mestoBold reddit-cardSubHeader\",\n\t\t\t\t\tas: \"div\",\n\t\t\t\t},\n\t\t\t\treact.createElement(\"span\", null, Spicetify.Locale.get(\"user.followers\", this.followersCount))\n\t\t\t);\n\t\t}\n\t}\n\n\trender() {\n\t\tconst detail = [];\n\t\tthis.visual.type && detail.push(this.type);\n\t\tthis.visual.upvotes && detail.push(`▲ ${this.upvotes}`);\n\n\t\treturn react.createElement(\n\t\t\tSpicetify.ReactComponent.RightClickMenu || \"div\",\n\t\t\t{\n\t\t\t\tmenu: react.createElement(this.menuType, { uri: this.uri }),\n\t\t\t},\n\t\t\treact.createElement(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tclassName: \"main-card-card\",\n\t\t\t\t\tonClick: (event) => {\n\t\t\t\t\t\tHistory.push(this.href);\n\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\treact.createElement(\n\t\t\t\t\t\"div\",\n\t\t\t\t\t{\n\t\t\t\t\t\tclassName: \"main-card-draggable\",\n\t\t\t\t\t\tdraggable: \"true\",\n\t\t\t\t\t},\n\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tclassName: \"main-card-imageContainer\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tclassName: \"main-cardImage-imageWrapper\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t\t\t{},\n\t\t\t\t\t\t\t\treact.createElement(\"img\", {\n\t\t\t\t\t\t\t\t\t\"aria-hidden\": \"false\",\n\t\t\t\t\t\t\t\t\tdraggable: \"false\",\n\t\t\t\t\t\t\t\t\tloading: \"lazy\",\n\t\t\t\t\t\t\t\t\tsrc: this.imageURL,\n\t\t\t\t\t\t\t\t\tclassName: \"main-image-image main-cardImage-image\",\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t),\n\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tclassName: \"main-card-PlayButtonContainer\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tclassName: \"main-playButton-PlayButton main-playButton-primary\",\n\t\t\t\t\t\t\t\t\t\"aria-label\": Spicetify.Locale.get(\"play\"),\n\t\t\t\t\t\t\t\t\tstyle: { \"--size\": \"40px\" },\n\t\t\t\t\t\t\t\t\tonClick: this.play.bind(this),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\t\"button\",\n\t\t\t\t\t\t\t\t\tnull,\n\t\t\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\t\t\"span\",\n\t\t\t\t\t\t\t\t\t\tnull,\n\t\t\t\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\t\t\t\"svg\",\n\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\theight: \"24\",\n\t\t\t\t\t\t\t\t\t\t\t\trole: \"img\",\n\t\t\t\t\t\t\t\t\t\t\t\twidth: \"24\",\n\t\t\t\t\t\t\t\t\t\t\t\tviewBox: \"0 0 24 24\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"aria-hidden\": \"true\",\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\treact.createElement(\"polygon\", {\n\t\t\t\t\t\t\t\t\t\t\t\tpoints: \"21.57 12 5.98 3 5.98 21 21.57 12\",\n\t\t\t\t\t\t\t\t\t\t\t\tfill: \"currentColor\",\n\t\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t)\n\t\t\t\t\t),\n\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tclassName: \"main-card-cardMetadata\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\"a\",\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tdraggable: \"false\",\n\t\t\t\t\t\t\t\ttitle: this.title,\n\t\t\t\t\t\t\t\tclassName: \"main-cardHeader-link\",\n\t\t\t\t\t\t\t\tdir: \"auto\",\n\t\t\t\t\t\t\t\thref: this.href,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tclassName: \"main-cardHeader-text main-type-balladBold\",\n\t\t\t\t\t\t\t\t\tas: \"div\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tthis.title\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t),\n\t\t\t\t\t\tdetail.length > 0 &&\n\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tclassName: \"main-cardSubHeader-root main-type-mestoBold reddit-cardSubHeader\",\n\t\t\t\t\t\t\t\t\tas: \"div\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\treact.createElement(\"span\", null, detail.join(\" ‒ \"))\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\tthis.getFollowers(),\n\t\t\t\t\t\tthis.getSubtitle()\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "CustomApps/reddit/Icons.js",
    "content": "class LoadingIcon extends react.Component {\n\trender() {\n\t\treturn react.createElement(\n\t\t\t\"svg\",\n\t\t\t{\n\t\t\t\twidth: \"100px\",\n\t\t\t\theight: \"100px\",\n\t\t\t\tviewBox: \"0 0 100 100\",\n\t\t\t\tpreserveAspectRatio: \"xMidYMid\",\n\t\t\t},\n\t\t\treact.createElement(\n\t\t\t\t\"circle\",\n\t\t\t\t{\n\t\t\t\t\tcx: \"50\",\n\t\t\t\t\tcy: \"50\",\n\t\t\t\t\tr: \"0\",\n\t\t\t\t\tfill: \"none\",\n\t\t\t\t\tstroke: \"currentColor\",\n\t\t\t\t\t\"stroke-width\": \"2\",\n\t\t\t\t},\n\t\t\t\treact.createElement(\"animate\", {\n\t\t\t\t\tattributeName: \"r\",\n\t\t\t\t\trepeatCount: \"indefinite\",\n\t\t\t\t\tdur: \"1s\",\n\t\t\t\t\tvalues: \"0;40\",\n\t\t\t\t\tkeyTimes: \"0;1\",\n\t\t\t\t\tkeySplines: \"0 0.2 0.8 1\",\n\t\t\t\t\tcalcMode: \"spline\",\n\t\t\t\t\tbegin: \"0s\",\n\t\t\t\t}),\n\t\t\t\treact.createElement(\"animate\", {\n\t\t\t\t\tattributeName: \"opacity\",\n\t\t\t\t\trepeatCount: \"indefinite\",\n\t\t\t\t\tdur: \"1s\",\n\t\t\t\t\tvalues: \"1;0\",\n\t\t\t\t\tkeyTimes: \"0;1\",\n\t\t\t\t\tkeySplines: \"0.2 0 0.8 1\",\n\t\t\t\t\tcalcMode: \"spline\",\n\t\t\t\t\tbegin: \"0s\",\n\t\t\t\t})\n\t\t\t),\n\t\t\treact.createElement(\n\t\t\t\t\"circle\",\n\t\t\t\t{\n\t\t\t\t\tcx: \"50\",\n\t\t\t\t\tcy: \"50\",\n\t\t\t\t\tr: \"0\",\n\t\t\t\t\tfill: \"none\",\n\t\t\t\t\tstroke: \"currentColor\",\n\t\t\t\t\t\"stroke-width\": \"2\",\n\t\t\t\t},\n\t\t\t\treact.createElement(\"animate\", {\n\t\t\t\t\tattributeName: \"r\",\n\t\t\t\t\trepeatCount: \"indefinite\",\n\t\t\t\t\tdur: \"1s\",\n\t\t\t\t\tvalues: \"0;40\",\n\t\t\t\t\tkeyTimes: \"0;1\",\n\t\t\t\t\tkeySplines: \"0 0.2 0.8 1\",\n\t\t\t\t\tcalcMode: \"spline\",\n\t\t\t\t\tbegin: \"-0.5s\",\n\t\t\t\t}),\n\t\t\t\treact.createElement(\"animate\", {\n\t\t\t\t\tattributeName: \"opacity\",\n\t\t\t\t\trepeatCount: \"indefinite\",\n\t\t\t\t\tdur: \"1s\",\n\t\t\t\t\tvalues: \"1;0\",\n\t\t\t\t\tkeyTimes: \"0;1\",\n\t\t\t\t\tkeySplines: \"0.2 0 0.8 1\",\n\t\t\t\t\tcalcMode: \"spline\",\n\t\t\t\t\tbegin: \"-0.5s\",\n\t\t\t\t})\n\t\t\t)\n\t\t);\n\t}\n}\n\nclass LoadMoreIcon extends react.Component {\n\trender() {\n\t\treturn react.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tonClick: this.props.onClick,\n\t\t\t},\n\t\t\treact.createElement(\n\t\t\t\t\"p\",\n\t\t\t\t{\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\tfontSize: 100,\n\t\t\t\t\t\tlineHeight: \"65px\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"»\"\n\t\t\t),\n\t\t\treact.createElement(\n\t\t\t\t\"span\",\n\t\t\t\t{\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\tfontSize: 20,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t\"Load more\"\n\t\t\t)\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "CustomApps/reddit/OptionsMenu.js",
    "content": "const OptionsMenuItemIcon = react.createElement(\n\t\"svg\",\n\t{\n\t\twidth: 16,\n\t\theight: 16,\n\t\tviewBox: \"0 0 16 16\",\n\t\tfill: \"currentColor\",\n\t},\n\treact.createElement(\"path\", {\n\t\td: \"M13.985 2.383L5.127 12.754 1.388 8.375l-.658.77 4.397 5.149 9.618-11.262z\",\n\t})\n);\n\nconst OptionsMenuItem = react.memo(({ onSelect, value, isSelected }) => {\n\treturn react.createElement(\n\t\tSpicetify.ReactComponent.MenuItem,\n\t\t{\n\t\t\tonClick: onSelect,\n\t\t\ticon: isSelected ? OptionsMenuItemIcon : null,\n\t\t\ttrailingIcon: isSelected ? OptionsMenuItemIcon : null,\n\t\t},\n\t\tvalue\n\t);\n});\n\nconst OptionsMenu = react.memo(({ options, onSelect, selected, defaultValue, bold = false }) => {\n\t/**\n\t * <Spicetify.ReactComponent.ContextMenu\n\t *      menu = { options.map(a => <OptionsMenuItem>) }\n\t * >\n\t *      <button>\n\t *          <span> {select.value} </span>\n\t *          <svg> arrow icon </svg>\n\t *      </button>\n\t * </Spicetify.ReactComponent.ContextMenu>\n\t */\n\tconst menuRef = react.useRef(null);\n\treturn react.createElement(\n\t\tSpicetify.ReactComponent.ContextMenu,\n\t\t{\n\t\t\tmenu: react.createElement(\n\t\t\t\tSpicetify.ReactComponent.Menu,\n\t\t\t\t{},\n\t\t\t\toptions.map(({ key, value }) =>\n\t\t\t\t\treact.createElement(OptionsMenuItem, {\n\t\t\t\t\t\tvalue,\n\t\t\t\t\t\tonSelect: () => {\n\t\t\t\t\t\t\tonSelect(key);\n\t\t\t\t\t\t\t// Close menu on item click\n\t\t\t\t\t\t\tmenuRef.current?.click();\n\t\t\t\t\t\t},\n\t\t\t\t\t\tisSelected: selected?.key === key,\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t\t),\n\t\t\ttrigger: \"click\",\n\t\t\taction: \"toggle\",\n\t\t\trenderInline: false,\n\t\t},\n\t\treact.createElement(\n\t\t\t\"button\",\n\t\t\t{\n\t\t\t\tclassName: \"optionsMenu-dropBox\",\n\t\t\t\tref: menuRef,\n\t\t\t},\n\t\t\treact.createElement(\n\t\t\t\t\"span\",\n\t\t\t\t{\n\t\t\t\t\tclassName: bold ? \"main-type-mestoBold\" : \"main-type-mesto\",\n\t\t\t\t},\n\t\t\t\tselected?.value || defaultValue\n\t\t\t),\n\t\t\treact.createElement(\n\t\t\t\t\"svg\",\n\t\t\t\t{\n\t\t\t\t\theight: \"16\",\n\t\t\t\t\twidth: \"16\",\n\t\t\t\t\tfill: \"currentColor\",\n\t\t\t\t\tviewBox: \"0 0 16 16\",\n\t\t\t\t},\n\t\t\t\treact.createElement(\"path\", {\n\t\t\t\t\td: \"M3 6l5 5.794L13 6z\",\n\t\t\t\t})\n\t\t\t)\n\t\t)\n\t);\n});\n"
  },
  {
    "path": "CustomApps/reddit/Settings.js",
    "content": "let configContainer;\n\nfunction openConfig() {\n\tif (configContainer) {\n\t\tSpicetify.PopupModal.display({\n\t\t\ttitle: \"Reddit\",\n\t\t\tcontent: configContainer,\n\t\t});\n\t\treturn;\n\t}\n\n\tCONFIG.servicesElement = {};\n\n\tconfigContainer = document.createElement(\"div\");\n\tconfigContainer.id = \"reddit-config-container\";\n\n\tconst optionHeader = document.createElement(\"h2\");\n\toptionHeader.innerText = \"Options\";\n\n\tconst serviceHeader = document.createElement(\"h2\");\n\tserviceHeader.innerText = \"Subreddits\";\n\n\tconst serviceContainer = document.createElement(\"div\");\n\n\tfunction stackServiceElements() {\n\t\tCONFIG.services.forEach((name, index) => {\n\t\t\tconst el = CONFIG.servicesElement[name];\n\n\t\t\tconst [up, down] = el.querySelectorAll(\"button\");\n\t\t\tif (CONFIG.services.length === 1) {\n\t\t\t\tup.disabled = true;\n\t\t\t\tdown.disabled = true;\n\t\t\t} else if (index === 0) {\n\t\t\t\tup.disabled = true;\n\t\t\t\tdown.disabled = false;\n\t\t\t} else if (index === CONFIG.services.length - 1) {\n\t\t\t\tup.disabled = false;\n\t\t\t\tdown.disabled = true;\n\t\t\t} else {\n\t\t\t\tup.disabled = false;\n\t\t\t\tdown.disabled = false;\n\t\t\t}\n\n\t\t\tserviceContainer.append(el);\n\t\t});\n\t\tgridUpdateTabs?.();\n\t}\n\n\tfunction posCallback(el, dir) {\n\t\tconst id = el.dataset.id;\n\t\tconst curPos = CONFIG.services.findIndex((val) => val === id);\n\t\tconst newPos = curPos + dir;\n\n\t\tif (CONFIG.services.length > 1) {\n\t\t\tconst temp = CONFIG.services[newPos];\n\t\t\tCONFIG.services[newPos] = CONFIG.services[curPos];\n\t\t\tCONFIG.services[curPos] = temp;\n\t\t}\n\n\t\tlocalStorage.setItem(\"reddit:services\", JSON.stringify(CONFIG.services));\n\n\t\tstackServiceElements();\n\t}\n\n\tfunction removeCallback(el) {\n\t\tconst id = el.dataset.id;\n\t\tCONFIG.services = CONFIG.services.filter((s) => s !== id);\n\t\tCONFIG.servicesElement[id].remove();\n\n\t\tlocalStorage.setItem(\"reddit:services\", JSON.stringify(CONFIG.services));\n\n\t\tstackServiceElements();\n\t}\n\n\tfor (const name of CONFIG.services) {\n\t\tCONFIG.servicesElement[name] = createServiceOption(name, posCallback, removeCallback);\n\t}\n\tstackServiceElements();\n\n\tconst serviceInput = document.createElement(\"input\");\n\tserviceInput.placeholder = \"Add new subreddit\";\n\tserviceInput.onkeydown = (event) => {\n\t\tif (event.key !== \"Enter\") {\n\t\t\treturn;\n\t\t}\n\t\tevent.preventDefault();\n\t\tconst name = serviceInput.value;\n\n\t\tif (!CONFIG.services.includes(name)) {\n\t\t\tCONFIG.services.push(name);\n\t\t\tCONFIG.servicesElement[name] = createServiceOption(name, posCallback, removeCallback);\n\t\t\tlocalStorage.setItem(\"reddit:services\", JSON.stringify(CONFIG.services));\n\t\t}\n\n\t\tstackServiceElements();\n\t\tserviceInput.value = \"\";\n\t\tconst parent = configContainer.parentElement.parentElement;\n\t\tparent.scrollTo(0, parent.scrollHeight);\n\t};\n\n\tconfigContainer.append(\n\t\toptionHeader,\n\t\tcreateSlider(\"Upvotes count\", \"upvotes\"),\n\t\tcreateSlider(\"Followers count\", \"followers\"),\n\t\tcreateSlider(\"Post type\", \"type\"),\n\t\tcreateSlider(\"Long description\", \"longDescription\"),\n\t\tserviceHeader,\n\t\tserviceContainer,\n\t\tserviceInput\n\t);\n\n\tSpicetify.PopupModal.display({\n\t\ttitle: \"Reddit\",\n\t\tcontent: configContainer,\n\t});\n}\n\nfunction createSlider(name, key) {\n\tconst container = document.createElement(\"div\");\n\tcontainer.innerHTML = `\n<div class=\"setting-row\">\n    <label class=\"col description\">${name}</label>\n    <div class=\"col action\"><button class=\"switch\">\n        <svg height=\"16\" width=\"16\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n            ${Spicetify.SVGIcons.check}\n        </svg>\n    </button></div>\n</div>`;\n\n\tconst slider = container.querySelector(\"button\");\n\tslider.classList.toggle(\"disabled\", !CONFIG.visual[key]);\n\n\tslider.onclick = () => {\n\t\tconst state = !slider.classList.toggle(\"disabled\");\n\t\tCONFIG.visual[key] = state;\n\t\tlocalStorage.setItem(`reddit:${key}`, String(state));\n\t\tgridUpdatePostsVisual?.();\n\t};\n\n\treturn container;\n}\n\nfunction createServiceOption(id, posCallback, removeCallback) {\n\tconst container = document.createElement(\"div\");\n\tcontainer.dataset.id = id;\n\tcontainer.innerHTML = `\n<div class=\"setting-row\">\n    <h3 class=\"col description\">${id}</h3>\n    <div class=\"col action\">\n        <button class=\"switch small\">\n            <svg height=\"10\" width=\"10\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n                ${Spicetify.SVGIcons[\"chart-up\"]}\n            </svg>\n        </button>\n        <button class=\"switch small\">\n            <svg height=\"10\" width=\"10\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n                ${Spicetify.SVGIcons[\"chart-down\"]}\n            </svg>\n        </button>\n        <button class=\"switch small\">\n            <svg height=\"10\" width=\"10\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n                ${Spicetify.SVGIcons.x}\n            </svg>\n        </button>\n    </div>\n</div>`;\n\n\tconst [up, down, remove] = container.querySelectorAll(\"button\");\n\n\tup.onclick = () => posCallback(container, -1);\n\tdown.onclick = () => posCallback(container, 1);\n\tremove.onclick = () => removeCallback(container);\n\n\treturn container;\n}\n"
  },
  {
    "path": "CustomApps/reddit/SortBox.js",
    "content": "class SortBox extends react.Component {\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tthis.sortByOptions = [\n\t\t\t{ key: \"hot\", value: \"Hot\" },\n\t\t\t{ key: \"new\", value: \"New\" },\n\t\t\t{ key: \"top\", value: \"Top\" },\n\t\t\t{ key: \"rising\", value: \"Rising\" },\n\t\t\t{ key: \"controversial\", value: \"Controversial\" },\n\t\t];\n\t\tthis.sortTimeOptions = [\n\t\t\t{ key: \"hour\", value: \"Hour\" },\n\t\t\t{ key: \"day\", value: \"Day\" },\n\t\t\t{ key: \"week\", value: \"Week\" },\n\t\t\t{ key: \"month\", value: \"Month\" },\n\t\t\t{ key: \"year\", value: \"Year\" },\n\t\t\t{ key: \"all\", value: \"All\" },\n\t\t];\n\t}\n\n\trender() {\n\t\tconst sortBySelected = this.sortByOptions.filter((a) => a.key === sortConfig.by)[0];\n\t\tconst sortTimeSelected = this.sortTimeOptions.filter((a) => a.key === sortConfig.time)[0];\n\n\t\treturn react.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tclassName: \"reddit-sort-bar\",\n\t\t\t},\n\t\t\treact.createElement(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tclassName: \"reddit-sort-container\",\n\t\t\t\t},\n\t\t\t\treact.createElement(OptionsMenu, {\n\t\t\t\t\toptions: this.sortByOptions,\n\t\t\t\t\tonSelect: (by) => this.props.onChange(by, null),\n\t\t\t\t\tselected: sortBySelected,\n\t\t\t\t}),\n\t\t\t\t!!sortConfig.by.match(/top|controversial/) &&\n\t\t\t\t\treact.createElement(OptionsMenu, {\n\t\t\t\t\t\toptions: this.sortTimeOptions,\n\t\t\t\t\t\tonSelect: (time) => this.props.onChange(null, time),\n\t\t\t\t\t\tselected: sortTimeSelected,\n\t\t\t\t\t})\n\t\t\t)\n\t\t);\n\t}\n}\n"
  },
  {
    "path": "CustomApps/reddit/TabBar.js",
    "content": "class TabBarItem extends react.Component {\n\trender() {\n\t\treturn react.createElement(\n\t\t\t\"li\",\n\t\t\t{\n\t\t\t\tclassName: \"reddit-tabBar-headerItem\",\n\t\t\t\tonClick: (event) => {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tthis.props.switchTo(this.props.item.key);\n\t\t\t\t},\n\t\t\t},\n\t\t\treact.createElement(\n\t\t\t\t\"a\",\n\t\t\t\t{\n\t\t\t\t\t\"aria-current\": \"page\",\n\t\t\t\t\tclassName: `reddit-tabBar-headerItemLink ${this.props.item.active ? \"reddit-tabBar-active\" : \"\"}`,\n\t\t\t\t\tdraggable: \"false\",\n\t\t\t\t\thref: \"\",\n\t\t\t\t},\n\t\t\t\treact.createElement(\n\t\t\t\t\t\"span\",\n\t\t\t\t\t{\n\t\t\t\t\t\tclassName: \"main-type-mestoBold\",\n\t\t\t\t\t},\n\t\t\t\t\tthis.props.item.value\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\t}\n}\n\nconst TabBarMore = react.memo(({ items, switchTo }) => {\n\tconst activeItem = items.find((item) => item.active);\n\n\treturn react.createElement(\n\t\t\"li\",\n\t\t{\n\t\t\tclassName: `reddit-tabBar-headerItem ${activeItem ? \"reddit-tabBar-active\" : \"\"}`,\n\t\t},\n\t\treact.createElement(OptionsMenu, {\n\t\t\toptions: items,\n\t\t\tonSelect: switchTo,\n\t\t\tselected: activeItem,\n\t\t\tdefaultValue: \"More\",\n\t\t\tbold: true,\n\t\t})\n\t);\n});\n\nconst TopBarContent = ({ links, activeLink, switchCallback }) => {\n\tconst resizeHost = document.querySelector(\n\t\t\".Root__main-view .os-resize-observer-host, .Root__main-view .os-size-observer, .Root__main-view .main-view-container__scroll-node\"\n\t);\n\tconst [windowSize, setWindowSize] = useState(resizeHost.clientWidth);\n\tconst resizeHandler = () => setWindowSize(resizeHost.clientWidth);\n\n\tuseEffect(() => {\n\t\tconst observer = new ResizeObserver(resizeHandler);\n\t\tobserver.observe(resizeHost);\n\t\treturn () => {\n\t\t\tobserver.disconnect();\n\t\t};\n\t}, [resizeHandler]);\n\n\treturn react.createElement(\n\t\tTabBarContext,\n\t\tnull,\n\t\treact.createElement(TabBar, {\n\t\t\tclassName: \"queue-queueHistoryTopBar-tabBar\",\n\t\t\tlinks,\n\t\t\tactiveLink,\n\t\t\twindowSize,\n\t\t\tswitchCallback,\n\t\t})\n\t);\n};\n\nconst TabBarContext = ({ children }) => {\n\treturn Spicetify.ReactDOM.createPortal(\n\t\treact.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tclassName: \"main-topBar-topbarContent\",\n\t\t\t},\n\t\t\tchildren\n\t\t),\n\t\tdocument.querySelector(\".main-topBar-topbarContentWrapper\")\n\t);\n};\n\nconst TabBar = react.memo(({ links, activeLink, switchCallback, windowSize = Number.POSITIVE_INFINITY }) => {\n\tconst tabBarRef = react.useRef(null);\n\tconst [childrenSizes, setChildrenSizes] = useState([]);\n\tconst [availableSpace, setAvailableSpace] = useState(0);\n\tconst [droplistItem, setDroplistItems] = useState([]);\n\n\tconst options = links.map((key) => {\n\t\tconst active = key === activeLink;\n\t\treturn { key, value: key, active };\n\t});\n\n\tuseEffect(() => {\n\t\tif (!tabBarRef.current) return;\n\t\tsetAvailableSpace(tabBarRef.current.clientWidth);\n\t}, [windowSize]);\n\n\tuseEffect(() => {\n\t\tif (!tabBarRef.current) return;\n\n\t\tconst children = Array.from(tabBarRef.current.children);\n\t\tconst tabbarItemSizes = children.map((child) => child.clientWidth);\n\n\t\tsetChildrenSizes(tabbarItemSizes);\n\t}, [links]);\n\n\tuseEffect(() => {\n\t\tif (!tabBarRef.current) return;\n\n\t\tconst totalSize = childrenSizes.reduce((a, b) => a + b, 0);\n\n\t\t// Can we render everything?\n\t\tif (totalSize <= availableSpace) {\n\t\t\tsetDroplistItems([]);\n\t\t\treturn;\n\t\t}\n\n\t\t// The `More` button can be set to _any_ of the children. So we\n\t\t// reserve space for the largest item instead of always taking\n\t\t// the last item.\n\t\tconst viewMoreButtonSize = Math.max(...childrenSizes);\n\n\t\t// Figure out how many children we can render while also showing\n\t\t// the More button\n\t\tconst itemsToHide = [];\n\t\tlet stopWidth = viewMoreButtonSize;\n\n\t\tchildrenSizes.forEach((childWidth, i) => {\n\t\t\tif (availableSpace >= stopWidth + childWidth) {\n\t\t\t\tstopWidth += childWidth;\n\t\t\t} else {\n\t\t\t\titemsToHide.push(i);\n\t\t\t}\n\t\t});\n\n\t\tsetDroplistItems(itemsToHide);\n\t}, [availableSpace, childrenSizes]);\n\n\treturn react.createElement(\n\t\t\"nav\",\n\t\t{\n\t\t\tclassName: \"reddit-tabBar reddit-tabBar-nav\",\n\t\t},\n\t\treact.createElement(\n\t\t\t\"ul\",\n\t\t\t{\n\t\t\t\tclassName: \"reddit-tabBar-header\",\n\t\t\t\tref: tabBarRef,\n\t\t\t},\n\t\t\toptions\n\t\t\t\t.filter((_, id) => !droplistItem.includes(id))\n\t\t\t\t.map((item) =>\n\t\t\t\t\treact.createElement(TabBarItem, {\n\t\t\t\t\t\titem,\n\t\t\t\t\t\tswitchTo: switchCallback,\n\t\t\t\t\t})\n\t\t\t\t),\n\t\t\tdroplistItem.length || childrenSizes.length === 0\n\t\t\t\t? react.createElement(TabBarMore, {\n\t\t\t\t\t\titems: droplistItem.map((i) => options[i]).filter(Boolean),\n\t\t\t\t\t\tswitchTo: switchCallback,\n\t\t\t\t\t})\n\t\t\t\t: null\n\t\t)\n\t);\n});\n"
  },
  {
    "path": "CustomApps/reddit/index.js",
    "content": "// Run \"npm i @types/react-dom @types/react\" to have this type package available in workspace\n/// <reference types=\"react\" />\n/// <reference types=\"react-dom\" />\n\n/** @type {React} */\nconst react = Spicetify.React;\n/** @type {ReactDOM} */\nconst reactDOM = Spicetify.ReactDOM;\nconst {\n\tURI,\n\tReact: { useState, useEffect, useCallback },\n\tPlatform: { History },\n} = Spicetify;\n\n// Define a function called \"render\" to specify app entry point\n// This function will be used to mount app to main view.\nfunction render() {\n\treturn react.createElement(Grid, { title: \"Reddit\" });\n}\n\nconst CONFIG = {\n\tvisual: {\n\t\ttype: localStorage.getItem(\"reddit:type\") === \"true\",\n\t\tupvotes: localStorage.getItem(\"reddit:upvotes\") === \"true\",\n\t\tfollowers: localStorage.getItem(\"reddit:followers\") === \"true\",\n\t\tlongDescription: localStorage.getItem(\"reddit:longDescription\") === \"true\",\n\t},\n\tservices: localStorage.getItem(\"reddit:services\") || `[\"spotify\",\"makemeaplaylist\",\"SpotifyPlaylists\",\"music\",\"edm\",\"popheads\"]`,\n\tlastService: localStorage.getItem(\"reddit:last-service\"),\n};\n\ntry {\n\tCONFIG.services = JSON.parse(CONFIG.services);\n\tif (!Array.isArray(CONFIG.services)) {\n\t\tthrow \"\";\n\t}\n} catch {\n\tCONFIG.services = [\"spotify\", \"makemeaplaylist\", \"SpotifyPlaylists\", \"music\", \"edm\", \"popheads\"];\n\tlocalStorage.setItem(\"reddit:services\", JSON.stringify(CONFIG.services));\n}\n\nif (!CONFIG.lastService || !CONFIG.services.includes(CONFIG.lastService)) {\n\tCONFIG.lastService = CONFIG.services[0];\n}\nconst sortConfig = {\n\tby: localStorage.getItem(\"reddit:sort-by\") || \"top\",\n\ttime: localStorage.getItem(\"reddit:sort-time\") || \"month\",\n};\nlet cardList = [];\nlet endOfList = false;\nlet lastScroll = 0;\nlet requestQueue = [];\nlet requestAfter = null;\n\nlet gridUpdateTabs;\nlet gridUpdatePostsVisual;\n\nconst typesLocale = {\n\talbum: Spicetify.Locale.get(\"album\"),\n\tsong: Spicetify.Locale.get(\"song\"),\n\tplaylist: Spicetify.Locale.get(\"playlist\"),\n};\n\nclass Grid extends react.Component {\n\tviewportSelector = document.querySelector(\"#main .os-viewport\") ? \"#main .os-viewport\" : \"#main .main-view-container__scroll-node\";\n\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tObject.assign(this, props);\n\t\tthis.state = {\n\t\t\tcards: [],\n\t\t\ttabs: CONFIG.services,\n\t\t\trest: true,\n\t\t\tendOfList: endOfList,\n\t\t};\n\t}\n\n\tnewRequest(amount) {\n\t\tcardList = [];\n\t\tconst queue = [];\n\t\trequestQueue.unshift(queue);\n\t\tthis.loadAmount(queue, amount);\n\t}\n\n\tappendCard(item) {\n\t\titem.visual = CONFIG.visual;\n\t\tcardList.push(react.createElement(Card, item));\n\t\tthis.setState({ cards: cardList });\n\t}\n\n\tupdateSort(sortByValue, sortTimeValue) {\n\t\tif (sortByValue) {\n\t\t\tsortConfig.by = sortByValue;\n\t\t\tlocalStorage.setItem(\"reddit:sort-by\", sortByValue);\n\t\t}\n\t\tif (sortTimeValue) {\n\t\t\tsortConfig.time = sortTimeValue;\n\t\t\tlocalStorage.setItem(\"reddit:sort-time\", sortTimeValue);\n\t\t}\n\n\t\trequestAfter = null;\n\t\tcardList = [];\n\t\tthis.setState({\n\t\t\tcards: [],\n\t\t\trest: false,\n\t\t\tendOfList: false,\n\t\t});\n\t\tendOfList = false;\n\n\t\tthis.newRequest(30);\n\t}\n\n\tupdateTabs() {\n\t\tthis.setState({\n\t\t\ttabs: [...CONFIG.services],\n\t\t});\n\t}\n\n\tupdatePostsVisual() {\n\t\tcardList = cardList.map((card) => {\n\t\t\treturn react.createElement(Card, card.props);\n\t\t});\n\t\tthis.setState({ cards: [...cardList] });\n\t}\n\n\tswitchTo(value) {\n\t\tCONFIG.lastService = value;\n\t\tlocalStorage.setItem(\"reddit:last-service\", value);\n\t\tcardList = [];\n\t\trequestAfter = null;\n\t\tthis.setState({\n\t\t\tcards: [],\n\t\t\trest: false,\n\t\t\tendOfList: false,\n\t\t});\n\t\tendOfList = false;\n\n\t\tthis.newRequest(30);\n\t}\n\n\tasync loadPage(queue) {\n\t\tconst subMeta = await getSubreddit(requestAfter);\n\t\tconst posts = postMapper(subMeta.data.children);\n\t\tfor (const post of posts) {\n\t\t\tlet item;\n\t\t\tswitch (post.type) {\n\t\t\t\tcase \"playlist\":\n\t\t\t\tcase \"playlist-v2\":\n\t\t\t\t\titem = await fetchPlaylist(post);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"track\":\n\t\t\t\t\titem = await fetchTrack(post);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"album\":\n\t\t\t\t\titem = await fetchAlbum(post);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (requestQueue.length > 1 && queue !== requestQueue[0]) {\n\t\t\t\t// Stop this queue from continuing to fetch and append to cards list\n\t\t\t\treturn -1;\n\t\t\t}\n\n\t\t\titem && this.appendCard(item);\n\t\t}\n\n\t\tif (subMeta.data.after) {\n\t\t\treturn subMeta.data.after;\n\t\t}\n\n\t\tthis.setState({ rest: true, endOfList: true });\n\t\tendOfList = true;\n\t\treturn null;\n\t}\n\n\tasync loadAmount(queue, quantity = 50) {\n\t\tthis.setState({ rest: false });\n\t\tlet addQuantity = quantity;\n\t\taddQuantity += cardList.length;\n\n\t\trequestAfter = await this.loadPage(queue);\n\t\twhile (requestAfter && requestAfter !== -1 && cardList.length < addQuantity && !this.endOfList) {\n\t\t\trequestAfter = await this.loadPage(queue);\n\t\t}\n\n\t\tif (requestAfter === -1) {\n\t\t\trequestQueue = requestQueue.filter((a) => a !== queue);\n\t\t\treturn;\n\t\t}\n\n\t\t// Remove this queue from queue list\n\t\trequestQueue.shift();\n\t\tthis.setState({ rest: true });\n\t}\n\n\tloadMore() {\n\t\tif (this.state.rest && !endOfList) {\n\t\t\tthis.loadAmount(requestQueue[0], 50);\n\t\t}\n\t}\n\n\tasync componentDidMount() {\n\t\tgridUpdateTabs = this.updateTabs.bind(this);\n\t\tgridUpdatePostsVisual = this.updatePostsVisual.bind(this);\n\n\t\tthis.configButton = new Spicetify.Menu.Item(\"Reddit config\", false, openConfig);\n\t\tthis.configButton.register();\n\n\t\tconst viewPort = document.querySelector(this.viewportSelector);\n\t\tthis.checkScroll = this.isScrolledBottom.bind(this);\n\t\tviewPort.addEventListener(\"scroll\", this.checkScroll);\n\n\t\tif (cardList.length) {\n\t\t\t// Already loaded\n\t\t\tif (lastScroll > 0) {\n\t\t\t\tviewPort.scrollTo(0, lastScroll);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tthis.newRequest(30);\n\t}\n\n\tcomponentWillUnmount() {\n\t\tgridUpdateTabs = gridUpdatePostsVisual = null;\n\t\tconst viewPort = document.querySelector(this.viewportSelector);\n\t\tlastScroll = viewPort.scrollTop;\n\t\tviewPort.removeEventListener(\"scroll\", this.checkScroll);\n\t\tthis.configButton.deregister();\n\t}\n\n\tisScrolledBottom(event) {\n\t\tconst viewPort = event.target;\n\t\tif (viewPort.scrollTop + viewPort.clientHeight >= viewPort.scrollHeight) {\n\t\t\t// At bottom, load more posts\n\t\t\tthis.loadMore();\n\t\t}\n\t}\n\n\trender() {\n\t\tconst expFeatures = JSON.parse(localStorage.getItem(\"spicetify-exp-features\") || \"{}\");\n\t\tconst isGlobalNav = expFeatures?.enableGlobalNavBar?.value !== \"control\";\n\t\tconst version = Spicetify.Platform.version.split(\".\").map((i) => Number.parseInt(i));\n\n\t\tconst tabBarMargin = {\n\t\t\tmarginTop: isGlobalNav || (version[0] === 1 && version[1] === 2 && version[2] >= 45) ? \"60px\" : \"0px\",\n\t\t};\n\t\treturn react.createElement(\n\t\t\t\"section\",\n\t\t\t{\n\t\t\t\tclassName: \"contentSpacing\",\n\t\t\t},\n\t\t\treact.createElement(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tclassName: \"reddit-header\",\n\t\t\t\t\tstyle: tabBarMargin,\n\t\t\t\t},\n\t\t\t\treact.createElement(\"h1\", null, this.props.title),\n\t\t\t\treact.createElement(SortBox, {\n\t\t\t\t\tonChange: this.updateSort.bind(this),\n\t\t\t\t\tonServicesChange: this.updateTabs.bind(this),\n\t\t\t\t})\n\t\t\t),\n\t\t\treact.createElement(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tid: \"reddit-grid\",\n\t\t\t\t\tclassName: \"main-gridContainer-gridContainer main-gridContainer-fixedWidth\",\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\"--minimumColumnWidth\": \"180px\",\n\t\t\t\t\t\t\"--column-width\": \"minmax(var(--minimumColumnWidth),1fr)\",\n\t\t\t\t\t\t\"--column-count\": \"auto-fill\",\n\t\t\t\t\t\t\"--grid-gap\": \"24px\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t[...cardList]\n\t\t\t),\n\t\t\treact.createElement(\n\t\t\t\t\"footer\",\n\t\t\t\t{\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\tmargin: \"auto\",\n\t\t\t\t\t\ttextAlign: \"center\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t!this.state.endOfList &&\n\t\t\t\t\t(this.state.rest ? react.createElement(LoadMoreIcon, { onClick: this.loadMore.bind(this) }) : react.createElement(LoadingIcon))\n\t\t\t),\n\t\t\t!!document.querySelector(\".main-topBar-topbarContentWrapper\") &&\n\t\t\t\treact.createElement(TopBarContent, {\n\t\t\t\t\tswitchCallback: this.switchTo.bind(this),\n\t\t\t\t\tlinks: CONFIG.services,\n\t\t\t\t\tactiveLink: CONFIG.lastService,\n\t\t\t\t})\n\t\t);\n\t}\n}\n\nasync function getSubreddit(after = \"\") {\n\t// www is needed or it will block with \"cross-origin\" error.\n\tlet url = `https://www.reddit.com/r/${CONFIG.lastService}/${sortConfig.by}.json?limit=100&count=10&raw_json=1`;\n\tif (after) {\n\t\turl += `&after=${after}`;\n\t}\n\tif (sortConfig.by.match(/top|controversial/) && sortConfig.time) {\n\t\turl += `&t=${sortConfig.time}`;\n\t}\n\n\treturn await fetch(url, { method: \"GET\" }).then((res) => res.json());\n}\n\nasync function fetchPlaylist(post) {\n\ttry {\n\t\tconst res = await Spicetify.CosmosAsync.get(`sp://core-playlist/v1/playlist/${post.uri}/metadata`, {\n\t\t\tpolicy: {\n\t\t\t\tname: true,\n\t\t\t\tpicture: true,\n\t\t\t\tfollowers: true,\n\t\t\t},\n\t\t});\n\n\t\tconst { metadata } = res;\n\t\treturn {\n\t\t\ttype: typesLocale.playlist,\n\t\t\turi: post.uri,\n\t\t\ttitle: metadata.name,\n\t\t\tsubtitle: post.title,\n\t\t\timageURL: metadata.picture,\n\t\t\tupvotes: post.upvotes,\n\t\t\tfollowersCount: metadata.followers,\n\t\t};\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nasync function fetchAlbum(post) {\n\tconst { getAlbum } = Spicetify.GraphQL.Definitions;\n\n\ttry {\n\t\tconst { data } = await Spicetify.GraphQL.Request(getAlbum, {\n\t\t\turi: post.uri,\n\t\t\tlocale: Spicetify.Locale.getLocale(),\n\t\t\toffset: 0,\n\t\t\tlimit: 10,\n\t\t});\n\t\tconst metadata = data.albumUnion;\n\n\t\treturn {\n\t\t\ttype: typesLocale.album,\n\t\t\turi: post.uri,\n\t\t\ttitle: metadata.name,\n\t\t\tsubtitle: metadata.artists.items.map((artist) => artist.profile.name).join(\", \"),\n\t\t\timageURL: metadata.coverArt.sources.reduce((prev, curr) => (prev.width > curr.width ? prev : curr)).url,\n\t\t\tupvotes: post.upvotes,\n\t\t};\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nasync function fetchTrack(post) {\n\tconst arg = post.uri.split(\":\")[2];\n\ttry {\n\t\tconst metadata = await Spicetify.CosmosAsync.get(`https://api.spotify.com/v1/tracks/${arg}`);\n\t\treturn {\n\t\t\ttype: typesLocale.song,\n\t\t\turi: post.uri,\n\t\t\ttitle: metadata.name,\n\t\t\tsubtitle: metadata.artists,\n\t\t\timageURL: metadata.album.images[0].url,\n\t\t\tupvotes: post.upvotes,\n\t\t};\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction postMapper(posts) {\n\tconst mappedPosts = [];\n\tfor (const post of posts) {\n\t\tconst uri = URI.from(post.data.url);\n\t\tif (uri && (uri.type === \"playlist\" || uri.type === \"playlist-v2\" || uri.type === \"track\" || uri.type === \"album\")) {\n\t\t\tmappedPosts.push({\n\t\t\t\turi: uri.toURI(),\n\t\t\t\ttype: uri.type,\n\t\t\t\ttitle: post.data.title,\n\t\t\t\tupvotes: post.data.ups,\n\t\t\t});\n\t\t}\n\t}\n\treturn mappedPosts;\n}\n"
  },
  {
    "path": "CustomApps/reddit/manifest.json",
    "content": "{\n\t\"name\": \"Reddit\",\n\t\"icon\": \"<svg viewBox=\\\"0 0 256 256\\\" fill=\\\"currentColor\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"1\\\"><path d=\\\"M 45.19 84.802 c -21.821 0 -39.58 -12.617 -39.63 -28.14 c -0.038 -0.584 -0.053 -1.17 -0.046 -1.757 c -0.654 -0.398 -1.28 -0.876 -1.87 -1.427 c -2.223 -2.082 -3.498 -4.902 -3.594 -7.943 c -0.096 -3.045 1 -5.944 3.086 -8.162 c 2.077 -2.217 4.897 -3.493 7.938 -3.589 c 2.517 -0.08 4.933 0.655 6.948 2.09 c 7.07 -4.481 15.131 -7.034 23.485 -7.437 l 4.429 -20.818 c 0.007 -0.054 0.016 -0.107 0.027 -0.156 c 0.228 -1.037 0.847 -1.923 1.742 -2.492 c 0.893 -0.569 1.957 -0.754 2.991 -0.523 L 66.49 7.604 c 1.059 0.212 1.747 1.242 1.535 2.302 c -0.211 1.059 -1.244 1.745 -2.301 1.535 L 49.799 8.259 c -0.004 0.021 -0.008 0.043 -0.012 0.063 l -4.292 20.175 c 8.266 0.582 19.14 3.267 26.48 7.764 c 1.829 -1.32 4.002 -2.075 6.258 -2.154 c 6.28 -0.203 11.576 4.711 11.808 10.976 c 0.07 3.952 -1.928 7.647 -5.176 9.771 c 0.009 0.604 -0.007 1.208 -0.046 1.808 C 84.767 72.184 67.01 84.802 45.19 84.802 z M 11.44 37.691 c -0.081 0 -0.162 0.001 -0.243 0.004 c -1.995 0.063 -3.845 0.9 -5.208 2.356 c -1.372 1.459 -2.091 3.362 -2.028 5.361 c 0.063 1.995 0.9 3.845 2.356 5.208 c 0.623 0.582 1.294 1.034 1.99 1.341 c 0.753 0.333 1.218 1.102 1.162 1.923 c -0.058 0.855 -0.058 1.722 0 2.578 c 0.003 0.044 0.004 0.089 0.004 0.133 c 0 13.396 16.022 24.294 35.717 24.294 c 19.694 0 35.717 -10.898 35.717 -24.294 c 0 -0.045 0.002 -0.089 0.005 -0.134 c 0.058 -0.855 0.058 -1.722 0 -2.577 c -0.054 -0.787 0.371 -1.53 1.077 -1.884 c 2.566 -1.283 4.192 -3.957 4.141 -6.812 c -0.151 -4.077 -3.647 -7.294 -7.762 -7.172 c -1.817 0.063 -3.559 0.802 -4.901 2.08 c -0.667 0.635 -1.688 0.72 -2.45 0.2 c -7.27 -4.952 -19.8 -7.814 -27.92 -7.998 c -8.622 0.146 -16.957 2.78 -24.104 7.616 c -0.759 0.512 -1.771 0.432 -2.437 -0.195 C 15.157 38.407 13.349 37.691 11.44 37.691 z M 44.004 75.379 c -5.779 0 -11.464 -1.896 -16.12 -5.396 c -0.864 -0.649 -1.037 -1.875 -0.388 -2.739 c 0.65 -0.865 1.875 -1.037 2.739 -0.389 c 4.232 3.181 9.455 4.818 14.712 4.59 c 0.054 -0.002 0.109 -0.002 0.164 0 c 5.261 0.205 10.484 -1.411 14.713 -4.59 c 0.593 -0.445 1.386 -0.519 2.05 -0.186 c 0.663 0.331 1.082 1.009 1.082 1.75 v 0.257 c 0 0.975 -0.715 1.785 -1.649 1.933 c -4.765 3.292 -10.51 4.961 -16.277 4.75 C 44.688 75.372 44.345 75.379 44.004 75.379 z\\\" transform=\\\"translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)\\\" stroke-linecap=\\\"round\\\"></path><path d=\\\"M 23.453 51.63 c 0 -3.566 2.908 -6.475 6.475 -6.475 c 3.566 0 6.475 2.908 6.475 6.475 c 0 3.566 -2.908 6.475 -6.475 6.475 C 26.346 58.071 23.453 55.196 23.453 51.63 z\\\" transform=\\\"translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)\\\" stroke-linecap=\\\"round\\\"></path><path d=\\\"M 60.115 58.57 c -0.112 0 -0.208 0 -0.322 0 l 0.049 -0.241 c -3.566 0 -6.475 -2.908 -6.475 -6.475 c 0 -3.566 2.908 -6.475 6.475 -6.475 s 6.475 2.908 6.475 6.475 C 66.461 55.421 63.681 58.425 60.115 58.57 z\\\" transform=\\\"translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)\\\" stroke-linecap=\\\"round\\\"></path><path d=\\\"M 72.514 18.796 c -4.493 0 -8.148 -3.655 -8.148 -8.148 c 0 -4.493 3.655 -8.148 8.148 -8.148 s 8.148 3.655 8.148 8.148 C 80.662 15.141 77.006 18.796 72.514 18.796 z M 72.514 6.414 c -2.335 0 -4.235 1.9 -4.235 4.235 c 0 2.335 1.9 4.235 4.235 4.235 s 4.235 -1.9 4.235 -4.235 C 76.749 8.314 74.848 6.414 72.514 6.414 z\\\" transform=\\\"translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)\\\" stroke-linecap=\\\"round\\\"></path></svg>\",\n\t\"active-icon\": \"<svg viewBox=\\\"0 0 256 256\\\" fill=\\\"currentColor\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"1\\\"><path d=\\\"M 89.998 45.604 c -0.201 -5.442 -4.77 -9.691 -10.229 -9.506 c -2.419 0.084 -4.719 1.075 -6.466 2.737 c -7.693 -5.24 -16.729 -8.113 -26.017 -8.314 L 51.67 9.442 l 14.461 3.041 c 0.402 3.712 3.728 6.4 7.44 5.996 c 3.712 -0.402 6.4 -3.728 5.996 -7.44 c -0.404 -3.712 -3.728 -6.4 -7.44 -5.996 c -2.134 0.218 -4.048 1.461 -5.105 3.309 L 50.461 5.043 c -1.125 -0.252 -2.251 0.453 -2.503 1.596 c 0 0.017 0 0.017 0 0.033 L 42.97 30.119 c -9.406 0.152 -18.559 3.041 -26.352 8.314 c -3.964 -3.728 -10.212 -3.544 -13.94 0.437 c -3.728 3.964 -3.544 10.212 0.437 13.94 c 0.773 0.722 1.662 1.344 2.653 1.781 c -0.068 0.991 -0.068 1.982 0 2.973 c 0 15.133 17.636 27.444 39.386 27.444 c 21.75 0 39.386 -12.295 39.386 -27.444 c 0.068 -0.991 0.068 -1.982 0 -2.973 C 87.932 52.894 90.066 49.4 89.998 45.604 z M 22.429 52.373 c 0 -3.728 3.041 -6.769 6.769 -6.769 c 3.728 0 6.769 3.041 6.769 6.769 c 0 3.728 -3.041 6.769 -6.769 6.769 C 25.453 59.108 22.429 56.102 22.429 52.373 z M 61.681 71.218 v -0.269 c -4.804 3.611 -10.682 5.458 -16.696 5.207 c -6.014 0.252 -11.891 -1.596 -16.696 -5.207 c -0.638 -0.773 -0.521 -1.931 0.252 -2.569 c 0.671 -0.554 1.629 -0.554 2.318 0 c 4.065 2.973 9.02 4.485 14.058 4.249 c 5.039 0.269 10.011 -1.176 14.125 -4.114 c 0.739 -0.722 1.948 -0.706 2.671 0.033 C 62.436 69.287 62.419 70.496 61.681 71.218 z M 60.757 59.629 c -0.117 0 -0.218 0 -0.336 0 l 0.051 -0.252 c -3.728 0 -6.769 -3.041 -6.769 -6.769 c 0 -3.728 3.041 -6.769 6.769 -6.769 c 3.728 0 6.769 3.041 6.769 6.769 C 67.391 56.337 64.486 59.477 60.757 59.629 z\\\" transform=\\\"translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)\\\" stroke-linecap=\\\"round\\\"></path></svg>\",\n\t\"subfiles\": [\"Card.js\", \"Icons.js\", \"OptionsMenu.js\", \"SortBox.js\", \"TabBar.js\", \"Settings.js\"]\n}\n"
  },
  {
    "path": "CustomApps/reddit/style.css",
    "content": ".setting-row::after {\n\tcontent: \"\";\n\tdisplay: table;\n\tclear: both;\n}\n.setting-row .col {\n\tdisplay: flex;\n\tpadding: 10px 0;\n\talign-items: center;\n}\n.setting-row .col.description {\n\tfloat: left;\n\tpadding-right: 15px;\n\tcursor: default;\n}\n.setting-row .col.action {\n\tfloat: right;\n\ttext-align: right;\n}\nbutton.switch {\n\talign-items: center;\n\tborder: 0px;\n\tborder-radius: 50%;\n\tbackground-color: rgba(var(--spice-rgb-shadow), 0.7);\n\tcolor: var(--spice-text);\n\tcursor: pointer;\n\tdisplay: flex;\n\tmargin-inline-start: 12px;\n\tpadding: 8px;\n}\nbutton.switch.disabled,\nbutton.switch[disabled] {\n\tcolor: rgba(var(--spice-rgb-text), 0.3);\n}\nbutton.switch.small {\n\twidth: 22px;\n\theight: 22px;\n\tpadding: 6px;\n}\n.reddit-sort-container .optionsMenu-dropBox {\n\tgrid-gap: 8px;\n\talign-items: center;\n\tbackground-color: transparent;\n\tborder-radius: 4px;\n\tdisplay: grid;\n\tgrid-template-columns: 1fr 16px;\n\tcolor: rgba(var(--spice-rgb-text), 0.7);\n\tborder: 0;\n\theight: 32px;\n\tmargin-left: 8px;\n\tpadding: 0 8px 0 12px;\n}\n.reddit-sort-container .optionsMenu-dropBox:hover {\n\tcolor: var(--spice-text);\n}\n#reddit-config-container input {\n\twidth: 100%;\n\tmargin-top: 10px;\n\tpadding: 0 5px;\n\theight: 32px;\n\tborder: 0;\n\tcolor: var(--spice-text);\n\tbackground-color: initial;\n\tborder-bottom: 1px solid var(--spice-text);\n}\n\noption {\n\tbackground-color: var(--spice-button);\n}\n\n.reddit-header {\n\t-webkit-box-pack: justify;\n\t-webkit-box-align: center;\n\talign-content: space-between;\n\talign-items: center;\n\tcolor: var(--spice-text);\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tmargin: 16px 0;\n}\n\n/* New layout top bar height = 64px + Original margin = 16px */\n.Root__fixed-top-bar ~ .Root__main-view .reddit-header {\n\tmargin-top: 80px;\n}\n\n.reddit-sort-bar {\n\talign-items: center;\n\tdisplay: flex;\n}\n\n.reddit-sort-container {\n\tposition: relative;\n\tdisplay: flex;\n}\n\n.reddit-tabBar-headerItem {\n\t-webkit-app-region: no-drag;\n\tdisplay: inline-block;\n\tpointer-events: auto;\n}\n\n.reddit-tabBar-headerItemLink {\n\tmargin: 0 8px 0 0;\n}\n\n.reddit-tabBar-active {\n\tbackground-color: var(--spice-tab-active);\n\tborder-radius: 4px;\n}\n\n.reddit-tabBar-headerItemLink {\n\tborder-radius: 4px;\n\tcolor: var(--spice-text);\n\tdisplay: inline-block;\n\tmargin: 0 8px;\n\tpadding: 8px 16px;\n\tposition: relative;\n\ttext-decoration: none !important;\n\tcursor: pointer;\n}\n\n.reddit-tabBar-nav {\n\t-webkit-app-region: drag;\n\tpointer-events: none;\n\twidth: 100%;\n}\n\n.reddit-tabBar-headerItem .optionsMenu-dropBox {\n\tcolor: var(--spice-text);\n\tborder: 0;\n\tmax-width: 150px;\n\theight: 42px;\n\tpadding: 0 30px 0 12px;\n\tbackground-color: initial;\n\tcursor: pointer;\n\tappearance: none;\n}\n\n.reddit-tabBar-headerItem .optionsMenu-dropBox svg {\n\tposition: absolute;\n\tmargin-left: 8px;\n}\n\ndiv.reddit-tabBar-headerItemLink {\n\tpadding: 0;\n}\n\n.reddit-cardSubHeader {\n\tmargin-top: 4px;\n\twhite-space: normal;\n\tcolor: var(--spice-subtext);\n}\n\n.reddit-longDescription {\n\tdisplay: flex;\n}\n.reddit-header + .main-gridContainer-gridContainer .main-card-card .main-playButton-PlayButton button {\n\t-webkit-tap-highlight-color: transparent;\n\tbackground-color: transparent;\n\tborder: 0px;\n\tborder-radius: 500px;\n\tdisplay: inline-block;\n\tposition: relative;\n\ttouch-action: manipulation;\n\ttransition-duration: 33ms;\n\ttransition-property: background-color, border-color, color, box-shadow, filter, transform;\n\tuser-select: none;\n\tvertical-align: middle;\n\ttransform: translate3d(0px, 0px, 0px);\n\tpadding: 0px;\n\tmin-inline-size: 0px;\n\talign-self: center;\n}\n.reddit-header + .main-gridContainer-gridContainer .main-card-card .main-playButton-PlayButton span {\n\t-webkit-tap-highlight-color: transparent;\n\tposition: relative;\n\tbackground-color: var(--spice-button-active);\n\tcolor: var(--spice-sidebar);\n\tdisplay: flex;\n\tborder-radius: 500px;\n\tfont-size: inherit;\n\tmin-block-size: 48px;\n\t-webkit-box-align: center;\n\talign-items: center;\n\t-webkit-box-pack: center;\n\tjustify-content: center;\n\tinline-size: 48px;\n\tblock-size: 48px;\n}\n.reddit-header + .main-gridContainer-gridContainer .main-card-card .main-playButton-PlayButton span:hover {\n\ttransform: scale(1.04);\n}\n"
  },
  {
    "path": "Extensions/autoSkipExplicit.js",
    "content": "// NAME: Christian Spotify\n// AUTHOR: khanhas\n// DESCRIPTION: Auto skip explicit songs. Toggle in Profile menu.\n\n/// <reference path=\"../globals.d.ts\" />\n\n(async function ChristianSpotify() {\n\tif (!Spicetify.LocalStorage) {\n\t\tsetTimeout(ChristianSpotify, 1000);\n\t\treturn;\n\t}\n\tawait new Promise((res) => Spicetify.Events.webpackLoaded.on(res));\n\n\tlet isEnabled = Spicetify.LocalStorage.get(\"ChristianMode\") === \"1\";\n\n\tnew Spicetify.Menu.Item(\"Christian mode\", isEnabled, (self) => {\n\t\tisEnabled = !isEnabled;\n\t\tSpicetify.LocalStorage.set(\"ChristianMode\", isEnabled ? \"1\" : \"0\");\n\t\tself.setState(isEnabled);\n\t}).register();\n\n\tSpicetify.Player.addEventListener(\"songchange\", () => {\n\t\tif (!isEnabled) return;\n\t\tconst data = Spicetify.Player.data || Spicetify.Queue;\n\t\tif (!data) return;\n\n\t\tconst isExplicit = data.item.metadata.is_explicit;\n\t\tif (isExplicit === \"true\") {\n\t\t\tSpicetify.Player.next();\n\t\t}\n\t});\n})();\n"
  },
  {
    "path": "Extensions/autoSkipVideo.js",
    "content": "// NAME: Auto Skip Video\n// AUTHOR: khanhas\n// DESCRIPTION: Auto skip video\n\n/// <reference path=\"../globals.d.ts\" />\n\n(function SkipVideo() {\n\tSpicetify.Player.addEventListener(\"songchange\", () => {\n\t\tconst data = Spicetify.Player.data || Spicetify.Queue;\n\t\tif (!data) return;\n\n\t\tconst meta = data.item.metadata;\n\t\t// Ads are also video media type so I need to exclude them out.\n\t\tif (meta[\"media.type\"] === \"video\" && meta.is_advertisement !== \"true\") {\n\t\t\tSpicetify.Player.next();\n\t\t}\n\t});\n})();\n"
  },
  {
    "path": "Extensions/bookmark.js",
    "content": "// NAME: Bookmark\n// AUTHOR: khanhas\n// VERSION: 2.0\n// DESCRIPTION: Store page, track, track with time to view/listen later.\n\n/// <reference path=\"../globals.d.ts\" />\n\n(function Bookmark() {\n\tconst { CosmosAsync, Player, LocalStorage, ContextMenu, URI } = Spicetify;\n\tif (!(CosmosAsync && URI)) {\n\t\tsetTimeout(Bookmark, 300);\n\t\treturn;\n\t}\n\n\t// UI Text\n\tconst BUTTON_NAME_TEXT = \"Bookmark\";\n\tconst REMOVE_TEXT = \"Remove\";\n\n\t// Local Storage keys\n\tconst STORAGE_KEY = \"bookmark_spicetify\";\n\n\tclass BookmarkCollection {\n\t\tconstructor() {\n\t\t\tconst menu = createMenu();\n\t\t\tthis.container = menu.container;\n\t\t\tthis.items = menu.menu;\n\t\t\tthis.lastScroll = 0;\n\t\t\tthis.container.onclick = () => {\n\t\t\t\tthis.storeScroll();\n\t\t\t\tthis.container.remove();\n\t\t\t};\n\t\t\tthis.filter = 0;\n\t\t\tthis.apply();\n\t\t}\n\n\t\tapply() {\n\t\t\tthis.items.textContent = \"\"; // Remove all childs\n\t\t\tthis.items.append(createMenuItem(\"Current page\", storeThisPage));\n\t\t\tthis.items.append(createMenuItem(\"Track\", storeTrack));\n\t\t\tthis.items.append(createMenuItem(\"Track with timestamp\", storeTrackWithTime));\n\n\t\t\tconst select = createSortSelect(this.filter);\n\t\t\tselect.onchange = (event) => {\n\t\t\t\tthis.filter = event.srcElement.selectedIndex;\n\t\t\t\tthis.apply();\n\t\t\t};\n\t\t\tthis.items.append(select);\n\n\t\t\tconst collection = this.getStorage();\n\t\t\tfor (const item of collection) {\n\t\t\t\tif (this.filter !== 0) {\n\t\t\t\t\tconst isTrack = this.isTrack(item.uri);\n\t\t\t\t\tif (this.filter === 1 && isTrack) continue;\n\t\t\t\t\tif (this.filter === 2 && !isTrack) continue;\n\t\t\t\t}\n\n\t\t\t\tthis.items.append(new CardContainer(item));\n\t\t\t}\n\t\t}\n\n\t\tisTrack(uri) {\n\t\t\treturn uri.startsWith(\"spotify:track:\") || uri.startsWith(\"spotify:episode:\");\n\t\t}\n\n\t\tgetStorage() {\n\t\t\tconst storageRaw = LocalStorage.get(STORAGE_KEY);\n\t\t\tlet storage = [];\n\n\t\t\tif (storageRaw) {\n\t\t\t\tstorage = JSON.parse(storageRaw);\n\t\t\t} else {\n\t\t\t\tLocalStorage.set(STORAGE_KEY, \"[]\");\n\t\t\t}\n\n\t\t\treturn storage;\n\t\t}\n\n\t\taddToStorage(data) {\n\t\t\tdata.id = `${data.uri}-${new Date().getTime()}`;\n\n\t\t\t/** @type {Object[]} */\n\t\t\tconst storage = this.getStorage();\n\t\t\tstorage.unshift(data);\n\n\t\t\tLocalStorage.set(STORAGE_KEY, JSON.stringify(storage));\n\t\t\tthis.apply();\n\t\t}\n\n\t\tremoveFromStorage(id) {\n\t\t\tconst storage = this.getStorage().filter((item) => item.id !== id);\n\n\t\t\tLocalStorage.set(STORAGE_KEY, JSON.stringify(storage));\n\t\t\tthis.apply();\n\t\t}\n\n\t\tchangePosition(x, y) {\n\t\t\tthis.items.style.left = `${x}px`;\n\t\t\tthis.items.style.top = `${y + 40}px`;\n\t\t}\n\n\t\tstoreScroll() {\n\t\t\tthis.lastScroll = this.items.scrollTop;\n\t\t}\n\n\t\tsetScroll() {\n\t\t\tthis.items.scrollTop = this.lastScroll;\n\t\t}\n\t}\n\n\tclass CardContainer extends HTMLElement {\n\t\tconstructor(info) {\n\t\t\tsuper();\n\t\t\tconst uri = URI.fromString(info.uri);\n\t\t\tconst isPlayable =\n\t\t\t\turi.type === URI.Type.TRACK ||\n\t\t\t\turi.type === URI.Type.PLAYLIST_V2 ||\n\t\t\t\turi.type === URI.Type.ALBUM ||\n\t\t\t\turi.type === URI.Type.EPISODE ||\n\t\t\t\turi.type === URI.Type.PLAYLIST;\n\n\t\t\tthis.innerHTML = `\n<style>\n.bookmark-card .ButtonInner-md-iconOnly:hover {\n\ttransform: scale(1.06);\n}\n</style>\n<div class=\"bookmark-card\">\n    ${\n\t\t\tinfo.imageUrl\n\t\t\t\t? `<img aria-hidden=\"false\" draggable=\"false\" loading=\"eager\" src=\"${info.imageUrl}\" alt=\"${info.title}\" class=\"bookmark-card-image\">`\n\t\t\t\t: \"\"\n\t\t}\n    <div class=\"bookmark-card-info\">\n        <div class=\"main-type-balladBold\"><span>${info.title}</span></div>\n        <div class=\"main-type-mesto\"><span>${info.description}</span></div>\n        ${\n\t\t\t\t\tinfo.time\n\t\t\t\t\t\t? `\n            <div class=\"bookmark-fixed-height\">\n                <div class=\"bookmark-progress\">\n                    <div class=\"bookmark-progress__bar\" style=\"--progress:${info.progress}\"></div>\n                </div>\n                <span class=\"bookmark-progress__time main-type-mesto\">${Player.formatTime(info.time)}</span>\n            </div>\n        `\n\t\t\t\t\t\t: \"\"\n\t\t\t\t}\n    </div>\n    ${\n\t\t\tisPlayable\n\t\t\t\t? `<div class=\"ButtonInner-md-iconOnly\"><button class=\"main-playButton-PlayButton main-playButton-primary\" data-tippy-content=\"Play\" style=\"--size:48px;\"><svg role=\"img\" height=\"24\" width=\"24\" viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M4.018 14L14.41 8 4.018 2z\"></path></svg></button></div>`\n\t\t\t\t: \"\"\n\t\t}\n    <button class=\"bookmark-controls\" data-tippy-content=\"${REMOVE_TEXT}\"><svg width=\"8\" height=\"8\" viewBox=\"0 0 16 16\" fill=\"currentColor\">${\n\t\t\tSpicetify.SVGIcons.x\n\t\t}</svg></button>\n</div>\n`;\n\n\t\t\tSpicetify.Tippy(this.querySelectorAll(\"[data-tippy-content]\"), Spicetify.TippyProps);\n\t\t\tif (isPlayable) {\n\t\t\t\t/** @type {HTMLButtonElement} */\n\t\t\t\tconst playButton = this.querySelector(\"button.main-playButton-PlayButton\");\n\t\t\t\tplayButton.onclick = (event) => {\n\t\t\t\t\tonPlayClick(info);\n\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t/** @type {HTMLDivElement} */\n\t\t\tconst controls = this.querySelector(\".bookmark-controls\");\n\t\t\tcontrols.onclick = (event) => {\n\t\t\t\tLIST.removeFromStorage(info.id);\n\t\t\t\tevent.stopPropagation();\n\t\t\t};\n\n\t\t\tthis.onclick = () => onLinkClick(info);\n\t\t}\n\t}\n\n\tcustomElements.define(\"bookmark-card-container\", CardContainer);\n\n\tconst LIST = new BookmarkCollection();\n\n\tnew Spicetify.Topbar.Button(\n\t\tBUTTON_NAME_TEXT,\n\t\t`<svg role=\"img\" height=\"16\" width=\"16\" viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M 13.350175,0.37457282 C 9.7802043,0.37457282 6.2102339,0.37457282 2.6402636,0.37457282 2.1901173,0.43000784 2.3537108,0.94911284 2.3229329,1.2621688 2.3229329,5.9446788 2.3229329,10.62721 2.3229329,15.309742 2.4084662,15.861041 2.9630936,15.536253 3.1614158,15.248148 4.7726941,13.696623 6.3839408,12.145098 7.9952191,10.593573 9.7069009,12.241789 11.418583,13.890005 13.130265,15.53822 13.626697,15.863325 13.724086,15.200771 13.667506,14.853516 13.667506,10.132999 13.667506,5.4124518 13.667506,0.69190384 13.671726,0.52196684 13.520105,0.37034182 13.350175,0.37457282 Z M 13.032844,14.563698 C 11.426929,13.017345 9.8210448,11.470993 8.2151293,9.9246401 7.8614008,9.6568761 7.6107412,10.12789 7.3645243,10.320193 5.8955371,11.734694 4.4265815,13.149196 2.9575943,14.563698 2.9575943,10.045543 2.9575943,5.5273888 2.9575943,1.0092338 6.3160002,1.0092338 9.674438,1.0092338 13.032844,1.0092338 13.032844,5.5273888 13.032844,10.045543 13.032844,14.563698 Z\"></path></svg>`,\n\t\t(self) => {\n\t\t\tconst bound = self.element.getBoundingClientRect();\n\t\t\tLIST.changePosition(bound.left, bound.top);\n\t\t\tdocument.body.append(LIST.container);\n\t\t\tLIST.setScroll();\n\t\t}\n\t);\n\n\t/**\n\t *\n\t * @param {string} title\n\t * @param {() => void} callback\n\t */\n\tfunction createMenuItem(title, callback) {\n\t\tconst wrapper = document.createElement(\"div\");\n\t\tSpicetify.ReactDOM.render(\n\t\t\tSpicetify.React.createElement(\n\t\t\t\tSpicetify.ReactComponent.MenuItem,\n\t\t\t\t{\n\t\t\t\t\tonClick: () => {\n\t\t\t\t\t\tcallback?.();\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\ttitle\n\t\t\t),\n\t\t\twrapper\n\t\t);\n\n\t\treturn wrapper;\n\t}\n\n\tfunction createSortSelect(defaultOpt = 0) {\n\t\tconst select = document.createElement(\"select\");\n\t\tselect.className = \"GlueDropdown bookmark-filter\";\n\t\tconst allOpt = document.createElement(\"option\");\n\t\tallOpt.text = \"All\";\n\t\tconst pageOpt = document.createElement(\"option\");\n\t\tpageOpt.text = \"Page\";\n\t\tconst trackOpt = document.createElement(\"option\");\n\t\ttrackOpt.text = \"Track\";\n\n\t\tselect.onclick = (ev) => ev.stopPropagation();\n\t\tselect.append(allOpt, pageOpt, trackOpt);\n\t\tselect.options[defaultOpt].selected = true;\n\n\t\treturn select;\n\t}\n\n\tasync function storeThisPage() {\n\t\tlet title;\n\t\tlet description;\n\t\tlet contextUri;\n\n\t\tconst context = Spicetify.Platform.History.location.pathname;\n\t\ttry {\n\t\t\tcontextUri = Spicetify.URI.fromString(context);\n\t\t} catch (e) {\n\t\t\tSpicetify.showNotification(\"Cannot bookmark this page\", true);\n\t\t\treturn;\n\t\t}\n\t\tconst uri = contextUri.toURI();\n\n\t\tconst titleElem =\n\t\t\tdocument.querySelector(\".Root__main-view h1\") ||\n\t\t\tdocument.querySelector(\".Root__main-view h2\") ||\n\t\t\tdocument.querySelector(\".Root__main-view h3\") ||\n\t\t\tdocument.querySelector(\".Root__main-view a\");\n\n\t\tif (titleElem) {\n\t\t\ttitle = titleElem.innerText;\n\t\t}\n\n\t\tif (!title && contextUri.type === URI.Type.APPLICATION) {\n\t\t\ttitle = idToProperName(contextUri.id);\n\t\t\tdescription = \"Application\";\n\t\t} else {\n\t\t\tdescription = contextUri.type.replace(/-.+$/, \"\");\n\t\t\tconst tail = context.split(\"/\");\n\t\t\tif (tail.length > 3) {\n\t\t\t\tdescription += ` ${tail[3]}`;\n\t\t\t}\n\t\t\tdescription = idToProperName(description);\n\t\t}\n\n\t\tconst headerElem = document.querySelector(\".Root__main-view .main-entityHeader-background\");\n\t\tlet imageUrl = headerElem?.style.backgroundImage.replace('url(\"', \"\").replace('\")', \"\");\n\n\t\tif (!imageUrl) {\n\t\t\tconst firstImgElem = document.querySelector(\".Root__main-view img\");\n\t\t\timageUrl = firstImgElem?.src;\n\t\t}\n\n\t\tLIST.addToStorage({\n\t\t\turi,\n\t\t\ttitle,\n\t\t\tdescription,\n\t\t\timageUrl,\n\t\t\tcontext,\n\t\t});\n\t}\n\n\tfunction getTrackMeta() {\n\t\tconst meta = {\n\t\t\ttitle: Player.data.item.metadata.title,\n\t\t\timageUrl: Player.data.item.metadata.image_url,\n\t\t};\n\t\tmeta.uri = Player.data.item.uri;\n\t\tif (URI.isEpisode(meta.uri)) {\n\t\t\tmeta.description = Player.data.item.metadata.album_title;\n\t\t} else {\n\t\t\tmeta.description = Player.data.item.metadata.artist_name;\n\t\t}\n\t\tconst playerState = Spicetify.Player.data;\n\t\tconst contextUri = URI.fromString(playerState.context_uri ?? playerState.context.uri);\n\t\tif (contextUri && (contextUri.type === URI.Type.PLAYLIST || contextUri.type === URI.Type.PLAYLIST_V2 || contextUri.type === URI.Type.ALBUM)) {\n\t\t\tmeta.context = `/${contextUri.toURLPath()}?uid=${Player.data.item.uid}`;\n\t\t}\n\n\t\treturn meta;\n\t}\n\n\tfunction storeTrack() {\n\t\tLIST.addToStorage(getTrackMeta());\n\t}\n\n\tfunction storeTrackWithTime() {\n\t\tconst meta = getTrackMeta();\n\t\tmeta.time = Player.getProgress();\n\t\tmeta.progress = Player.getProgressPercent();\n\t\tLIST.addToStorage(meta);\n\t}\n\n\t// Utilities\n\tfunction idToProperName(id) {\n\t\tconst newId = id.replace(/-/g, \" \").replace(/^.|\\s./g, (char) => char.toUpperCase());\n\n\t\treturn newId;\n\t}\n\n\tfunction createMenu() {\n\t\tconst container = document.createElement(\"div\");\n\t\tcontainer.id = \"bookmark-spicetify\";\n\t\tcontainer.className = \"context-menu-container\";\n\t\tcontainer.style.zIndex = \"1029\";\n\n\t\tconst style = document.createElement(\"style\");\n\t\tstyle.textContent = `\n#bookmark-spicetify {\n    position: absolute;\n    left: 0;\n    right: 0;\n    width: 100vw;\n    height: 100vh;\n    z-index: 5000;\n}\n#bookmark-menu {\n    display: inline-block;\n    width: 25%;\n    min-width: 380px;\n    max-height: 70%;\n    overflow: hidden auto;\n    padding-bottom: 10px;\n    position: absolute;\n    z-index: 5001;\n}\n.bookmark-card {\n    display: flex;\n    flex-direction: row;\n    justify-content: flex-start;\n    align-items: center;\n    margin-top: 20px;\n    cursor: pointer;\n    padding: 0 10px;\n}\n.bookmark-card-image {\n    width: 70px;\n    height: 70px;\n    object-fit: cover;\n    object-position: center center;\n    border-radius: 4px;\n}\n.bookmark-card-info {\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    align-items: flex-start;\n    width: 100%;\n    padding: 10px 20px;\n    color: var(--spice-text);\n}\n.bookmark-card-info span {\n    -webkit-line-clamp: 1;\n    -webkit-box-orient: vertical;\n    display: -webkit-box;\n    white-space: normal;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n.bookmark-filter {\n    margin-top: 7px;\n    margin-left: 8px;\n    border-radius: 4px;\n    padding: 0 8px 0 12px;\n    height: 32px;\n    align-items: center;\n    background-color: transparent;\n    border: 0;\n    color: var(--spice-text);\n}\n.bookmark-controls {\n    margin: 10px 0 10px 10px;\n    width: 24px;\n    height: 24px;\n    align-items: center;\n    background-color: rgba(var(--spice-rgb-shadow),.7);\n    border: none;\n    border-radius: 50%;\n    color: var(--spice-text);\n    cursor: pointer;\n    display: inline-flex;\n    justify-content: center;\n    padding: 8px;\n}\n.bookmark-fixed-height {\n    height: 30px;\n    display: flex;\n    align-items: center;\n}\n.bookmark-progress {\n    overflow: hidden;\n    width: 100px;\n    height: 4px;\n    border-radius: 2px;\n    background-color: rgba(var(--spice-rgb-text), .2);\n}\n\n.bookmark-progress__bar {\n    --progress: 0;\n    width: calc(var(--progress) * 100%);\n    height: 4px;\n    background-color: var(--spice-text);\n}\n\n.bookmark-progress__time {\n    padding-left: 5px;\n    color: var(--spice-subtext);\n}\n`;\n\n\t\tconst menu = document.createElement(\"ul\");\n\t\tmenu.id = \"bookmark-menu\";\n\t\tmenu.className = \"main-contextMenu-menu\";\n\t\tmenu.onclick = (e) => e.stopPropagation();\n\n\t\tcontainer.append(style, menu);\n\n\t\treturn { container, menu };\n\t}\n\n\t/**\n\t * Handle Link click event when item context is a playlist\n\t */\n\tasync function onLinkClick(info) {\n\t\tif (info.context?.startsWith(\"/\")) {\n\t\t\tSpicetify.Platform.History.push(info.context);\n\t\t\treturn;\n\t\t}\n\t\tconst url = Spicetify.URI.fromString(info.uri).toURLPath(true);\n\t\tSpicetify.Platform.History.push(url);\n\t}\n\n\tfunction onPlayClick(info) {\n\t\tlet uri = info.uri;\n\t\tconst options = {};\n\t\tif (info.time) {\n\t\t\toptions.seekTo = info.time;\n\t\t}\n\t\tif (info.context?.startsWith(\"/\")) {\n\t\t\turi = URI.fromString(info.context).toURI();\n\t\t\tif (uri !== info.uri) {\n\t\t\t\toptions.skipTo = {};\n\t\t\t\toptions.skipTo.uid = info.context.split(\"?uid=\", 2)[1];\n\t\t\t\toptions.skipTo.uri = info.uri;\n\t\t\t}\n\t\t}\n\n\t\tSpicetify.Player.playUri(uri, {}, options);\n\t}\n\n\tconst fetchAlbum = async (uri) => {\n\t\tconst { getAlbum } = Spicetify.GraphQL.Definitions;\n\t\tconst { data } = await Spicetify.GraphQL.Request(getAlbum, {\n\t\t\turi,\n\t\t\tlocale: Spicetify.Locale.getLocale(),\n\t\t\toffset: 0,\n\t\t\tlimit: 10,\n\t\t});\n\t\tconst res = data.albumUnion;\n\t\treturn {\n\t\t\turi,\n\t\t\ttitle: res.name,\n\t\t\tdescription: \"Album\",\n\t\t\timageUrl: res.coverArt.sources.reduce((prev, curr) => (prev.width > curr.width ? prev : curr)).url,\n\t\t};\n\t};\n\n\tconst fetchShow = async (uri) => {\n\t\tconst base62 = uri.split(\":\")[2];\n\t\tconst res = await CosmosAsync.get(`sp://core-show/v1/shows/${base62}?responseFormat=protobufJson`, {\n\t\t\tpolicy: { list: { index: true } },\n\t\t});\n\t\treturn {\n\t\t\turi,\n\t\t\ttitle: res.header.showMetadata.name,\n\t\t\tdescription: \"Podcast\",\n\t\t\timageUrl: res.header.showMetadata.covers.standardLink,\n\t\t};\n\t};\n\n\tconst fetchArtist = async (uri) => {\n\t\tconst { queryArtistOverview } = Spicetify.GraphQL.Definitions;\n\t\tconst { data } = await Spicetify.GraphQL.Request(queryArtistOverview, {\n\t\t\turi,\n\t\t\tlocale: Spicetify.Locale.getLocale(),\n\t\t\tincludePrerelease: false,\n\t\t});\n\t\tconst res = data.artistUnion;\n\t\treturn {\n\t\t\turi,\n\t\t\ttitle: res.profile.name,\n\t\t\tdescription: \"Artist\",\n\t\t\timageUrl:\n\t\t\t\tres.visuals.avatarImage?.sources.reduce((prev, curr) => (prev.width > curr.width ? prev : curr)).url ||\n\t\t\t\tres.visuals.headerImage?.sources[0].url,\n\t\t};\n\t};\n\n\tconst fetchTrack = async (uri, uid, context) => {\n\t\tconst base62 = uri.split(\":\")[2];\n\t\tconst res = await CosmosAsync.get(`https://api.spotify.com/v1/tracks/${base62}`);\n\t\tlet newContext;\n\t\tif (context && uid && Spicetify.URI.isPlaylistV1OrV2(context)) {\n\t\t\tnewContext = `${Spicetify.URI.fromString(context).toURLPath(true)}?uid=${uid}`;\n\t\t}\n\t\treturn {\n\t\t\turi,\n\t\t\ttitle: res.name,\n\t\t\tdescription: res.artists[0].name,\n\t\t\timageUrl: res.album.images[0].url,\n\t\t\tcontext: newContext ?? context,\n\t\t};\n\t};\n\n\tconst fetchEpisode = async (uri) => {\n\t\tconst base62 = uri.split(\":\")[2];\n\t\tconst res = await CosmosAsync.get(`https://api.spotify.com/v1/episodes/${base62}`);\n\t\tconsole.log(res);\n\t\treturn {\n\t\t\turi,\n\t\t\ttitle: res.name,\n\t\t\tdescription: `${res.show.name} episode`,\n\t\t\timageUrl: res.show.images[0].url,\n\t\t};\n\t};\n\n\tconst fetchPlaylist = async (uri) => {\n\t\tconst res = await Spicetify.CosmosAsync.get(`sp://core-playlist/v1/playlist/${uri}/metadata`, {\n\t\t\tpolicy: { picture: true, name: true },\n\t\t});\n\t\treturn {\n\t\t\turi,\n\t\t\ttitle: res.metadata.name,\n\t\t\tdescription: \"Playlist\",\n\t\t\timageUrl: res.metadata.picture,\n\t\t};\n\t};\n\n\tnew Spicetify.ContextMenu.Item(\n\t\t\"Bookmark\",\n\t\tasync ([uri], [uid] = [], context = undefined) => {\n\t\t\tconst type = uri.split(\":\")[1];\n\t\t\tlet meta;\n\t\t\tswitch (type) {\n\t\t\t\tcase Spicetify.URI.Type.TRACK:\n\t\t\t\t\tmeta = await fetchTrack(uri, uid, context);\n\t\t\t\t\tbreak;\n\t\t\t\tcase Spicetify.URI.Type.ALBUM:\n\t\t\t\t\tmeta = await fetchAlbum(uri);\n\t\t\t\t\tbreak;\n\t\t\t\tcase Spicetify.URI.Type.ARTIST:\n\t\t\t\t\tmeta = await fetchArtist(uri);\n\t\t\t\t\tbreak;\n\t\t\t\tcase Spicetify.URI.Type.SHOW:\n\t\t\t\t\tmeta = await fetchShow(uri);\n\t\t\t\t\tbreak;\n\t\t\t\tcase Spicetify.URI.Type.EPISODE:\n\t\t\t\t\tmeta = await fetchEpisode(uri);\n\t\t\t\t\tbreak;\n\t\t\t\tcase Spicetify.URI.Type.PLAYLIST:\n\t\t\t\tcase Spicetify.URI.Type.PLAYLIST_V2:\n\t\t\t\t\tmeta = await fetchPlaylist(uri);\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t\tLIST.addToStorage(meta);\n\t\t},\n\t\t([uri]) => {\n\t\t\tconst type = uri.split(\":\")[1];\n\t\t\tswitch (type) {\n\t\t\t\tcase Spicetify.URI.Type.TRACK:\n\t\t\t\tcase Spicetify.URI.Type.ALBUM:\n\t\t\t\tcase Spicetify.URI.Type.ARTIST:\n\t\t\t\tcase Spicetify.URI.Type.SHOW:\n\t\t\t\tcase Spicetify.URI.Type.EPISODE:\n\t\t\t\tcase Spicetify.URI.Type.PLAYLIST:\n\t\t\t\tcase Spicetify.URI.Type.PLAYLIST_V2:\n\t\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t},\n\t\t`<svg role=\"img\" height=\"16\" width=\"16\" viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M 13.350175,0.37457282 C 9.7802043,0.37457282 6.2102339,0.37457282 2.6402636,0.37457282 2.1901173,0.43000784 2.3537108,0.94911284 2.3229329,1.2621688 2.3229329,5.9446788 2.3229329,10.62721 2.3229329,15.309742 2.4084662,15.861041 2.9630936,15.536253 3.1614158,15.248148 4.7726941,13.696623 6.3839408,12.145098 7.9952191,10.593573 9.7069009,12.241789 11.418583,13.890005 13.130265,15.53822 13.626697,15.863325 13.724086,15.200771 13.667506,14.853516 13.667506,10.132999 13.667506,5.4124518 13.667506,0.69190384 13.671726,0.52196684 13.520105,0.37034182 13.350175,0.37457282 Z M 13.032844,14.563698 C 11.426929,13.017345 9.8210448,11.470993 8.2151293,9.9246401 7.8614008,9.6568761 7.6107412,10.12789 7.3645243,10.320193 5.8955371,11.734694 4.4265815,13.149196 2.9575943,14.563698 2.9575943,10.045543 2.9575943,5.5273888 2.9575943,1.0092338 6.3160002,1.0092338 9.674438,1.0092338 13.032844,1.0092338 13.032844,5.5273888 13.032844,10.045543 13.032844,14.563698 Z\"></path></svg>`\n\t).register();\n})();\n"
  },
  {
    "path": "Extensions/fullAppDisplay.js",
    "content": "// NAME: Full App Display\n// AUTHOR: khanhas\n// VERSION: 1.0\n// DESCRIPTION: Fancy artwork and track status display.\n\n/// <reference path=\"../globals.d.ts\" />\n(function FullAppDisplay() {\n\tif (!Spicetify.Keyboard || !Spicetify.React || !Spicetify.ReactDOM) {\n\t\tsetTimeout(FullAppDisplay, 200);\n\t\treturn;\n\t}\n\n\tconst { React: react, ReactDOM: reactDOM } = Spicetify;\n\tconst { useState, useEffect, useRef } = react;\n\n\tconst CONFIG = getConfig();\n\tlet updateVisual;\n\n\tconst style = document.createElement(\"style\");\n\tconst styleBase = `\n#full-app-display {\n    display: none;\n    position: fixed;\n    width: 100%;\n    height: 100%;\n    cursor: default;\n    left: 0;\n    top: 0;\n}\n#full-app-display.hide-cursor {\n\tcursor: none;\n}\n#fad-header {\n    position: fixed;\n    width: 100%;\n    height: 80px;\n    -webkit-app-region: drag;\n}\n#fad-body {\n    height: 100vh;\n}\n#fad-foreground {\n    position: relative;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    transform: scale(var(--fad-scale));\n}\n#fad-art-image {\n    position: relative;\n    width: 100%;\n    height: 100%;\n    padding-bottom: 100%;\n    border-radius: 15px;\n    background-size: cover;\n}\n#fad-art-inner {\n    position: absolute;\n    left: 3%;\n    bottom: 0;\n    width: 94%;\n    height: 94%;\n    z-index: -1;\n    backface-visibility: hidden;\n    transform: translateZ(0);\n    filter: blur(6px);\n    backdrop-filter: blur(6px);\n    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);\n}\n#fad-art-overlay {\n    display: none;\n}\n#fad-art:hover #fad-art-overlay {\n    position: absolute;\n    width: 100%;\n    height: 100%;\n    display: flex;\n    align-content: center;\n    align-items: center;\n    justify-content: center;\n    border-radius: 15px;\n    backdrop-filter: brightness(0.75);\n}\n#fad-heart {\n    background-color: transparent;\n    border: 0;\n    color: #fff;\n    padding: 0 5px;\n    cursor: pointer;\n}\n#fad-progress-container {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    flex-grow: 1;\n    gap: 10px;\n}\n#fad-progress {\n    width: 100%;\n    height: 6px;\n    border-radius: 6px;\n    background-color: #ffffff50;\n    flex-grow: 1;\n    min-width: 150px;\n}\n#fad-progress:hover #fad-progress-inner {\n    background-color: var(--spice-button);\n}\n#fad-progress:hover #fad-thumb {\n    visibility: visible;\n}\n#fad-progress-inner {\n    width: var(--progress-width);\n    height: 100%;\n    border-radius: 6px;\n    background-color: #ffffff;\n    box-shadow: 4px 0 12px rgba(0, 0, 0, 0.8);\n\tposition: relative;\n}\n#fad-thumb {\n    position: absolute;\n    top: -3px;\n    right: -6px;\n    width: 12px;\n    height: 12px;\n    border-radius: 50%;\n    background-color: #ffffff;\n    cursor: pointer;\n    visibility: hidden;\n}\n#fad-background {\n    position: absolute;\n    left: 0;\n    top: 0;\n    width: 100%;\n    height: 100%;\n    z-index: -2;\n}\nbody.fad-activated #full-app-display {\n    display: block\n}\n.fad-background-fade {\n    transition: background-image 1s linear;\n}\nbody.video-full-screen.video-full-screen--hide-ui {\n    cursor: auto;\n}\n#fad-controls button {\n    background-color: transparent;\n    border: 0;\n    color: currentColor;\n    padding: 0 5px;\n    cursor: pointer;\n}\n#fad-controls button svg {\n    vertical-align: middle;\n}\n#fad-elapsed, #fad-duration {\n    font-variant-numeric: tabular-nums;\n}\n#fad-artist svg, #fad-album svg, #fad-release-date svg {\n    display: inline-block;\n}\n::-webkit-scrollbar {\n    width: 8px;\n}\n`;\n\n\tconst styleChoices = [\n\t\t`\n#fad-foreground {\n    flex-direction: row;\n    text-align: left;\n}\n#fad-art {\n    width: calc(100vw - 840px);\n    min-width: 200px;\n    max-width: 340px;\n}\n#fad-details {\n    padding-left: 50px;\n    line-height: initial;\n    max-width: 70%;\n    color: #FFFFFF;\n}\n#fad-title {\n    font-size: 87px;\n    font-weight: var(--glue-font-weight-black);\n}\n#fad-artist, #fad-album, #fad-release-date {\n    font-size: 54px;\n    font-weight: var(--glue-font-weight-medium);\n}\n#fad-artist svg, #fad-album svg, #fad-release-date svg {\n    margin-right: 5px;\n}\n#fad-status {\n    display: flex;\n    align-items: center;\n    flex-wrap: wrap;\n    gap: 10px;\n}\n#fad-status.active {\n    margin-top: 20px;\n}\n#fad-controls {\n    display: flex;\n    margin: 0 auto;\n}`,\n\t\t`\n#fad-art {\n    width: calc(100vh - 400px);\n    max-width: 340px;\n}\n#fad-foreground {\n    flex-direction: column;\n    text-align: center;\n}\n#fad-details {\n    padding-top: 50px;\n    line-height: initial;\n    max-width: 70%;\n    color: #FFFFFF;\n}\n#fad-title {\n    font-size: 54px;\n    font-weight: var(--glue-font-weight-black);\n}\n#fad-artist, #fad-album, #fad-release-date {\n    font-size: 33px;\n    font-weight: var(--glue-font-weight-medium);\n}\n#fad-artist svg, #fad-album svg, #fad-release-date svg {\n    width: 25px;\n    height: 25px;\n    margin-right: 5px;\n}\n#fad-status {\n    display: flex;\n    min-width: 400px;\n    max-width: 400px;\n    align-items: center;\n    flex-direction: column;\n}\n#fad-status.active {\n    margin: 20px auto 0;\n}\n#fad-controls {\n    margin-top: 20px;\n    order: 2\n}\n#fad-progress-container {\n    width: 100%;\n}`,\n\t];\n\n\tconst lyricsPlusBase = `\n#fad-body {\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n}\n#fad-foreground {\n    padding: 0 50px 0 100px;\n    width: 50vw;\n}\n#fad-lyrics-plus-container {\n    position: relative;\n    width: 50vw;\n}\n`;\n\tconst lyricsPlusStyleChoices = [\n\t\t`\n#fad-title {\n    font-size: 54px;\n}\n#fad-art {\n    max-width: 210px;\n}`,\n\t\t\"\",\n\t];\n\tupdateStyle();\n\n\tconst DisplayIcon = ({ icon, size }) => {\n\t\treturn react.createElement(\"svg\", {\n\t\t\twidth: size,\n\t\t\theight: size,\n\t\t\tviewBox: \"0 0 16 16\",\n\t\t\tfill: \"currentColor\",\n\t\t\tdangerouslySetInnerHTML: {\n\t\t\t\t__html: icon,\n\t\t\t},\n\t\t});\n\t};\n\n\tconst SubInfo = ({ text, id, icon }) => {\n\t\treturn react.createElement(\n\t\t\t\"div\",\n\t\t\t{\n\t\t\t\tid,\n\t\t\t},\n\t\t\tCONFIG.icons && react.createElement(DisplayIcon, { icon, size: 35 }),\n\t\t\treact.createElement(\"span\", null, text)\n\t\t);\n\t};\n\n\tconst ButtonIcon = ({ icon, onClick }) => {\n\t\treturn react.createElement(\n\t\t\t\"button\",\n\t\t\t{\n\t\t\t\tonClick,\n\t\t\t},\n\t\t\treact.createElement(DisplayIcon, { icon, size: 20 })\n\t\t);\n\t};\n\n\tconst ProgressBar = () => {\n\t\tconst [progress, setProgress] = useState(Spicetify.Player.getProgress());\n\t\tconst duration = Spicetify.Platform.PlayerAPI._state.duration;\n\n\t\tconst progressDivRef = useRef(null);\n\t\tconst [isDragging, setIsDragging] = useState(false);\n\n\t\tuseEffect(() => {\n\t\t\tif (isDragging) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst update = ({ data }) => setProgress(data);\n\t\t\tSpicetify.Player.addEventListener(\"onprogress\", update);\n\t\t\treturn () => Spicetify.Player.removeEventListener(\"onprogress\", update);\n\t\t}, [isDragging]);\n\n\t\t// Handle click on progress bar to set progress\n\t\tconst handleClick = (e) => {\n\t\t\tconst container = progressDivRef.current;\n\t\t\tif (isDragging || !container) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst containerRect = container.getBoundingClientRect();\n\t\t\tconst clickX = e.clientX - containerRect.left;\n\t\t\tconst newProgress = (clickX / containerRect.width) * duration;\n\t\t\tSpicetify.Player.seek(newProgress);\n\t\t\tsetProgress(newProgress);\n\t\t};\n\n\t\t// Handle dragging functionality\n\t\tconst handleMouseDown = () => setIsDragging(true);\n\t\tconst handleMouseMove = (e) => {\n\t\t\tconst container = progressDivRef.current;\n\t\t\tif (!isDragging || !container) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst containerRect = container.getBoundingClientRect();\n\t\t\tconst offsetX = e.clientX - containerRect.left;\n\t\t\tconst newProgress = (offsetX / containerRect.width) * duration;\n\t\t\tsetProgress(newProgress);\n\t\t};\n\t\tconst handleMouseUp = () => {\n\t\t\tif (!isDragging) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tSpicetify.Player.seek(progress);\n\t\t\tsetIsDragging(false);\n\t\t};\n\n\t\t// Attach mousemove and mouseup listeners when dragging starts\n\t\tuseEffect(() => {\n\t\t\tif (isDragging) {\n\t\t\t\twindow.addEventListener(\"mousemove\", handleMouseMove);\n\t\t\t\twindow.addEventListener(\"mouseup\", handleMouseUp);\n\t\t\t} else {\n\t\t\t\twindow.removeEventListener(\"mousemove\", handleMouseMove);\n\t\t\t\twindow.removeEventListener(\"mouseup\", handleMouseUp);\n\t\t\t}\n\n\t\t\treturn () => {\n\t\t\t\twindow.removeEventListener(\"mousemove\", handleMouseMove);\n\t\t\t\twindow.removeEventListener(\"mouseup\", handleMouseUp);\n\t\t\t};\n\t\t}, [isDragging]);\n\n\t\t// Calculate the thumb position\n\t\tconst thumbPosition = (progress / duration) * 100;\n\n\t\treturn react.createElement(\n\t\t\t\"div\",\n\t\t\t{ id: \"fad-progress-container\" },\n\t\t\treact.createElement(\"span\", { id: \"fad-elapsed\" }, Spicetify.Player.formatTime(progress)),\n\t\t\treact.createElement(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tid: \"fad-progress\",\n\t\t\t\t\tref: progressDivRef,\n\t\t\t\t\tonClick: handleClick,\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\"--progress-width\": `${thumbPosition}%`,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\treact.createElement(\n\t\t\t\t\t\"div\",\n\t\t\t\t\t{ id: \"fad-progress-inner\" },\n\t\t\t\t\treact.createElement(\"div\", {\n\t\t\t\t\t\tid: \"fad-thumb\",\n\t\t\t\t\t\tonMouseDown: handleMouseDown,\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t\t),\n\t\t\treact.createElement(\"span\", { id: \"fad-duration\" }, Spicetify.Player.formatTime(duration))\n\t\t);\n\t};\n\n\tconst PlayerControls = () => {\n\t\tconst [value, setValue] = useState(Spicetify.Player.isPlaying());\n\t\tuseEffect(() => {\n\t\t\tconst update = ({ data }) => setValue(!data.isPaused);\n\t\t\tSpicetify.Player.addEventListener(\"onplaypause\", update);\n\t\t\treturn () => Spicetify.Player.removeEventListener(\"onplaypause\", update);\n\t\t});\n\t\treturn react.createElement(\n\t\t\t\"div\",\n\t\t\t{ id: \"fad-controls\" },\n\t\t\treact.createElement(ButtonIcon, {\n\t\t\t\ticon: Spicetify.SVGIcons[\"skip-back\"],\n\t\t\t\tonClick: Spicetify.Player.back,\n\t\t\t}),\n\t\t\treact.createElement(ButtonIcon, {\n\t\t\t\ticon: Spicetify.SVGIcons[value ? \"pause\" : \"play\"],\n\t\t\t\tonClick: Spicetify.Player.togglePlay,\n\t\t\t}),\n\t\t\treact.createElement(ButtonIcon, {\n\t\t\t\ticon: Spicetify.SVGIcons[\"skip-forward\"],\n\t\t\t\tonClick: Spicetify.Player.next,\n\t\t\t})\n\t\t);\n\t};\n\n\tclass FAD extends react.Component {\n\t\tconstructor(props) {\n\t\t\tsuper(props);\n\n\t\t\tthis.state = {\n\t\t\t\ttitle: \"\",\n\t\t\t\tartist: \"\",\n\t\t\t\talbum: \"\",\n\t\t\t\treleaseDate: \"\",\n\t\t\t\tcover: \"\",\n\t\t\t\theart: Spicetify.Player.getHeart(),\n\t\t\t};\n\t\t\tthis.currTrackImg = new Image();\n\t\t\tthis.nextTrackImg = new Image();\n\t\t\tthis.mousetrap = new Spicetify.Mousetrap();\n\t\t}\n\n\t\tasync getAlbumDate(uri) {\n\t\t\tconst id = uri.replace(\"spotify:album:\", \"\");\n\n\t\t\tconst albumInfo = await Spicetify.CosmosAsync.get(`https://api.spotify.com/v1/albums/${id}`);\n\n\t\t\tconst albumDate = new Date(albumInfo.release_date);\n\t\t\treturn albumDate.toLocaleString(\"default\", {\n\t\t\t\tyear: \"numeric\",\n\t\t\t\tmonth: \"short\",\n\t\t\t\tday: \"numeric\",\n\t\t\t});\n\t\t}\n\n\t\tasync fetchInfo() {\n\t\t\tconst meta = Spicetify.Player.data.item.metadata;\n\n\t\t\t// prepare title\n\t\t\tlet rawTitle = meta.title;\n\t\t\tif (CONFIG.trimTitle) {\n\t\t\t\trawTitle = rawTitle\n\t\t\t\t\t.replace(/\\(.+?\\)/g, \"\")\n\t\t\t\t\t.replace(/\\[.+?\\]/g, \"\")\n\t\t\t\t\t.replace(/\\s-\\s.+?$/, \"\")\n\t\t\t\t\t.replace(/,.+?$/, \"\")\n\t\t\t\t\t.trim();\n\t\t\t}\n\n\t\t\t// prepare artist\n\t\t\tlet artistName;\n\t\t\tif (CONFIG.showAllArtists) {\n\t\t\t\tartistName = Object.keys(meta)\n\t\t\t\t\t.filter((key) => key.startsWith(\"artist_name\"))\n\t\t\t\t\t.sort()\n\t\t\t\t\t.map((key) => meta[key])\n\t\t\t\t\t.join(\", \");\n\t\t\t} else {\n\t\t\t\tartistName = meta.artist_name;\n\t\t\t}\n\n\t\t\t// prepare release date\n\t\t\tlet releaseDate;\n\t\t\tif (CONFIG.showReleaseDate) {\n\t\t\t\tconst albumURI = meta.album_uri;\n\t\t\t\tif (albumURI?.startsWith(\"spotify:album:\")) {\n\t\t\t\t\treleaseDate = await this.getAlbumDate(albumURI);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// prepare album\n\t\t\tconst albumText = meta.album_title || \"\";\n\n\t\t\tif (meta.image_xlarge_url === this.currTrackImg.src) {\n\t\t\t\tthis.setState({\n\t\t\t\t\ttitle: rawTitle || \"\",\n\t\t\t\t\tartist: artistName || \"\",\n\t\t\t\t\talbum: albumText || \"\",\n\t\t\t\t\treleaseDate: releaseDate || \"\",\n\t\t\t\t\theart: Spicetify.Player.getHeart(),\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// TODO: Pre-load next track\n\t\t\t// Wait until next track image is downloaded then update UI text and images\n\t\t\tconst previousImg = this.currTrackImg.cloneNode();\n\t\t\tthis.currTrackImg.src = meta.image_xlarge_url;\n\t\t\tthis.currTrackImg.onload = () => {\n\t\t\t\tconst bgImage = `url(\"${this.currTrackImg.src}\")`;\n\n\t\t\t\tthis.animateCanvas(previousImg, this.currTrackImg);\n\t\t\t\tthis.setState({\n\t\t\t\t\ttitle: rawTitle || \"\",\n\t\t\t\t\tartist: artistName || \"\",\n\t\t\t\t\talbum: albumText || \"\",\n\t\t\t\t\treleaseDate: releaseDate || \"\",\n\t\t\t\t\tcover: bgImage,\n\t\t\t\t\theart: Spicetify.Player.getHeart(),\n\t\t\t\t});\n\t\t\t};\n\t\t\tthis.currTrackImg.onerror = () => {\n\t\t\t\t// Placeholder\n\t\t\t\tthis.currTrackImg.src =\n\t\t\t\t\t\"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCI+CiAgPHJlY3Qgc3R5bGU9ImZpbGw6I2ZmZmZmZiIgd2lkdGg9IjQwIiBoZWlnaHQ9IjQwIiB4PSIwIiB5PSIwIiAvPgogIDxwYXRoIGZpbGw9IiNCM0IzQjMiIGQ9Ik0yNi4yNSAxNi4xNjJMMjEuMDA1IDEzLjEzNEwyMS4wMTIgMjIuNTA2QzIwLjU5NCAyMi4xOTIgMjAuMDgxIDIxLjk5OSAxOS41MTkgMjEuOTk5QzE4LjE0MSAyMS45OTkgMTcuMDE5IDIzLjEyMSAxNy4wMTkgMjQuNDk5QzE3LjAxOSAyNS44NzggMTguMTQxIDI2Ljk5OSAxOS41MTkgMjYuOTk5QzIwLjg5NyAyNi45OTkgMjIuMDE5IDI1Ljg3OCAyMi4wMTkgMjQuNDk5QzIyLjAxOSAyNC40MjIgMjIuMDA2IDE0Ljg2NyAyMi4wMDYgMTQuODY3TDI1Ljc1IDE3LjAyOUwyNi4yNSAxNi4xNjJaTTE5LjUxOSAyNS45OThDMTguNjkyIDI1Ljk5OCAxOC4wMTkgMjUuMzI1IDE4LjAxOSAyNC40OThDMTguMDE5IDIzLjY3MSAxOC42OTIgMjIuOTk4IDE5LjUxOSAyMi45OThDMjAuMzQ2IDIyLjk5OCAyMS4wMTkgMjMuNjcxIDIxLjAxOSAyNC40OThDMjEuMDE5IDI1LjMyNSAyMC4zNDYgMjUuOTk4IDE5LjUxOSAyNS45OThaIi8+Cjwvc3ZnPgo=\";\n\t\t\t};\n\t\t}\n\n\t\tanimateCanvas(prevImg, nextImg) {\n\t\t\tconst { innerWidth: width, innerHeight: height } = window;\n\t\t\tthis.back.width = width;\n\t\t\tthis.back.height = height;\n\n\t\t\tconst ctx = this.back.getContext(\"2d\");\n\t\t\tctx.imageSmoothingEnabled = false;\n\t\t\tctx.filter = \"blur(30px) brightness(0.6)\";\n\t\t\tconst blur = 30;\n\n\t\t\tconst x = -blur * 2;\n\n\t\t\tlet y;\n\t\t\tlet dim;\n\n\t\t\tif (width > height) {\n\t\t\t\tdim = width;\n\t\t\t\ty = x - (width - height) / 2;\n\t\t\t} else {\n\t\t\t\tdim = height;\n\t\t\t\ty = x;\n\t\t\t}\n\n\t\t\tconst size = dim + 4 * blur;\n\n\t\t\tif (!CONFIG.enableFade) {\n\t\t\t\tctx.globalAlpha = 1;\n\t\t\t\tctx.drawImage(nextImg, x, y, size, size);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet factor = 0.0;\n\t\t\tconst animate = () => {\n\t\t\t\tctx.globalAlpha = 1;\n\t\t\t\tctx.drawImage(prevImg, x, y, size, size);\n\t\t\t\tctx.globalAlpha = Math.sin((Math.PI / 2) * factor);\n\t\t\t\tctx.drawImage(nextImg, x, y, size, size);\n\n\t\t\t\tif (factor < 1.0) {\n\t\t\t\t\tfactor += 0.016;\n\t\t\t\t\trequestAnimationFrame(animate);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\trequestAnimationFrame(animate);\n\t\t}\n\n\t\tcomponentDidMount() {\n\t\t\tthis.updateInfo = this.fetchInfo.bind(this);\n\t\t\tSpicetify.Player.addEventListener(\"songchange\", this.updateInfo);\n\t\t\tthis.updateInfo();\n\n\t\t\tupdateVisual = () => {\n\t\t\t\tupdateStyle();\n\t\t\t\tthis.fetchInfo();\n\t\t\t};\n\n\t\t\tthis.onQueueChange = async (queueData) => {\n\t\t\t\tconst queue = queueData.data;\n\t\t\t\tlet nextTrack;\n\t\t\t\tif (queue.queued.length) {\n\t\t\t\t\tnextTrack = queue.queued[0];\n\t\t\t\t} else {\n\t\t\t\t\tnextTrack = queue.nextUp[0];\n\t\t\t\t}\n\t\t\t\tthis.nextTrackImg.src = nextTrack.metadata.image_xlarge_url;\n\t\t\t};\n\n\t\t\tconst scaleLimit = { min: 0.1, max: 4, step: 0.05 };\n\t\t\tthis.onScaleChange = (event) => {\n\t\t\t\tif (!event.ctrlKey) return;\n\t\t\t\tconst dir = event.deltaY < 0 ? 1 : -1;\n\t\t\t\tlet temp = (CONFIG.scale || 1) + dir * scaleLimit.step;\n\t\t\t\tif (temp < scaleLimit.min) {\n\t\t\t\t\ttemp = scaleLimit.min;\n\t\t\t\t} else if (temp > scaleLimit.max) {\n\t\t\t\t\ttemp = scaleLimit.max;\n\t\t\t\t}\n\t\t\t\tCONFIG.scale = temp;\n\t\t\t\tsaveConfig();\n\t\t\t\tupdateVisual();\n\t\t\t};\n\n\t\t\tSpicetify.Platform.PlayerAPI._events.addListener(\"queue_update\", this.onQueueChange);\n\t\t\tthis.mousetrap.bind(\"esc\", deactivate);\n\t\t\twindow.dispatchEvent(new Event(\"fad-request\"));\n\t\t}\n\n\t\tcomponentWillUnmount() {\n\t\t\tSpicetify.Player.removeEventListener(\"songchange\", this.updateInfo);\n\t\t\tSpicetify.Platform.PlayerAPI._events.removeListener(\"queue_update\", this.onQueueChange);\n\t\t\tthis.mousetrap.unbind(\"esc\");\n\t\t}\n\n\t\trender() {\n\t\t\treturn react.createElement(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tid: \"full-app-display\",\n\t\t\t\t\tclassName: \"Video VideoPlayer--fullscreen VideoPlayer--landscape\",\n\t\t\t\t\tonDoubleClick: deactivate,\n\t\t\t\t\tonContextMenu: openConfig,\n\t\t\t\t},\n\t\t\t\treact.createElement(\"canvas\", {\n\t\t\t\t\tid: \"fad-background\",\n\t\t\t\t\tref: (el) => {\n\t\t\t\t\t\tthis.back = el;\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t\treact.createElement(\"div\", { id: \"fad-header\" }),\n\t\t\t\treact.createElement(\n\t\t\t\t\t\"div\",\n\t\t\t\t\t{ id: \"fad-body\" },\n\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tid: \"fad-foreground\",\n\t\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\t\t\"--fad-scale\": CONFIG.scale || 1,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tref: (el) => {\n\t\t\t\t\t\t\t\tif (!el) return;\n\t\t\t\t\t\t\t\tel.onmousewheel = this.onScaleChange;\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t\t{ id: \"fad-art\" },\n\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tid: \"fad-art-image\",\n\t\t\t\t\t\t\t\t\tclassName: CONFIG.enableFade && \"fad-background-fade\",\n\t\t\t\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\t\t\t\tbackgroundImage: this.state.cover,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tid: \"fad-art-overlay\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\t\t\"button\",\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tid: \"fad-heart\",\n\t\t\t\t\t\t\t\t\t\t\tonClick: () => {\n\t\t\t\t\t\t\t\t\t\t\t\tSpicetify.Player.toggleHeart();\n\t\t\t\t\t\t\t\t\t\t\t\tthis.setState({ heart: !this.state.heart });\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\treact.createElement(DisplayIcon, {\n\t\t\t\t\t\t\t\t\t\t\ticon: Spicetify.SVGIcons[this.state.heart ? \"heart-active\" : \"heart\"],\n\t\t\t\t\t\t\t\t\t\t\tsize: 50,\n\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\treact.createElement(\"div\", {\n\t\t\t\t\t\t\t\t\tid: \"fad-art-inner\",\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t),\n\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t\t{ id: \"fad-details\" },\n\t\t\t\t\t\t\treact.createElement(\"div\", { id: \"fad-title\" }, this.state.title),\n\t\t\t\t\t\t\treact.createElement(SubInfo, {\n\t\t\t\t\t\t\t\tid: \"fad-artist\",\n\t\t\t\t\t\t\t\ttext: this.state.artist,\n\t\t\t\t\t\t\t\ticon: Spicetify.SVGIcons.artist,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\tCONFIG.showAlbum &&\n\t\t\t\t\t\t\t\treact.createElement(SubInfo, {\n\t\t\t\t\t\t\t\t\tid: \"fad-album\",\n\t\t\t\t\t\t\t\t\ttext: this.state.album,\n\t\t\t\t\t\t\t\t\ticon: Spicetify.SVGIcons.album,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\tCONFIG.showReleaseDate &&\n\t\t\t\t\t\t\t\treact.createElement(SubInfo, {\n\t\t\t\t\t\t\t\t\tid: \"fad-release-date\",\n\t\t\t\t\t\t\t\t\ttext: this.state.releaseDate,\n\t\t\t\t\t\t\t\t\ticon: Spicetify.SVGIcons.clock,\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\treact.createElement(\n\t\t\t\t\t\t\t\t\"div\",\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tid: \"fad-status\",\n\t\t\t\t\t\t\t\t\tclassName: (CONFIG.enableControl || CONFIG.enableProgress) && \"active\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tCONFIG.enableControl && react.createElement(PlayerControls),\n\t\t\t\t\t\t\t\tCONFIG.enableProgress && react.createElement(ProgressBar)\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t)\n\t\t\t\t\t),\n\t\t\t\t\tCONFIG.lyricsPlus &&\n\t\t\t\t\t\treact.createElement(\"div\", {\n\t\t\t\t\t\t\tid: \"fad-lyrics-plus-container\",\n\t\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\t\t\"--lyrics-color-active\": \"#ffffff\",\n\t\t\t\t\t\t\t\t\"--lyrics-color-inactive\": \"#ffffff50\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t})\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\t}\n\n\tconst classes = [\"video\", \"video-full-screen\", \"video-full-window\", \"video-full-screen--hide-ui\", \"fad-activated\"];\n\n\tconst container = document.createElement(\"div\");\n\tcontainer.id = \"fad-main\";\n\tlet lastApp;\n\tlet cursorTimeout;\n\tlet fad;\n\n\tasync function toggleFullscreen() {\n\t\tif (CONFIG.enableFullscreen) {\n\t\t\tawait document.documentElement.requestFullscreen();\n\t\t\ttoggleCursor(false);\n\t\t} else if (document.webkitIsFullScreen) {\n\t\t\tawait document.exitFullscreen();\n\t\t\ttoggleCursor(true);\n\t\t}\n\t}\n\n\tfunction eventListener() {\n\t\tshowCursor();\n\t\tcursorTimeout = setTimeout(hideCursor, 2000);\n\t}\n\n\tfunction showCursor() {\n\t\tfad.classList.remove(\"hide-cursor\");\n\t\tclearTimeout(cursorTimeout);\n\t}\n\n\tfunction hideCursor() {\n\t\tfad.classList.add(\"hide-cursor\");\n\t}\n\n\tfunction toggleCursor(show = true) {\n\t\tfad = document.getElementById(\"full-app-display\");\n\n\t\tif (!fad) {\n\t\t\tsetTimeout(toggleCursor, 300, show);\n\t\t\treturn;\n\t\t}\n\n\t\tif (show) {\n\t\t\tdocument.removeEventListener(\"mousemove\", eventListener);\n\t\t\tshowCursor();\n\t\t} else {\n\t\t\tcursorTimeout = setTimeout(hideCursor, 2000);\n\t\t\tdocument.addEventListener(\"mousemove\", eventListener);\n\t\t}\n\t}\n\n\tasync function activate() {\n\t\tif (!Spicetify.Player.data) return;\n\n\t\tawait toggleFullscreen();\n\n\t\tdocument.body.classList.add(...classes);\n\t\tdocument.body.append(style, container);\n\t\treactDOM.render(react.createElement(FAD), container);\n\n\t\trequestLyricsPlus();\n\t}\n\n\tfunction deactivate() {\n\t\tif (CONFIG.enableFullscreen || document.webkitIsFullScreen) {\n\t\t\tdocument.exitFullscreen();\n\t\t}\n\t\ttoggleCursor(true);\n\t\tdocument.body.classList.remove(...classes);\n\t\treactDOM.unmountComponentAtNode(container);\n\t\tstyle.remove();\n\t\tcontainer.remove();\n\t\twindow.dispatchEvent(new Event(\"fad-request\"));\n\n\t\tif (lastApp && lastApp !== \"/lyrics-plus\") {\n\t\t\tSpicetify.Platform.History.push(lastApp);\n\t\t}\n\t}\n\n\tfunction toggleFad() {\n\t\tif (document.body.classList.contains(\"fad-activated\")) {\n\t\t\tdeactivate();\n\t\t} else {\n\t\t\tactivate();\n\t\t}\n\t}\n\n\tfunction updateStyle() {\n\t\tstyle.innerHTML =\n\t\t\tstyleBase +\n\t\t\tstyleChoices[CONFIG.vertical ? 1 : 0] +\n\t\t\t(checkLyricsPlus() && CONFIG.lyricsPlus ? lyricsPlusBase + lyricsPlusStyleChoices[CONFIG.vertical ? 1 : 0] : \"\");\n\t}\n\n\tfunction checkLyricsPlus() {\n\t\treturn Spicetify.Config?.custom_apps?.includes(\"lyrics-plus\") || !!document.querySelector(\"a[href='/lyrics-plus']\");\n\t}\n\n\tfunction requestLyricsPlus() {\n\t\tif (CONFIG.lyricsPlus && checkLyricsPlus()) {\n\t\t\tlastApp = Spicetify.Platform.History.location.pathname;\n\t\t\tif (lastApp !== \"/lyrics-plus\") {\n\t\t\t\tSpicetify.Platform.History.push(\"/lyrics-plus\");\n\t\t\t}\n\t\t}\n\t\twindow.dispatchEvent(new Event(\"fad-request\"));\n\t}\n\n\tfunction getConfig() {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(Spicetify.LocalStorage.get(\"full-app-display-config\") || \"{}\");\n\t\t\tif (parsed && typeof parsed === \"object\") {\n\t\t\t\treturn parsed;\n\t\t\t}\n\t\t\tthrow \"\";\n\t\t} catch {\n\t\t\tSpicetify.LocalStorage.set(\"full-app-display-config\", \"{}\");\n\t\t\treturn {};\n\t\t}\n\t}\n\n\tfunction saveConfig() {\n\t\tSpicetify.LocalStorage.set(\"full-app-display-config\", JSON.stringify(CONFIG));\n\t}\n\n\tconst ConfigItem = ({ name, field, func, disabled = false }) => {\n\t\tconst [value, setValue] = useState(CONFIG[field]);\n\t\treturn react.createElement(\n\t\t\t\"div\",\n\t\t\t{ className: \"setting-row\" },\n\t\t\treact.createElement(\"label\", { className: \"col description\" }, name),\n\t\t\treact.createElement(\n\t\t\t\t\"div\",\n\t\t\t\t{ className: \"col action\" },\n\t\t\t\treact.createElement(\n\t\t\t\t\t\"button\",\n\t\t\t\t\t{\n\t\t\t\t\t\tclassName: `switch${value ? \"\" : \" disabled\"}`,\n\t\t\t\t\t\tdisabled,\n\t\t\t\t\t\tonClick: () => {\n\t\t\t\t\t\t\tconst state = !value;\n\t\t\t\t\t\t\tCONFIG[field] = state;\n\t\t\t\t\t\t\tsetValue(state);\n\t\t\t\t\t\t\tsaveConfig();\n\t\t\t\t\t\t\tfunc();\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\treact.createElement(DisplayIcon, {\n\t\t\t\t\t\ticon: Spicetify.SVGIcons.check,\n\t\t\t\t\t\tsize: 16,\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t\t)\n\t\t);\n\t};\n\n\tfunction openConfig(event) {\n\t\tevent.preventDefault();\n\t\tconst style = react.createElement(\"style\", {\n\t\t\tdangerouslySetInnerHTML: {\n\t\t\t\t__html: `\n.setting-row::after {\n    content: \"\";\n    display: table;\n    clear: both;\n}\n.setting-row .col {\n    display: flex;\n    padding: 10px 0;\n    align-items: center;\n}\n.setting-row .col.description {\n    float: left;\n    padding-right: 15px;\n}\n.setting-row .col.action {\n    float: right;\n    text-align: right;\n}\nbutton.switch {\n    align-items: center;\n    border: 0px;\n    border-radius: 50%;\n    background-color: rgba(var(--spice-rgb-shadow), .7);\n    color: var(--spice-text);\n    cursor: pointer;\n    display: flex;\n    margin-inline-start: 12px;\n    padding: 8px;\n}\nbutton.switch.disabled,\nbutton.switch[disabled] {\n    color: rgba(var(--spice-rgb-text), .3);\n}\n`,\n\t\t\t},\n\t\t});\n\t\tconst configContainer = react.createElement(\n\t\t\t\"div\",\n\t\t\tnull,\n\t\t\tstyle,\n\t\t\treact.createElement(ConfigItem, {\n\t\t\t\tname: checkLyricsPlus() ? \"Enable Lyrics Plus integration\" : \"Lyrics Plus not applied\",\n\t\t\t\tfield: \"lyricsPlus\",\n\t\t\t\tfunc: () => {\n\t\t\t\t\tupdateVisual();\n\t\t\t\t\trequestLyricsPlus();\n\t\t\t\t},\n\t\t\t\tdisabled: !checkLyricsPlus(),\n\t\t\t}),\n\t\t\treact.createElement(ConfigItem, {\n\t\t\t\tname: \"Enable progress bar\",\n\t\t\t\tfield: \"enableProgress\",\n\t\t\t\tfunc: updateVisual,\n\t\t\t}),\n\t\t\treact.createElement(ConfigItem, {\n\t\t\t\tname: \"Enable controls\",\n\t\t\t\tfield: \"enableControl\",\n\t\t\t\tfunc: updateVisual,\n\t\t\t}),\n\t\t\treact.createElement(ConfigItem, {\n\t\t\t\tname: \"Trim title\",\n\t\t\t\tfield: \"trimTitle\",\n\t\t\t\tfunc: updateVisual,\n\t\t\t}),\n\t\t\treact.createElement(ConfigItem, {\n\t\t\t\tname: \"Show album\",\n\t\t\t\tfield: \"showAlbum\",\n\t\t\t\tfunc: updateVisual,\n\t\t\t}),\n\t\t\treact.createElement(ConfigItem, {\n\t\t\t\tname: \"Show all artists\",\n\t\t\t\tfield: \"showAllArtists\",\n\t\t\t\tfunc: updateVisual,\n\t\t\t}),\n\t\t\treact.createElement(ConfigItem, {\n\t\t\t\tname: \"Show release date\",\n\t\t\t\tfield: \"showReleaseDate\",\n\t\t\t\tfunc: updateVisual,\n\t\t\t}),\n\t\t\treact.createElement(ConfigItem, {\n\t\t\t\tname: \"Show icons\",\n\t\t\t\tfield: \"icons\",\n\t\t\t\tfunc: updateVisual,\n\t\t\t}),\n\t\t\treact.createElement(ConfigItem, {\n\t\t\t\tname: \"Vertical mode\",\n\t\t\t\tfield: \"vertical\",\n\t\t\t\tfunc: updateStyle,\n\t\t\t}),\n\t\t\treact.createElement(ConfigItem, {\n\t\t\t\tname: \"Enable fullscreen\",\n\t\t\t\tfield: \"enableFullscreen\",\n\t\t\t\tfunc: toggleFullscreen,\n\t\t\t}),\n\t\t\treact.createElement(ConfigItem, {\n\t\t\t\tname: \"Enable song change animation\",\n\t\t\t\tfield: \"enableFade\",\n\t\t\t\tfunc: updateVisual,\n\t\t\t})\n\t\t);\n\t\tSpicetify.PopupModal.display({\n\t\t\ttitle: \"Full App Display\",\n\t\t\tcontent: configContainer,\n\t\t});\n\t}\n\n\t// Add activator on top bar\n\tnew Spicetify.Topbar.Button(\n\t\t\"Full App Display\",\n\t\t`<svg role=\"img\" height=\"16\" width=\"16\" viewBox=\"0 0 16 16\" fill=\"currentColor\">${Spicetify.SVGIcons.projector}</svg>`,\n\t\tactivate\n\t);\n\n\tSpicetify.Mousetrap.bind(\"f11\", toggleFad);\n})();\n"
  },
  {
    "path": "Extensions/keyboardShortcut.js",
    "content": "// NAME: Keyboard Shortcut\n// AUTHOR: khanhas, OhItsTom\n// DESCRIPTION: Register a few more keybinds to support keyboard-driven navigation in Spotify client.\n\n/// <reference path=\"../globals.d.ts\" />\n\n(function KeyboardShortcut() {\n\tif (!Spicetify.Mousetrap) {\n\t\tsetTimeout(KeyboardShortcut, 1000);\n\t\treturn;\n\t}\n\n\t// Variables / Conditions\n\tconst vim = new VimBind();\n\tconst SCROLL_STEP = 25;\n\n\t/**\n\t * Binds a keyboard shortcut using Mousetrap.\n\t * @param {string} key - The Mousetrap keybind.\n\t * @param {boolean | undefined} staticCondition - A static condition.\n\t * @param {(event: KeyboardEvent) => void} callback - Callback function for the event.\n\t */\n\tconst binds = {\n\t\t// Shutdown Spotify using Ctrl+Q\n\t\t\"ctrl+q\": {\n\t\t\tcallback: () =>\n\t\t\t\tSpicetify.CosmosAsync.post(\"sp://esperanto/spotify.desktop.lifecycle_esperanto.proto.DesktopLifecycle/Shutdown\") &&\n\t\t\t\tSpicetify.CosmosAsync.post(\"sp://desktop/v1/shutdown\"),\n\t\t},\n\n\t\t// Rotate through sidebar items using Ctrl+Tab and Ctrl+Shift+Tab\n\t\t\"ctrl+tab\": { callback: () => rotateSidebar(1) },\n\t\t\"ctrl+shift+tab\": { callback: () => rotateSidebar(-1) },\n\n\t\t// Focus on the app content before scrolling using Shift+PageUp and Shift+PageDown\n\t\t\"shift+pageup\": { callback: () => focusOnApp() },\n\t\t\"shift+pagedown\": { callback: () => focusOnApp() },\n\n\t\t// Scroll actions using 'j' and 'k' keys\n\t\tj: { callback: () => createScrollCallback(SCROLL_STEP) },\n\t\tk: { callback: () => createScrollCallback(-SCROLL_STEP) },\n\n\t\t// Scroll to the top ('gg') or bottom ('Shift+g') of the page\n\t\t\"g g\": { callback: () => scrollToPosition(0) },\n\t\t\"shift+g\": { callback: () => scrollToPosition(1) },\n\n\t\t// Shift + H and Shift + L to go back and forward page\n\t\t\"shift+h\": { callback: () => Spicetify.Platform.History.goBack() },\n\t\t\"shift+l\": { callback: () => Spicetify.Platform.History.goForward() },\n\n\t\t// M to Like/Unlike track\n\t\tm: { callback: () => Spicetify.Player.toggleHeart() },\n\n\t\t// Forward Slash to open search page\n\t\t\"/\": { callback: () => Spicetify.Platform.History.replace(\"/search\") },\n\n\t\t// CTRL + Arrow Left Next and CTRL + Arrow Right  Previous Song\n\t\t\"ctrl+left\": { callback: () => Spicetify.Player.back() },\n\t\t\"ctrl+right\": { callback: () => Spicetify.Player.next() },\n\n\t\t// CTRL + Arrow Up Increase Volume CTRL + Arrow Down Decrease Volume\n\t\t\"ctrl+up\": { callback: () => Spicetify.Player.setVolume(Spicetify.Player.getVolume() + 0.05) },\n\t\t\"ctrl+down\": { callback: () => Spicetify.Player.setVolume(Spicetify.Player.getVolume() - 0.05) },\n\n\t\t// Activate Vim mode and set cancel key to 'ESCAPE'\n\t\tf: {\n\t\t\tcallback: (event) => {\n\t\t\t\tvim.activate(event);\n\t\t\t\tvim.setCancelKey(\"ESCAPE\");\n\t\t\t},\n\t\t},\n\t};\n\n\t// Bind all the keys\n\tfor (const [key, { staticCondition, callback }] of Object.entries(binds)) {\n\t\tif (typeof staticCondition === \"undefined\" || staticCondition) {\n\t\t\tSpicetify.Mousetrap.bind(key, (event) => {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tif (!vim.isActive) {\n\t\t\t\t\tcallback(event);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\n\t// re-render vim on window resize & prevent mouse event while active\n\twindow.addEventListener(\n\t\t\"resize\",\n\t\t(event) => {\n\t\t\tif (vim.isActive) {\n\t\t\t\tvim.activate();\n\t\t\t}\n\t\t},\n\t\ttrue\n\t);\n\n\twindow.addEventListener(\n\t\t\"mousedown\",\n\t\t(event) => {\n\t\t\tif (vim.isActive) {\n\t\t\t\tevent.stopPropagation();\n\t\t\t}\n\t\t},\n\t\ttrue\n\t);\n\n\t// Functions\n\tfunction focusOnApp() {\n\t\treturn document.querySelector(\n\t\t\t\".Root__main-view .os-viewport, .Root__main-view .main-view-container > .main-view-container__scroll-node:not([data-overlayscrollbars-initialize]), .Root__main-view .main-view-container__scroll-node > [data-overlayscrollbars-viewport]\"\n\t\t);\n\t}\n\n\tfunction createScrollCallback(step) {\n\t\tconst app = focusOnApp();\n\t\tif (app) {\n\t\t\tconst scrollInterval = setInterval(() => {\n\t\t\t\tapp.scrollTop += step;\n\t\t\t}, 10);\n\t\t\tdocument.addEventListener(\"keyup\", () => {\n\t\t\t\tclearInterval(scrollInterval);\n\t\t\t});\n\t\t}\n\t}\n\n\tfunction scrollToPosition(position) {\n\t\tconst app = focusOnApp();\n\t\tapp.scroll(0, position === 0 ? 0 : app.scrollHeight);\n\t}\n\n\t/**\n\t * @returns {number | undefined}\n\t * @param {NodeListOf<Element>} allItems\n\t */\n\tfunction findActiveIndex(allItems) {\n\t\tconst activeLink = document.querySelector(\".main-yourLibraryX-navLinkActive\");\n\t\tconst historyURI = Spicetify.Platform.History.location.pathname.replace(/^\\//, \"spotify:\").replace(/\\//g, \":\");\n\t\tconst activePage = document.querySelector(`[aria-describedby=\"onClickHint${historyURI}\"]`);\n\n\t\tif (!activeLink && !activePage) {\n\t\t\treturn -1;\n\t\t}\n\n\t\tlet index = 0;\n\t\tfor (const item of allItems) {\n\t\t\tif (item === activeLink || item === activePage) {\n\t\t\t\treturn index;\n\t\t\t}\n\n\t\t\tindex++;\n\t\t}\n\t}\n\n\t/**\n\t *\n\t * @param {1 | -1} direction\n\t */\n\tfunction rotateSidebar(direction) {\n\t\tconst allItems = document.querySelectorAll(\n\t\t\t\"#spicetify-sticky-list .main-yourLibraryX-navLink, .main-yourLibraryX-listItem > div:not(:has([data-skip-in-keyboard-nav])) > div:first-child\"\n\t\t);\n\t\tconst maxIndex = allItems.length - 1;\n\n\t\tlet index = findActiveIndex(allItems) + direction;\n\t\tif (index < 0) index = maxIndex;\n\t\telse if (index > maxIndex) index = 0;\n\n\t\tallItems[index].click();\n\t}\n})();\n\nfunction VimBind() {\n\tconst elementQuery = [\"[href]\", \"button\", \".main-trackList-trackListRow\", \"[role='button']\"].join(\",\");\n\n\tconst keyList = \"qwertasdfgzxcvyuiophjklbnm\".split(\"\");\n\n\tconst lastKeyIndex = keyList.length - 1;\n\n\tthis.isActive = false;\n\n\tconst vimOverlay = document.createElement(\"div\");\n\tconst baseOverlay = document.createElement(\"div\");\n\tconst tippyOverlay = document.createElement(\"div\");\n\tvimOverlay.id = \"vim-overlay\";\n\tbaseOverlay.id = \"base-overlay\";\n\ttippyOverlay.id = \"tippy-overlay\";\n\tvimOverlay.style.position = baseOverlay.style.position = tippyOverlay.style.position = \"absolute\";\n\tvimOverlay.style.width = baseOverlay.style.width = tippyOverlay.style.width = \"100%\";\n\tvimOverlay.style.height = baseOverlay.style.height = tippyOverlay.style.height = \"100%\";\n\tbaseOverlay.style.zIndex = \"9999\";\n\ttippyOverlay.style.zIndex = \"10000\";\n\tvimOverlay.style.display = \"none\";\n\tvimOverlay.innerHTML = `<style>\n.vim-key {\n    position: fixed;\n    padding: 3px 6px;\n    background-color: var(--spice-button-disabled);\n    border-radius: 3px;\n    border: solid 2px var(--spice-text);\n    color: var(--spice-text);\n    text-transform: lowercase;\n    line-height: normal;\n    font-size: 14px;\n    font-weight: 500;\n}\n</style>`;\n\tvimOverlay.append(baseOverlay);\n\tvimOverlay.append(tippyOverlay);\n\tdocument.body.append(vimOverlay);\n\n\tconst mousetrap = new Spicetify.Mousetrap(document);\n\tmousetrap.bind(keyList, listenToKeys.bind(this), \"keypress\");\n\t// Pause mousetrap event emitter\n\tconst orgStopCallback = mousetrap.stopCallback;\n\tmousetrap.stopCallback = () => true;\n\n\t/**\n\t *\n\t * @param {KeyboardEvent} event\n\t */\n\tthis.activate = function (event) {\n\t\tvimOverlay.style.display = \"block\";\n\n\t\tconst vimkey = getVims();\n\t\tif (vimkey.length > 0) {\n\t\t\tfor (const e of vimkey) {\n\t\t\t\te.remove();\n\t\t\t}\n\t\t}\n\n\t\tlet firstKey = 0;\n\t\tlet secondKey = 0;\n\n\t\tfor (const e of getLinks()) {\n\t\t\tconst computed = window.getComputedStyle(e);\n\t\t\tif (computed.display === \"none\" || computed.visibility === \"hidden\" || computed.opacity === \"0\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst bound = e.getBoundingClientRect();\n\t\t\tconst owner = document.body;\n\n\t\t\tlet top = bound.top;\n\t\t\tlet left = bound.left;\n\n\t\t\tif (\n\t\t\t\tbound.bottom > owner.clientHeight ||\n\t\t\t\tbound.left > owner.clientWidth ||\n\t\t\t\tbound.right < 0 ||\n\t\t\t\tbound.top < 0 ||\n\t\t\t\tbound.width === 0 ||\n\t\t\t\tbound.height === 0\n\t\t\t) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Exclude certain elements from the centering calculation\n\t\t\tif (e.parentNode.role !== \"row\") {\n\t\t\t\ttop = top + bound.height / 2 - 15;\n\t\t\t\tleft = left + bound.width / 2 - 15;\n\t\t\t}\n\n\t\t\t// Append the key to the correct overlay\n\t\t\tif (e.tagName === \"BUTTON\" && e.parentNode.tagName === \"LI\") {\n\t\t\t\ttippyOverlay.append(createKey(e, keyList[firstKey] + keyList[secondKey], top, left));\n\t\t\t} else {\n\t\t\t\tbaseOverlay.append(createKey(e, keyList[firstKey] + keyList[secondKey], top, left));\n\t\t\t}\n\n\t\t\tsecondKey++;\n\t\t\tif (secondKey > lastKeyIndex) {\n\t\t\t\tsecondKey = 0;\n\t\t\t\tfirstKey++;\n\t\t\t}\n\t\t}\n\n\t\tthis.isActive = true;\n\t\tsetTimeout(() => {\n\t\t\tmousetrap.stopCallback = orgStopCallback.bind(mousetrap);\n\t\t}, 100);\n\t};\n\n\t/**\n\t *\n\t * @param {KeyboardEvent} event\n\t */\n\tthis.deactivate = function (event) {\n\t\tmousetrap.stopCallback = () => true;\n\t\tthis.isActive = false;\n\t\tvimOverlay.style.display = \"none\";\n\t\tfor (const e of getVims()) {\n\t\t\te.remove();\n\t\t}\n\t};\n\n\tfunction getLinks() {\n\t\tconst elements = Array.from(document.querySelectorAll(elementQuery));\n\t\treturn elements;\n\t}\n\n\tfunction getVims() {\n\t\treturn Array.from(vimOverlay.getElementsByClassName(\"vim-key\"));\n\t}\n\n\t/**\n\t * @param {KeyboardEvent} event\n\t */\n\tfunction listenToKeys(event) {\n\t\tif (!this.isActive) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst vimkey = getVims();\n\n\t\tif (vimkey.length === 0) {\n\t\t\tthis.deactivate(event);\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const div of vimkey) {\n\t\t\tconst text = div.innerText.toLowerCase();\n\t\t\tif (text[0] !== event.key) {\n\t\t\t\tdiv.remove();\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst newText = text.slice(1);\n\t\t\tif (newText.length === 0) {\n\t\t\t\tinteract(div.target);\n\t\t\t\tthis.deactivate(event);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tdiv.innerText = newText;\n\t\t}\n\n\t\tif (baseOverlay.childNodes.length === 0 && tippyOverlay.childNodes.length === 0) {\n\t\t\tthis.deactivate(event);\n\t\t}\n\t}\n\n\t/**\n\t * @param {HTMLElement} element\n\t */\n\tfunction interact(element) {\n\t\t// Hover on contextmenu dropdown list items\n\t\tif (element.tagName === \"BUTTON\" && element.parentNode.tagName === \"LI\" && element.ariaExpanded !== null) {\n\t\t\tconst event = new MouseEvent(\"mouseover\", {\n\t\t\t\tview: window,\n\t\t\t\tbubbles: true,\n\t\t\t\tcancelable: true,\n\t\t\t});\n\n\t\t\telement.dispatchEvent(event);\n\t\t\treturn;\n\t\t}\n\n\t\tif (element.hasAttribute(\"href\") || element.tagName === \"BUTTON\" || element.role === \"button\" || element.parentNode.role === \"row\") {\n\t\t\telement.click();\n\t\t\treturn;\n\t\t}\n\n\t\tconst findButton = element.querySelector(`button[data-ta-id=\"play-button\"]`) || element.querySelector(`button[data-button=\"play\"]`);\n\t\tif (findButton instanceof HTMLButtonElement) {\n\t\t\tfindButton.click();\n\t\t\treturn;\n\t\t}\n\t\talert(\"Let me know where you found this button, please. I can't click this for you without that information.\");\n\t\treturn;\n\t}\n\n\t/**\n\t * @param {Element} target\n\t * @param {string} key\n\t * @param {string | number} top\n\t * @param {string | number} left\n\t */\n\tfunction createKey(target, key, top, left) {\n\t\tconst div = document.createElement(\"span\");\n\t\tdiv.classList.add(\"vim-key\");\n\t\tdiv.innerText = key;\n\t\tdiv.style.top = `${top}px`;\n\t\tdiv.style.left = `${left}px`;\n\t\tdiv.target = target;\n\t\treturn div;\n\t}\n\n\t/**\n\t *\n\t * @param {Spicetify.Keyboard.ValidKey} key\n\t */\n\tthis.setCancelKey = function (key) {\n\t\tmousetrap.bind(Spicetify.Keyboard.KEYS[key], this.deactivate.bind(this));\n\t};\n\n\treturn this;\n}\n"
  },
  {
    "path": "Extensions/loopyLoop.js",
    "content": "// NAME: Loopy loop\n// AUTHOR: khanhas\n// VERSION: 0.1\n// DESCRIPTION: Simple tool to help you practice hitting that note right. Right click at process bar to open up menu.\n\n/// <reference path=\"../globals.d.ts\" />\n\n(function LoopyLoop() {\n\tconst playbackBar = document.querySelector(\".playback-bar\");\n\tconst progressContainer = playbackBar?.querySelector(\".playback-progressbar-container\");\n\tconst rangeInput = progressContainer?.querySelector('input[type=\"range\"]');\n\tconst bar = rangeInput?.closest(\"label\")?.nextElementSibling;\n\tif (!(bar && Spicetify.Player)) {\n\t\tsetTimeout(LoopyLoop, 100);\n\t\treturn;\n\t}\n\n\tconst style = document.createElement(\"style\");\n\tstyle.innerHTML = `\n#loopy-loop-start, #loopy-loop-end {\n    position: absolute;\n    font-weight: bolder;\n    font-size: 15px;\n    top: -7px;\n}\n`;\n\n\tconst startMark = document.createElement(\"div\");\n\tstartMark.id = \"loopy-loop-start\";\n\tstartMark.innerText = \"[\";\n\tconst endMark = document.createElement(\"div\");\n\tendMark.id = \"loopy-loop-end\";\n\tendMark.innerText = \"]\";\n\tstartMark.style.position = endMark.style.position = \"absolute\";\n\tstartMark.hidden = endMark.hidden = true;\n\n\tbar.append(style);\n\tbar.append(startMark);\n\tbar.append(endMark);\n\n\tlet start = null;\n\tlet end = null;\n\tlet mouseOnBarPercent = 0.0;\n\n\tfunction drawOnBar() {\n\t\tif (start === null && end === null) {\n\t\t\tstartMark.hidden = endMark.hidden = true;\n\t\t\treturn;\n\t\t}\n\t\tstartMark.hidden = endMark.hidden = false;\n\t\tstartMark.style.left = `${start * 100}%`;\n\t\tendMark.style.left = `${end * 100}%`;\n\t}\n\tfunction reset() {\n\t\tstart = null;\n\t\tend = null;\n\t\tdrawOnBar();\n\t}\n\n\tlet debouncing = 0;\n\tSpicetify.Player.addEventListener(\"onprogress\", (event) => {\n\t\tif (start != null && end != null) {\n\t\t\tif (debouncing) {\n\t\t\t\tif (event.timeStamp - debouncing > 1000) {\n\t\t\t\t\tdebouncing = 0;\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst percent = Spicetify.Player.getProgressPercent();\n\t\t\tif (percent > end || percent < start) {\n\t\t\t\tdebouncing = event.timeStamp;\n\t\t\t\tSpicetify.Player.seek(start);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t});\n\n\tSpicetify.Player.addEventListener(\"songchange\", reset);\n\n\tfunction createMenuItem(title, callback) {\n\t\tconst item = document.createElement(\"li\");\n\t\titem.setAttribute(\"role\", \"menuitem\");\n\t\tconst button = document.createElement(\"button\");\n\t\tbutton.classList.add(\"main-contextMenu-menuItemButton\");\n\t\tbutton.textContent = title;\n\t\tbutton.onclick = () => {\n\t\t\tcontextMenu.hidden = true;\n\t\t\tcallback?.();\n\t\t};\n\t\titem.append(button);\n\t\treturn item;\n\t}\n\n\tconst startBtn = createMenuItem(\"Set start\", () => {\n\t\tstart = mouseOnBarPercent;\n\t\tif (end === null || start > end) {\n\t\t\tend = 0.99;\n\t\t}\n\t\tdrawOnBar();\n\t});\n\tconst endBtn = createMenuItem(\"Set end\", () => {\n\t\tend = mouseOnBarPercent;\n\t\tif (start === null || end < start) {\n\t\t\tstart = 0;\n\t\t}\n\t\tdrawOnBar();\n\t});\n\tconst resetBtn = createMenuItem(\"Reset\", reset);\n\n\tconst contextMenu = document.createElement(\"div\");\n\tcontextMenu.id = \"loopy-context-menu\";\n\tcontextMenu.innerHTML = `<ul tabindex=\"0\" class=\"main-contextMenu-menu\"></ul>`;\n\tcontextMenu.style.position = \"absolute\";\n\tcontextMenu.firstElementChild.append(startBtn, endBtn, resetBtn);\n\tdocument.body.append(contextMenu);\n\tconst { height: contextMenuHeight } = contextMenu.getBoundingClientRect();\n\tcontextMenu.hidden = true;\n\twindow.addEventListener(\"click\", () => {\n\t\tcontextMenu.hidden = true;\n\t});\n\n\tprogressContainer.oncontextmenu = (event) => {\n\t\tconst { x, width } = bar.getBoundingClientRect();\n\t\tmouseOnBarPercent = Math.max(0, Math.min(1, (event.clientX - x) / width));\n\t\tcontextMenu.style.transform = `translate(${event.clientX}px,${event.clientY - contextMenuHeight}px)`;\n\t\tcontextMenu.hidden = false;\n\t\tevent.preventDefault();\n\t};\n})();\n"
  },
  {
    "path": "Extensions/popupLyrics.js",
    "content": "// NAME: Popup Lyrics\n// AUTHOR: khanhas\n//         Netease API parser and UI from https://github.com/mantou132/Spotify-Lyrics\n// DESCRIPTION: Pop lyrics up\n\n/// <reference path=\"../globals.d.ts\" />\n\nif (!navigator.serviceWorker) {\n\t// Worker code\n\t// When Spotify client is minimised, requestAnimationFrame does not call our tick function\n\t// setTimeout and setInterval are also throttled at 1 second.\n\t// Offload setInterval to a Worker to consistently call tick function.\n\tlet num = null;\n\t// biome-ignore lint/suspicious/noGlobalAssign: <explanation>\n\tonmessage = (event) => {\n\t\tif (event.data === \"popup-lyric-request-update\") {\n\t\t\tconsole.warn(\"popup-lyric-request-update\");\n\t\t\tnum = setInterval(() => postMessage(\"popup-lyric-update-ui\"), 16.66);\n\t\t} else if (event.data === \"popup-lyric-stop-update\") {\n\t\t\tclearInterval(num);\n\t\t\tpostMessage(\"popup-lyric-update-ui\");\n\t\t\tnum = null;\n\t\t}\n\t};\n} else {\n\tPopupLyrics();\n}\n\nlet CACHE = {};\n\nfunction PopupLyrics() {\n\tconst { Player, CosmosAsync, LocalStorage, ContextMenu } = Spicetify;\n\n\tif (!CosmosAsync || !LocalStorage || !ContextMenu) {\n\t\tsetTimeout(PopupLyrics, 500);\n\t\treturn;\n\t}\n\n\tconst worker = new Worker(\"./extensions/popupLyrics.js\");\n\tworker.onmessage = (event) => {\n\t\tif (event.data === \"popup-lyric-update-ui\") {\n\t\t\ttick(userConfigs);\n\t\t}\n\t};\n\n\tlet workerIsRunning = null;\n\tdocument.addEventListener(\"visibilitychange\", (e) => {\n\t\tif (e.target.hidden) {\n\t\t\tif (!workerIsRunning) {\n\t\t\t\tworker.postMessage(\"popup-lyric-request-update\");\n\t\t\t\tworkerIsRunning = true;\n\t\t\t}\n\t\t} else {\n\t\t\tif (workerIsRunning) {\n\t\t\t\tworker.postMessage(\"popup-lyric-stop-update\");\n\t\t\t\tworkerIsRunning = false;\n\t\t\t}\n\t\t}\n\t});\n\n\tconst LyricUtils = {\n\t\tnormalize(s, emptySymbol = true) {\n\t\t\tconst result = s\n\t\t\t\t.replace(/（/g, \"(\")\n\t\t\t\t.replace(/）/g, \")\")\n\t\t\t\t.replace(/【/g, \"[\")\n\t\t\t\t.replace(/】/g, \"]\")\n\t\t\t\t.replace(/。/g, \". \")\n\t\t\t\t.replace(/；/g, \"; \")\n\t\t\t\t.replace(/：/g, \": \")\n\t\t\t\t.replace(/？/g, \"? \")\n\t\t\t\t.replace(/！/g, \"! \")\n\t\t\t\t.replace(/、|，/g, \", \")\n\t\t\t\t.replace(/‘|’|′|＇/g, \"'\")\n\t\t\t\t.replace(/“|”/g, '\"')\n\t\t\t\t.replace(/〜/g, \"~\")\n\t\t\t\t.replace(/·|・/g, \"•\");\n\t\t\tif (emptySymbol) {\n\t\t\t\tresult.replace(/-/g, \" \").replace(/\\//g, \" \");\n\t\t\t}\n\t\t\treturn result.replace(/\\s+/g, \" \").trim();\n\t\t},\n\n\t\tremoveExtraInfo(s) {\n\t\t\treturn (\n\t\t\t\ts\n\t\t\t\t\t.replace(/-\\s+(feat|with|prod).*/i, \"\")\n\t\t\t\t\t.replace(/(\\(|\\[)(feat|with|prod)\\.?\\s+.*(\\)|\\])$/i, \"\")\n\t\t\t\t\t.replace(/\\s-\\s.*/, \"\")\n\t\t\t\t\t.trim() || s\n\t\t\t);\n\t\t},\n\n\t\tcapitalize(s) {\n\t\t\treturn s.replace(/^(\\w)/, ($1) => $1.toUpperCase());\n\t\t},\n\t};\n\n\tconst LyricProviders = {\n\t\tasync fetchSpotify(info) {\n\t\t\tconst baseURL = \"https://spclient.wg.spotify.com/color-lyrics/v2/track/\";\n\t\t\tconst id = info.uri.split(\":\")[2];\n\t\t\tconst body = await CosmosAsync.get(`${baseURL + id}?format=json&vocalRemoval=false&market=from_token`);\n\n\t\t\tconst lyricsData = body.lyrics;\n\t\t\tif (!lyricsData || lyricsData.syncType !== \"LINE_SYNCED\") {\n\t\t\t\treturn { error: \"No lyrics\" };\n\t\t\t}\n\n\t\t\tconst lines = lyricsData.lines;\n\t\t\tconst lyrics = lines.map((a) => ({\n\t\t\t\tstartTime: a.startTimeMs / 1000,\n\t\t\t\ttext: a.words,\n\t\t\t}));\n\n\t\t\treturn { lyrics };\n\t\t},\n\n\t\tasync fetchMusixmatch(info) {\n\t\t\tconst baseURL =\n\t\t\t\t\"https://apic-desktop.musixmatch.com/ws/1.1/macro.subtitles.get?format=json&namespace=lyrics_synched&subtitle_format=mxm&app_id=web-desktop-app-v1.0&\";\n\n\t\t\tconst durr = info.duration / 1000;\n\n\t\t\tconst params = {\n\t\t\t\tq_album: info.album,\n\t\t\t\tq_artist: info.artist,\n\t\t\t\tq_artists: info.artist,\n\t\t\t\tq_track: info.title,\n\t\t\t\ttrack_spotify_id: info.uri,\n\t\t\t\tq_duration: durr,\n\t\t\t\tf_subtitle_length: Math.floor(durr),\n\t\t\t\tusertoken: userConfigs.services.musixmatch.token,\n\t\t\t};\n\n\t\t\tconst finalURL =\n\t\t\t\tbaseURL +\n\t\t\t\tObject.keys(params)\n\t\t\t\t\t.map((key) => `${key}=${encodeURIComponent(params[key])}`)\n\t\t\t\t\t.join(\"&\");\n\n\t\t\ttry {\n\t\t\t\tlet body = await CosmosAsync.get(finalURL, null, {\n\t\t\t\t\tauthority: \"apic-desktop.musixmatch.com\",\n\t\t\t\t\tcookie: \"x-mxm-token-guid=\",\n\t\t\t\t});\n\n\t\t\t\tbody = body.message.body.macro_calls;\n\n\t\t\t\tif (body[\"matcher.track.get\"].message.header.status_code !== 200) {\n\t\t\t\t\tconst head = body[\"matcher.track.get\"].message.header;\n\t\t\t\t\treturn {\n\t\t\t\t\t\terror: `Requested error: ${head.status_code}: ${head.hint} - ${head.mode}`,\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\tconst meta = body[\"matcher.track.get\"].message.body;\n\t\t\t\tconst hasSynced = meta.track.has_subtitles;\n\t\t\t\tconst isRestricted = body[\"track.lyrics.get\"].message.header.status_code === 200 && body[\"track.lyrics.get\"].message.body.lyrics.restricted;\n\t\t\t\tconst isInstrumental = meta.track.instrumental;\n\n\t\t\t\tif (isRestricted) return { error: \"Unfortunately we're not authorized to show these lyrics.\" };\n\t\t\t\tif (isInstrumental) return { error: \"Instrumental\" };\n\t\t\t\tif (hasSynced) {\n\t\t\t\t\tconst subtitle = body[\"track.subtitles.get\"].message.body.subtitle_list[0].subtitle;\n\n\t\t\t\t\tconst lyrics = JSON.parse(subtitle.subtitle_body).map((line) => ({\n\t\t\t\t\t\ttext: line.text || \"♪\",\n\t\t\t\t\t\tstartTime: line.time.total,\n\t\t\t\t\t}));\n\t\t\t\t\treturn { lyrics };\n\t\t\t\t}\n\n\t\t\t\treturn { error: \"No lyrics\" };\n\t\t\t} catch (err) {\n\t\t\t\treturn { error: err.message };\n\t\t\t}\n\t\t},\n\n\t\tasync fetchNetease(info) {\n\t\t\tconst searchURL = \"https://music.xianqiao.wang/neteaseapiv2/search?limit=10&type=1&keywords=\";\n\t\t\tconst lyricURL = \"https://music.xianqiao.wang/neteaseapiv2/lyric?id=\";\n\t\t\tconst requestHeader = {\n\t\t\t\t\"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0\",\n\t\t\t};\n\n\t\t\tconst cleanTitle = LyricUtils.removeExtraInfo(LyricUtils.normalize(info.title));\n\t\t\tconst finalURL = searchURL + encodeURIComponent(`${cleanTitle} ${info.artist}`);\n\n\t\t\tconst searchResults = await CosmosAsync.get(finalURL, null, requestHeader);\n\t\t\tconst items = searchResults.result.songs;\n\t\t\tif (!items || !items.length) {\n\t\t\t\treturn { error: \"Cannot find track\" };\n\t\t\t}\n\n\t\t\tconst album = LyricUtils.capitalize(info.album);\n\t\t\tconst itemId = items.findIndex((val) => LyricUtils.capitalize(val.album.name) === album || Math.abs(info.duration - val.duration) < 1000);\n\t\t\tif (itemId === -1) return { error: \"Cannot find track\" };\n\n\t\t\tconst meta = await CosmosAsync.get(lyricURL + items[itemId].id, null, requestHeader);\n\t\t\tlet lyricStr = meta.lrc;\n\n\t\t\tif (!lyricStr || !lyricStr.lyric) {\n\t\t\t\treturn { error: \"No lyrics\" };\n\t\t\t}\n\t\t\tlyricStr = lyricStr.lyric;\n\n\t\t\tconst otherInfoKeys = [\n\t\t\t\t\"\\\\s?作?\\\\s*词|\\\\s?作?\\\\s*曲|\\\\s?编\\\\s*曲?|\\\\s?监\\\\s*制?\",\n\t\t\t\t\".*编写|.*和音|.*和声|.*合声|.*提琴|.*录|.*工程|.*工作室|.*设计|.*剪辑|.*制作|.*发行|.*出品|.*后期|.*混音|.*缩混\",\n\t\t\t\t\"原唱|翻唱|题字|文案|海报|古筝|二胡|钢琴|吉他|贝斯|笛子|鼓|弦乐\",\n\t\t\t\t\"lrc|publish|vocal|guitar|program|produce|write|mix\",\n\t\t\t];\n\t\t\tconst otherInfoRegexp = new RegExp(`^(${otherInfoKeys.join(\"|\")}).*(:|：)`, \"i\");\n\n\t\t\tconst lines = lyricStr.split(/\\r?\\n/).map((line) => line.trim());\n\t\t\tlet noLyrics = false;\n\t\t\tconst lyrics = lines\n\t\t\t\t.flatMap((line) => {\n\t\t\t\t\t// [\"[ar:Beyond]\"]\n\t\t\t\t\t// [\"[03:10]\"]\n\t\t\t\t\t// [\"[03:10]\", \"永远高唱我歌\"]\n\t\t\t\t\t// [\"永远高唱我歌\"]\n\t\t\t\t\t// [\"[03:10]\", \"[03:10]\", \"永远高唱我歌\"]\n\t\t\t\t\tconst matchResult = line.match(/(\\[.*?\\])|([^[\\]]+)/g) || [line];\n\t\t\t\t\tif (!matchResult.length || matchResult.length === 1) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst textIndex = matchResult.findIndex((slice) => !slice.endsWith(\"]\"));\n\t\t\t\t\tlet text = \"\";\n\t\t\t\t\tif (textIndex > -1) {\n\t\t\t\t\t\ttext = matchResult.splice(textIndex, 1)[0];\n\t\t\t\t\t\ttext = LyricUtils.capitalize(LyricUtils.normalize(text, false));\n\t\t\t\t\t}\n\t\t\t\t\tif (text === \"纯音乐, 请欣赏\") noLyrics = true;\n\t\t\t\t\treturn matchResult.map((slice) => {\n\t\t\t\t\t\tconst result = {};\n\t\t\t\t\t\tconst matchResult = slice.match(/[^[\\]]+/g);\n\t\t\t\t\t\tconst [key, value] = matchResult[0].split(\":\") || [];\n\t\t\t\t\t\tconst [min, sec] = [Number.parseFloat(key), Number.parseFloat(value)];\n\t\t\t\t\t\tif (!Number.isNaN(min) && !Number.isNaN(sec) && !otherInfoRegexp.test(text)) {\n\t\t\t\t\t\t\tresult.startTime = min * 60 + sec;\n\t\t\t\t\t\t\tresult.text = text || \"♪\";\n\t\t\t\t\t\t\treturn result;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn;\n\t\t\t\t\t});\n\t\t\t\t})\n\t\t\t\t.sort((a, b) => {\n\t\t\t\t\tif (a.startTime === null) {\n\t\t\t\t\t\treturn 0;\n\t\t\t\t\t}\n\t\t\t\t\tif (b.startTime === null) {\n\t\t\t\t\t\treturn 1;\n\t\t\t\t\t}\n\t\t\t\t\treturn a.startTime - b.startTime;\n\t\t\t\t})\n\t\t\t\t.filter(Boolean);\n\n\t\t\tif (noLyrics) {\n\t\t\t\treturn { error: \"No lyrics\" };\n\t\t\t}\n\t\t\tif (!lyrics.length) {\n\t\t\t\treturn { error: \"No synced lyrics\" };\n\t\t\t}\n\n\t\t\treturn { lyrics };\n\t\t},\n\n\t\tasync fetchLrclib(info) {\n\t\t\tconst baseURL = \"https://lrclib.net/api/get\";\n\t\t\tconst durr = info.duration / 1000;\n\t\t\tconst params = {\n\t\t\t\ttrack_name: info.title,\n\t\t\t\tartist_name: info.artist,\n\t\t\t\talbum_name: info.album,\n\t\t\t\tduration: durr,\n\t\t\t};\n\n\t\t\tconst finalURL = `${baseURL}?${Object.keys(params)\n\t\t\t\t.map((key) => `${key}=${encodeURIComponent(params[key])}`)\n\t\t\t\t.join(\"&\")}`;\n\n\t\t\tconst body = await fetch(finalURL, {\n\t\t\t\theaders: {\n\t\t\t\t\t\"x-user-agent\": `spicetify v${Spicetify.Config.version} (https://github.com/spicetify/cli)`,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tif (body.status !== 200) {\n\t\t\t\treturn { error: \"Request error: Track wasn't found\" };\n\t\t\t}\n\n\t\t\tconst meta = await body.json();\n\t\t\tif (meta?.instrumental) {\n\t\t\t\treturn { error: \"Instrumental\" };\n\t\t\t}\n\t\t\tif (!meta?.syncedLyrics) {\n\t\t\t\treturn { error: \"No synced lyrics\" };\n\t\t\t}\n\n\t\t\t// Preprocess lyrics by removing [tags] and empty lines\n\t\t\tconst lines = meta?.syncedLyrics\n\t\t\t\t.replaceAll(/\\[[a-zA-Z]+:.+\\]/g, \"\")\n\t\t\t\t.trim()\n\t\t\t\t.split(\"\\n\");\n\n\t\t\tconst syncedTimestamp = /\\[([0-9:.]+)\\]/;\n\t\t\tconst isSynced = lines[0].match(syncedTimestamp);\n\n\t\t\tconst lyrics = lines.map((line) => {\n\t\t\t\tconst time = line.match(syncedTimestamp)?.[1];\n\t\t\t\tconst lyricContent = line.replace(syncedTimestamp, \"\").trim();\n\t\t\t\tconst lyric = lyricContent.replaceAll(/<([0-9:.]+)>/g, \"\").trim();\n\t\t\t\tconst [min, sec] = time.replace(/\\[\\]<>/, \"\").split(\":\");\n\n\t\t\t\tif (line.trim() !== \"\" && isSynced && time) {\n\t\t\t\t\treturn { text: lyric || \"♪\", startTime: Number(min) * 60 + Number(sec) };\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t});\n\n\t\t\treturn { lyrics };\n\t\t},\n\t};\n\n\tconst userConfigs = {\n\t\tsmooth: boolLocalStorage(\"popup-lyrics:smooth\"),\n\t\tcenterAlign: boolLocalStorage(\"popup-lyrics:center-align\"),\n\t\tshowCover: boolLocalStorage(\"popup-lyrics:show-cover\"),\n\t\tfontSize: Number(LocalStorage.get(\"popup-lyrics:font-size\")),\n\t\tblurSize: Number(LocalStorage.get(\"popup-lyrics:blur-size\")),\n\t\tfontFamily: LocalStorage.get(\"popup-lyrics:font-family\") || \"spotify-circular\",\n\t\tratio: LocalStorage.get(\"popup-lyrics:ratio\") || \"11\",\n\t\tdelay: Number(LocalStorage.get(\"popup-lyrics:delay\")),\n\t\tservices: {\n\t\t\tnetease: {\n\t\t\t\ton: boolLocalStorage(\"popup-lyrics:services:netease:on\"),\n\t\t\t\tcall: LyricProviders.fetchNetease,\n\t\t\t\tdesc: \"Crowdsourced lyrics provider ran by Chinese developers and users.\",\n\t\t\t},\n\t\t\tmusixmatch: {\n\t\t\t\ton: boolLocalStorage(\"popup-lyrics:services:musixmatch:on\"),\n\t\t\t\tcall: LyricProviders.fetchMusixmatch,\n\t\t\t\tdesc: \"Fully compatible with Spotify. Requires a token that can be retrieved from the official Musixmatch app. If you have problems with retrieving lyrics, try refreshing the token by clicking <code>Refresh Token</code> button.\",\n\t\t\t\ttoken: LocalStorage.get(\"popup-lyrics:services:musixmatch:token\") || \"2005218b74f939209bda92cb633c7380612e14cb7fe92dcd6a780f\",\n\t\t\t},\n\t\t\tspotify: {\n\t\t\t\ton: boolLocalStorage(\"popup-lyrics:services:spotify:on\"),\n\t\t\t\tcall: LyricProviders.fetchSpotify,\n\t\t\t\tdesc: \"Lyrics sourced from official Spotify API.\",\n\t\t\t},\n\t\t\tlrclib: {\n\t\t\t\ton: boolLocalStorage(\"popup-lyrics:services:lrclib:on\"),\n\t\t\t\tcall: LyricProviders.fetchLrclib,\n\t\t\t\tdesc: \"Lyrics sourced from lrclib.net. Supports both synced and unsynced lyrics. LRCLIB is a free and open-source lyrics provider.\",\n\t\t\t},\n\t\t},\n\t\tservicesOrder: [],\n\t};\n\n\tuserConfigs.fontSize = userConfigs.fontSize ? Number(userConfigs.fontSize) : 46;\n\ttry {\n\t\tconst rawServicesOrder = LocalStorage.get(\"popup-lyrics:services-order\");\n\t\tuserConfigs.servicesOrder = JSON.parse(rawServicesOrder);\n\n\t\tif (!Array.isArray(userConfigs.servicesOrder)) throw \"\";\n\n\t\tuserConfigs.servicesOrder = userConfigs.servicesOrder.filter((s) => userConfigs.services[s]); // Remove obsoleted services\n\n\t\tconst allServices = Object.keys(userConfigs.services);\n\t\tif (userConfigs.servicesOrder.length !== allServices.length) {\n\t\t\tfor (const s of allServices) {\n\t\t\t\tif (!userConfigs.servicesOrder.includes(s)) {\n\t\t\t\t\tuserConfigs.servicesOrder.push(s);\n\t\t\t\t}\n\t\t\t}\n\t\t\tLocalStorage.set(\"popup-lyrics:services-order\", JSON.stringify(userConfigs.servicesOrder));\n\t\t}\n\t} catch {\n\t\tuserConfigs.servicesOrder = Object.keys(userConfigs.services);\n\t\tLocalStorage.set(\"popup-lyrics:services-order\", JSON.stringify(userConfigs.servicesOrder));\n\t}\n\n\tconst lyricVideo = document.createElement(\"video\");\n\tlyricVideo.muted = true;\n\tlyricVideo.width = 600;\n\tswitch (userConfigs.ratio) {\n\t\tcase \"43\":\n\t\t\tlyricVideo.height = Math.round((lyricVideo.width * 3) / 4);\n\t\t\tbreak;\n\t\tcase \"169\":\n\t\t\tlyricVideo.height = Math.round((lyricVideo.width * 9) / 16);\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tlyricVideo.height = lyricVideo.width;\n\t\t\tbreak;\n\t}\n\n\tlet lyricVideoIsOpen = false;\n\tlyricVideo.onenterpictureinpicture = () => {\n\t\tlyricVideo.play();\n\t\tlyricVideoIsOpen = true;\n\t\ttick(userConfigs);\n\t\tupdateTrack();\n\t};\n\tlyricVideo.onleavepictureinpicture = () => {\n\t\tlyricVideoIsOpen = false;\n\t};\n\n\tconst lyricCanvas = document.createElement(\"canvas\");\n\tlyricCanvas.width = lyricVideo.width;\n\tlyricCanvas.height = lyricVideo.height;\n\n\tconst lyricCtx = lyricCanvas.getContext(\"2d\");\n\tlyricVideo.srcObject = lyricCanvas.captureStream();\n\tlyricCtx.fillRect(0, 0, 1, 1);\n\tlyricVideo.play();\n\n\tconst button = new Spicetify.Topbar.Button(\"Popup Lyrics\", \"lyrics\", () => {\n\t\tif (!lyricVideoIsOpen) {\n\t\t\tlyricVideo.requestPictureInPicture();\n\t\t} else {\n\t\t\tdocument.exitPictureInPicture();\n\t\t}\n\t});\n\tbutton.element.oncontextmenu = openConfig;\n\n\tconst coverCanvas = document.createElement(\"canvas\");\n\tcoverCanvas.width = lyricVideo.width;\n\tcoverCanvas.height = lyricVideo.width;\n\tconst coverCtx = coverCanvas.getContext(\"2d\");\n\n\tconst largeImage = new Image();\n\tlargeImage.onload = () => {\n\t\tcoverCtx.drawImage(largeImage, 0, 0, coverCtx.canvas.width, coverCtx.canvas.width);\n\t};\n\tuserConfigs.backgroundImage = coverCanvas;\n\n\tlet sharedData = {};\n\n\tPlayer.addEventListener(\"songchange\", () => {\n\t\tupdateTrack();\n\t});\n\n\tasync function updateTrack(refresh = false) {\n\t\tif (!lyricVideoIsOpen) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst meta = Player.data.item.metadata;\n\n\t\tif (!Spicetify.URI.isTrack(Player.data.item.uri) && !Spicetify.URI.isLocalTrack(Player.data.item.uri)) {\n\t\t\treturn;\n\t\t}\n\n\t\tlargeImage.src = meta.image_url;\n\t\tconst info = {\n\t\t\tduration: Number(meta.duration),\n\t\t\talbum: meta.album_title,\n\t\t\tartist: meta.artist_name,\n\t\t\ttitle: meta.title,\n\t\t\turi: Player.data.item.uri,\n\t\t};\n\n\t\tif (CACHE?.[info.uri]?.lyrics?.length && !refresh) {\n\t\t\tsharedData = CACHE[info.uri];\n\t\t} else {\n\t\t\tfor (const name of userConfigs.servicesOrder) {\n\t\t\t\tconst service = userConfigs.services[name];\n\t\t\t\tif (!service.on) continue;\n\t\t\t\tsharedData = { lyrics: [] };\n\n\t\t\t\ttry {\n\t\t\t\t\tconst data = await service.call(info);\n\t\t\t\t\tsharedData = data;\n\t\t\t\t\tCACHE[info.uri] = sharedData;\n\n\t\t\t\t\tif (!sharedData.error) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tsharedData = { error: \"No lyrics\" };\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// simple word segmentation rules\n\tfunction getWords(str) {\n\t\tconst result = [];\n\t\tconst words = str.split(/(\\p{sc=Han}|\\p{sc=Katakana}|\\p{sc=Hiragana}|\\p{sc=Hang}|\\p{gc=Punctuation})|\\s+/gu);\n\t\tlet tempWord = \"\";\n\t\tfor (let word of words) {\n\t\t\tword ??= \" \";\n\t\t\tif (word) {\n\t\t\t\tif (tempWord && /(“|')$/.test(tempWord) && word !== \" \") {\n\t\t\t\t\t// End of line not allowed\n\t\t\t\t\ttempWord += word;\n\t\t\t\t} else if (/(,|\\.|\\?|:|;|'|，|。|？|：|；|”)/.test(word) && tempWord !== \" \") {\n\t\t\t\t\t// Start of line not allowed\n\t\t\t\t\ttempWord += word;\n\t\t\t\t} else {\n\t\t\t\t\tif (tempWord) result.push(tempWord);\n\t\t\t\t\ttempWord = word;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (tempWord) result.push(tempWord);\n\t\treturn result;\n\t}\n\n\tfunction drawParagraph(ctx, str, options) {\n\t\tlet actualWidth = 0;\n\t\tconst maxWidth = ctx.canvas.width - options.left - options.right;\n\t\tconst words = getWords(str);\n\t\tconst lines = [];\n\t\tconst measures = [];\n\t\tlet tempLine = \"\";\n\t\tlet textMeasures = ctx.measureText(\"\");\n\t\tfor (let i = 0; i < words.length; i++) {\n\t\t\tconst word = words[i];\n\t\t\tconst line = tempLine + word;\n\t\t\tconst mea = ctx.measureText(line);\n\t\t\tconst isSpace = /\\s/.test(word);\n\t\t\tif (mea.width > maxWidth && tempLine && !isSpace) {\n\t\t\t\tactualWidth = Math.max(actualWidth, textMeasures.width);\n\t\t\t\tlines.push(tempLine);\n\t\t\t\tmeasures.push(textMeasures);\n\t\t\t\ttempLine = word;\n\t\t\t} else {\n\t\t\t\ttempLine = line;\n\t\t\t\tif (!isSpace) {\n\t\t\t\t\ttextMeasures = mea;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (tempLine !== \"\") {\n\t\t\tactualWidth = Math.max(actualWidth, textMeasures.width);\n\t\t\tlines.push(tempLine);\n\t\t\tmeasures.push(ctx.measureText(tempLine));\n\t\t}\n\n\t\tconst ascent = measures.length ? measures[0].actualBoundingBoxAscent : 0;\n\t\tconst body = measures.length ? options.lineHeight * (measures.length - 1) : 0;\n\t\tconst descent = measures.length ? measures[measures.length - 1].actualBoundingBoxDescent : 0;\n\t\tconst actualHeight = ascent + body + descent;\n\n\t\tlet startX = 0;\n\t\tlet startY = 0;\n\t\tlet translateX = 0;\n\t\tlet translateY = 0;\n\t\tif (options.hCenter) {\n\t\t\tstartX = (ctx.canvas.width - actualWidth) / 2;\n\t\t} else {\n\t\t\tstartX = options.left + translateX;\n\t\t}\n\n\t\tif (options.vCenter) {\n\t\t\tstartY = (ctx.canvas.height - actualHeight) / 2 + ascent;\n\t\t} else if (options.top) {\n\t\t\tstartY = options.top + ascent;\n\t\t} else if (options.bottom) {\n\t\t\tstartY = options.bottom - descent - body;\n\t\t}\n\n\t\tif (typeof options.translateX === \"function\") {\n\t\t\ttranslateX = options.translateX(actualWidth);\n\t\t}\n\t\tif (typeof options.translateX === \"number\") {\n\t\t\ttranslateX = options.translateX;\n\t\t}\n\t\tif (typeof options.translateY === \"function\") {\n\t\t\ttranslateY = options.translateY(actualHeight);\n\t\t}\n\t\tif (typeof options.translateY === \"number\") {\n\t\t\ttranslateY = options.translateY;\n\t\t}\n\t\tif (!options.measure) {\n\t\t\tlines.forEach((str, index) => {\n\t\t\t\tconst x = options.hCenter ? (ctx.canvas.width - measures[index].width) / 2 : startX;\n\t\t\t\tctx.fillText(str, x, startY + index * options.lineHeight + translateY);\n\t\t\t});\n\t\t}\n\t\treturn {\n\t\t\twidth: actualWidth,\n\t\t\theight: actualHeight,\n\t\t\tleft: startX + translateX,\n\t\t\tright: ctx.canvas.width - options.left - actualWidth + translateX,\n\t\t\ttop: startY - ascent + translateY,\n\t\t\tbottom: startY + body + descent + translateY,\n\t\t};\n\t}\n\n\tfunction drawBackground(ctx, image) {\n\t\tif (userConfigs.showCover) {\n\t\t\tconst { width, height } = ctx.canvas;\n\t\t\tctx.imageSmoothingEnabled = false;\n\t\t\tctx.save();\n\t\t\tconst blurSize = Number(userConfigs.blurSize);\n\t\t\tctx.filter = `blur(${blurSize}px)`;\n\t\t\tctx.drawImage(image, -blurSize * 2, -blurSize * 2 - (width - height) / 2, width + 4 * blurSize, width + 4 * blurSize);\n\t\t\tctx.restore();\n\t\t\tctx.fillStyle = \"#000000b0\";\n\t\t} else {\n\t\t\tctx.save();\n\t\t\tctx.fillStyle = \"#000000\";\n\t\t}\n\n\t\tctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);\n\t\tctx.restore();\n\t}\n\n\tfunction drawText(ctx, text, color = \"white\") {\n\t\tdrawBackground(ctx, userConfigs.backgroundImage);\n\t\tconst fontSize = userConfigs.fontSize;\n\t\tctx.fillStyle = color;\n\t\tctx.font = `bold ${fontSize}px ${userConfigs.fontFamily}, sans-serif`;\n\t\tdrawParagraph(ctx, text, {\n\t\t\tvCenter: true,\n\t\t\thCenter: true,\n\t\t\tleft: 0,\n\t\t\tright: 0,\n\t\t\tlineHeight: fontSize,\n\t\t});\n\t\tctx.restore();\n\t}\n\n\tlet offscreenCanvas;\n\tlet offscreenCtx;\n\tlet gradient1;\n\tlet gradient2;\n\n\tfunction initOffscreenCtx(ctx) {\n\t\tif (!offscreenCtx) {\n\t\t\toffscreenCanvas = document.createElement(\"canvas\");\n\t\t\toffscreenCtx = offscreenCanvas.getContext(\"2d\");\n\t\t\tgradient1 = offscreenCtx.createLinearGradient(0, 0, 0, ctx.canvas.height);\n\t\t\tgradient1.addColorStop(0.08, \"transparent\");\n\t\t\tgradient1.addColorStop(0.15, \"white\");\n\t\t\tgradient1.addColorStop(0.85, \"white\");\n\t\t\tgradient1.addColorStop(0.92, \"transparent\");\n\t\t\tgradient2 = offscreenCtx.createLinearGradient(0, 0, 0, ctx.canvas.height);\n\t\t\tgradient2.addColorStop(0.0, \"white\");\n\t\t\tgradient2.addColorStop(0.7, \"white\");\n\t\t\tgradient2.addColorStop(0.925, \"transparent\");\n\t\t}\n\t\toffscreenCtx.canvas.width = ctx.canvas.width;\n\t\toffscreenCtx.canvas.height = ctx.canvas.height;\n\t\treturn {\n\t\t\toffscreenCtx,\n\t\t\tgradient1,\n\t\t\tgradient2,\n\t\t};\n\t}\n\n\t// Avoid drawing again when the same\n\t// Do not operate canvas again in other functions\n\tlet renderState;\n\n\tfunction isEqualState(state1, state2) {\n\t\tif (!state1 || !state2) return false;\n\t\treturn Object.keys(state1).reduce((p, c) => {\n\t\t\treturn p && state1[c] === state2[c];\n\t\t}, true);\n\t}\n\n\tfunction renderLyrics(ctx, lyrics, currentTime) {\n\t\tconst focusLineFontSize = userConfigs.fontSize;\n\t\tconst focusLineHeight = focusLineFontSize * 1.2;\n\t\tconst focusLineMargin = focusLineFontSize * 1;\n\t\tconst otherLineFontSize = focusLineFontSize * 1;\n\t\tconst otherLineHeight = otherLineFontSize * 1.2;\n\t\tconst otherLineMargin = otherLineFontSize * 1;\n\t\tconst otherLineOpacity = 0.35;\n\t\tconst marginWidth = ctx.canvas.width * 0.075;\n\t\tconst animateDuration = userConfigs.smooth ? 0.3 : 0;\n\t\tconst hCenter = userConfigs.centerAlign;\n\t\tconst fontFamily = `${userConfigs.fontFamily}, sans-serif`;\n\n\t\tlet currentIndex = -1;\n\t\tlet progress = 1;\n\t\tlyrics.forEach(({ startTime }, index) => {\n\t\t\tif (startTime && currentTime > startTime - animateDuration) {\n\t\t\t\tcurrentIndex = index;\n\t\t\t\tif (currentTime < startTime) {\n\t\t\t\t\tprogress = (currentTime - startTime + animateDuration) / animateDuration;\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\tif (currentIndex === -1) {\n\t\t\tdrawText(ctx, \"\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst nextState = {\n\t\t\t...userConfigs,\n\t\t\tcurrentIndex,\n\t\t\tlyrics,\n\t\t\tprogress,\n\t\t};\n\t\tif (isEqualState(nextState, renderState)) return;\n\t\trenderState = nextState;\n\n\t\tdrawBackground(ctx, userConfigs.backgroundImage);\n\n\t\tconst { offscreenCtx, gradient1 } = initOffscreenCtx(ctx);\n\t\toffscreenCtx.save();\n\n\t\t// focus line\n\t\tconst fFontSize = otherLineFontSize + progress * (focusLineFontSize - otherLineFontSize);\n\t\tconst fLineHeight = otherLineHeight + progress * (focusLineHeight - otherLineHeight);\n\t\tconst fLineOpacity = otherLineOpacity + progress * (1 - otherLineOpacity);\n\t\tconst otherRight = ctx.canvas.width - marginWidth - (otherLineFontSize / focusLineFontSize) * (ctx.canvas.width - 2 * marginWidth);\n\t\tconst progressRight = marginWidth + (1 - progress) * (otherRight - marginWidth);\n\t\toffscreenCtx.fillStyle = `rgba(255, 255, 255, ${fLineOpacity})`;\n\t\toffscreenCtx.font = `bold ${fFontSize}px ${fontFamily}`;\n\t\tconst prevLineFocusHeight = drawParagraph(offscreenCtx, lyrics[currentIndex - 1] ? lyrics[currentIndex - 1].text : \"\", {\n\t\t\tvCenter: true,\n\t\t\thCenter,\n\t\t\tleft: marginWidth,\n\t\t\tright: marginWidth,\n\t\t\tlineHeight: focusLineFontSize,\n\t\t\tmeasure: true,\n\t\t}).height;\n\n\t\tconst pos = drawParagraph(offscreenCtx, lyrics[currentIndex].text, {\n\t\t\tvCenter: true,\n\t\t\thCenter,\n\t\t\tleft: marginWidth,\n\t\t\tright: progressRight,\n\t\t\tlineHeight: fLineHeight,\n\t\t\ttranslateY: (selfHeight) => ((prevLineFocusHeight + selfHeight) / 2 + focusLineMargin) * (1 - progress),\n\t\t});\n\t\t// offscreenCtx.strokeRect(pos.left, pos.top, pos.width, pos.height);\n\n\t\t// prev line\n\t\tlet lastBeforePos = pos;\n\t\tfor (let i = 0; i < currentIndex; i++) {\n\t\t\tif (i === 0) {\n\t\t\t\tconst prevProgressLineFontSize = otherLineFontSize + (1 - progress) * (focusLineFontSize - otherLineFontSize);\n\t\t\t\tconst prevProgressLineOpacity = otherLineOpacity + (1 - progress) * (1 - otherLineOpacity);\n\t\t\t\toffscreenCtx.fillStyle = `rgba(255, 255, 255, ${prevProgressLineOpacity})`;\n\t\t\t\toffscreenCtx.font = `bold ${prevProgressLineFontSize}px ${fontFamily}`;\n\t\t\t} else {\n\t\t\t\toffscreenCtx.fillStyle = `rgba(255, 255, 255, ${otherLineOpacity})`;\n\t\t\t\toffscreenCtx.font = `bold ${otherLineFontSize}px ${fontFamily}`;\n\t\t\t}\n\t\t\tlastBeforePos = drawParagraph(offscreenCtx, lyrics[currentIndex - 1 - i].text, {\n\t\t\t\thCenter,\n\t\t\t\tbottom: i === 0 ? lastBeforePos.top - focusLineMargin : lastBeforePos.top - otherLineMargin,\n\t\t\t\tleft: marginWidth,\n\t\t\t\tright: i === 0 ? marginWidth + progress * (otherRight - marginWidth) : otherRight,\n\t\t\t\tlineHeight: i === 0 ? otherLineHeight + (1 - progress) * (focusLineHeight - otherLineHeight) : otherLineHeight,\n\t\t\t});\n\t\t\tif (lastBeforePos.top < 0) break;\n\t\t}\n\t\t// next line\n\t\toffscreenCtx.fillStyle = `rgba(255, 255, 255, ${otherLineOpacity})`;\n\t\toffscreenCtx.font = `bold ${otherLineFontSize}px ${fontFamily}`;\n\t\tlet lastAfterPos = pos;\n\t\tfor (let i = currentIndex + 1; i < lyrics.length; i++) {\n\t\t\tlastAfterPos = drawParagraph(offscreenCtx, lyrics[i].text, {\n\t\t\t\thCenter,\n\t\t\t\ttop: i === currentIndex + 1 ? lastAfterPos.bottom + focusLineMargin : lastAfterPos.bottom + otherLineMargin,\n\t\t\t\tleft: marginWidth,\n\t\t\t\tright: otherRight,\n\t\t\t\tlineHeight: otherLineHeight,\n\t\t\t});\n\t\t\tif (lastAfterPos.bottom > ctx.canvas.height) break;\n\t\t}\n\n\t\toffscreenCtx.globalCompositeOperation = \"source-in\";\n\t\toffscreenCtx.fillStyle = gradient1;\n\t\toffscreenCtx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);\n\t\toffscreenCtx.restore();\n\t\tctx.drawImage(offscreenCtx.canvas, 0, 0);\n\n\t\tctx.restore();\n\t}\n\n\tlet timeout = null;\n\n\tasync function tick(options) {\n\t\tif (!lyricVideoIsOpen) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (timeout) clearTimeout(timeout);\n\n\t\tconst audio = {\n\t\t\tcurrentTime: (Player.getProgress() - Number(options.delay)) / 1000,\n\t\t\tduration: Player.getDuration() / 1000,\n\t\t};\n\n\t\tconst { error, lyrics } = sharedData;\n\n\t\tif (error) {\n\t\t\tif (error === \"Instrumental\") {\n\t\t\t\tdrawText(lyricCtx, error);\n\t\t\t} else {\n\t\t\t\tdrawText(lyricCtx, error, \"red\");\n\t\t\t}\n\t\t} else if (!lyrics) {\n\t\t\tdrawText(lyricCtx, \"No lyrics\");\n\t\t} else if (audio.duration && lyrics.length) {\n\t\t\trenderLyrics(lyricCtx, lyrics, audio.currentTime);\n\t\t} else if (!audio.duration || lyrics.length === 0) {\n\t\t\tdrawText(lyricCtx, audio.currentSrc ? \"Loading\" : \"Waiting\");\n\t\t}\n\n\t\tif (!lyrics?.length) {\n\t\t\ttimeout = setTimeout(tick, 1000, options);\n\t\t\treturn;\n\t\t}\n\n\t\tif (!document.hidden) {\n\t\t\trequestAnimationFrame(() => tick(options));\n\t\t}\n\t}\n\n\tfunction boolLocalStorage(name, defaultVal = true) {\n\t\tconst value = LocalStorage.get(name);\n\t\treturn value ? value === \"true\" : defaultVal;\n\t}\n\n\tlet configContainer;\n\n\tfunction openConfig(event) {\n\t\tevent.preventDefault();\n\n\t\t// Reset on reopen\n\t\tif (configContainer) {\n\t\t\tresetTokenButton(configContainer);\n\t\t} else {\n\t\t\tconfigContainer = document.createElement(\"div\");\n\t\t\tconfigContainer.id = \"popup-config-container\";\n\t\t\tconst style = document.createElement(\"style\");\n\t\t\tstyle.innerHTML = `\n.setting-row {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n}\n.setting-row::after {\n    content: \"\";\n    display: table;\n    clear: both;\n}\n.setting-row .col {\n    display: flex;\n    padding: 10px 0;\n    align-items: center;\n}\n.setting-row .col.description {\n    padding-right: 15px;\n    cursor: default;\n    width: 50%;\n}\n.setting-row .col.action {\n    justify-content: flex-end;\n    width: 50%;\n}\n.popup-config-col-margin {\n    margin-top: 10px;\n}\nbutton.switch {\n    align-items: center;\n    border: 0px;\n    border-radius: 50%;\n    background-color: rgba(var(--spice-rgb-shadow), .7);\n    color: var(--spice-text);\n    cursor: pointer;\n    display: flex;\n    margin-inline-start: 12px;\n    padding: 8px;\n}\nbutton.switch.disabled,\nbutton.switch[disabled] {\n    color: rgba(var(--spice-rgb-text), .3);\n}\nbutton.switch.small {\n    width: 22px;\n    height: 22px;\n    padding: 6px;\n}\nbutton.btn {\n    font-weight: 700;\n    display: block;\n    background-color: rgba(var(--spice-rgb-shadow), .7);\n    border-radius: 500px;\n    transition-duration: 33ms;\n    transition-property: background-color, border-color, color, box-shadow, filter, transform;\n    padding-inline: 15px;\n    border: 1px solid #727272;\n    color: var(--spice-text);\n    min-block-size: 32px;\n    cursor: pointer;\n}\nbutton.btn:hover {\n    transform: scale(1.04);\n    border-color: var(--spice-text);\n}\nbutton.btn:disabled {\n    opacity: 0.5;\n    cursor: not-allowed;\n}\n#popup-config-container select {\n    color: var(--spice-text);\n    background: rgba(var(--spice-rgb-shadow), .7);\n    border: 0;\n    height: 32px;\n}\n#popup-config-container input {\n    width: 100%;\n    padding: 0 5px;\n    height: 32px;\n    border: 0;\n}\n#popup-lyrics-delay-input {\n    background-color: rgba(var(--spice-rgb-shadow), .7);\n    color: var(--spice-text);\n}\n`;\n\t\t\tconst optionHeader = document.createElement(\"h2\");\n\t\t\toptionHeader.innerText = \"Options\";\n\t\t\tconst smooth = createSlider(\"Smooth scrolling\", userConfigs.smooth, (state) => {\n\t\t\t\tuserConfigs.smooth = state;\n\t\t\t\tLocalStorage.set(\"popup-lyrics:smooth\", String(state));\n\t\t\t});\n\t\t\tconst center = createSlider(\"Center align\", userConfigs.centerAlign, (state) => {\n\t\t\t\tuserConfigs.centerAlign = state;\n\t\t\t\tLocalStorage.set(\"popup-lyrics:center-align\", String(state));\n\t\t\t});\n\t\t\tconst cover = createSlider(\"Show cover\", userConfigs.showCover, (state) => {\n\t\t\t\tuserConfigs.showCover = state;\n\t\t\t\tLocalStorage.set(\"popup-lyrics:show-cover\", String(state));\n\t\t\t});\n\t\t\tconst ratio = createOptions(\"Aspect ratio\", { 11: \"1:1\", 43: \"4:3\", 169: \"16:9\" }, userConfigs.ratio, (state) => {\n\t\t\t\tuserConfigs.ratio = state;\n\t\t\t\tLocalStorage.set(\"popup-lyrics:ratio\", state);\n\t\t\t\tlet value = lyricVideo.width;\n\t\t\t\tswitch (userConfigs.ratio) {\n\t\t\t\t\tcase \"11\":\n\t\t\t\t\t\tvalue = lyricVideo.width;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"43\":\n\t\t\t\t\t\tvalue = Math.round((lyricVideo.width * 3) / 4);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"169\":\n\t\t\t\t\t\tvalue = Math.round((lyricVideo.width * 9) / 16);\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tlyricVideo.height = lyricCanvas.height = value;\n\t\t\t\toffscreenCtx = null;\n\t\t\t});\n\t\t\tconst fontSize = createOptions(\n\t\t\t\t\"Font size\",\n\t\t\t\t{\n\t\t\t\t\t30: \"30px\",\n\t\t\t\t\t34: \"34px\",\n\t\t\t\t\t38: \"38px\",\n\t\t\t\t\t42: \"42px\",\n\t\t\t\t\t46: \"46px\",\n\t\t\t\t\t50: \"50px\",\n\t\t\t\t\t54: \"54px\",\n\t\t\t\t\t58: \"58px\",\n\t\t\t\t},\n\t\t\t\tString(userConfigs.fontSize),\n\t\t\t\t(state) => {\n\t\t\t\t\tuserConfigs.fontSize = Number(state);\n\t\t\t\t\tLocalStorage.set(\"popup-lyrics:font-size\", state);\n\t\t\t\t}\n\t\t\t);\n\t\t\tconst blurSize = createOptions(\n\t\t\t\t\"Blur size\",\n\t\t\t\t{\n\t\t\t\t\t2: \"2px\",\n\t\t\t\t\t5: \"5px\",\n\t\t\t\t\t10: \"10px\",\n\t\t\t\t\t15: \"15px\",\n\t\t\t\t},\n\t\t\t\tString(userConfigs.blurSize),\n\t\t\t\t(state) => {\n\t\t\t\t\tuserConfigs.blurSize = Number(state);\n\t\t\t\t\tLocalStorage.set(\"popup-lyrics:blur-size\", state);\n\t\t\t\t}\n\t\t\t);\n\t\t\tconst delay = createOptionsInput(\"Delay\", String(userConfigs.delay), (state) => {\n\t\t\t\tuserConfigs.delay = Number(state);\n\t\t\t\tLocalStorage.set(\"popup-lyrics:delay\", state);\n\t\t\t});\n\t\t\tconst clearCache = descriptiveElement(\n\t\t\t\tcreateButton(\"Clear Memory Cache\", \"Clear Memory Cache\", () => {\n\t\t\t\t\tCACHE = {};\n\t\t\t\t\tupdateTrack();\n\t\t\t\t}),\n\t\t\t\t\"Loaded lyrics are cached in memory for faster reloading. Press this button to clear the cached lyrics from memory without restarting Spotify.\"\n\t\t\t);\n\n\t\t\tconst serviceHeader = document.createElement(\"h2\");\n\t\t\tserviceHeader.innerText = \"Services\";\n\n\t\t\tconst serviceContainer = document.createElement(\"div\");\n\n\t\t\tfunction stackServiceElements() {\n\t\t\t\tuserConfigs.servicesOrder.forEach((name, index) => {\n\t\t\t\t\tconst el = userConfigs.services[name].element;\n\n\t\t\t\t\tconst [up, down] = el.querySelectorAll(\"button\");\n\t\t\t\t\tif (index === 0) {\n\t\t\t\t\t\tup.disabled = true;\n\t\t\t\t\t\tdown.disabled = false;\n\t\t\t\t\t} else if (index === userConfigs.servicesOrder.length - 1) {\n\t\t\t\t\t\tup.disabled = false;\n\t\t\t\t\t\tdown.disabled = true;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tup.disabled = false;\n\t\t\t\t\t\tdown.disabled = false;\n\t\t\t\t\t}\n\n\t\t\t\t\tserviceContainer.append(el);\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tfunction switchCallback(el, state) {\n\t\t\t\tconst id = el.dataset.id;\n\t\t\t\tuserConfigs.services[id].on = state;\n\t\t\t\tLocalStorage.set(`popup-lyrics:services:${id}:on`, state);\n\t\t\t\tupdateTrack(true);\n\t\t\t}\n\n\t\t\tfunction posCallback(el, dir) {\n\t\t\t\tconst id = el.dataset.id;\n\t\t\t\tconst curPos = userConfigs.servicesOrder.findIndex((val) => val === id);\n\t\t\t\tconst newPos = curPos + dir;\n\n\t\t\t\tconst temp = userConfigs.servicesOrder[newPos];\n\t\t\t\tuserConfigs.servicesOrder[newPos] = userConfigs.servicesOrder[curPos];\n\t\t\t\tuserConfigs.servicesOrder[curPos] = temp;\n\n\t\t\t\tLocalStorage.set(\"popup-lyrics:services-order\", JSON.stringify(userConfigs.servicesOrder));\n\n\t\t\t\tstackServiceElements();\n\t\t\t\tupdateTrack(true);\n\t\t\t}\n\n\t\t\tfor (const name of userConfigs.servicesOrder) {\n\t\t\t\tuserConfigs.services[name].element = createServiceOption(name, userConfigs.services[name], switchCallback, posCallback);\n\t\t\t}\n\t\t\tstackServiceElements();\n\n\t\t\tconfigContainer.append(\n\t\t\t\tstyle,\n\t\t\t\toptionHeader,\n\t\t\t\tsmooth,\n\t\t\t\tcenter,\n\t\t\t\tcover,\n\t\t\t\tblurSize,\n\t\t\t\tfontSize,\n\t\t\t\tratio,\n\t\t\t\tdelay,\n\t\t\t\tclearCache,\n\t\t\t\tserviceHeader,\n\t\t\t\tserviceContainer\n\t\t\t);\n\t\t}\n\t\tSpicetify.PopupModal.display({\n\t\t\ttitle: \"Popup Lyrics\",\n\t\t\tcontent: configContainer,\n\t\t});\n\t}\n\n\tfunction createSlider(name, defaultVal, callback) {\n\t\tconst container = document.createElement(\"div\");\n\t\tcontainer.innerHTML = `\n<div class=\"setting-row\">\n    <label class=\"col description\">${name}</label>\n    <div class=\"col action\"><button class=\"switch\">\n        <svg height=\"16\" width=\"16\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n            ${Spicetify.SVGIcons.check}\n        </svg>\n    </button></div>\n</div>`;\n\n\t\tconst slider = container.querySelector(\"button\");\n\t\tslider.classList.toggle(\"disabled\", !defaultVal);\n\n\t\tslider.onclick = () => {\n\t\t\tconst state = slider.classList.contains(\"disabled\");\n\t\t\tslider.classList.toggle(\"disabled\");\n\t\t\tcallback(state);\n\t\t};\n\n\t\treturn container;\n\t}\n\tfunction createOptions(name, options, defaultValue, callback) {\n\t\tconst container = document.createElement(\"div\");\n\t\tcontainer.innerHTML = `\n<div class=\"setting-row\">\n    <label class=\"col description\">${name}</label>\n    <div class=\"col action\">\n        <select>\n            ${Object.keys(options)\n\t\t\t\t\t\t\t.map(\n\t\t\t\t\t\t\t\t(item) => `\n                <option value=\"${item}\" dir=\"auto\">${options[item]}</option>\n            `\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.join(\"\\n\")}\n        </select>\n    </div>\n</div>`;\n\n\t\tconst select = container.querySelector(\"select\");\n\t\tselect.value = defaultValue;\n\t\tselect.onchange = (e) => {\n\t\t\tcallback(e.target.value);\n\t\t};\n\n\t\treturn container;\n\t}\n\tfunction createOptionsInput(name, defaultValue, callback) {\n\t\tconst container = document.createElement(\"div\");\n\t\tcontainer.innerHTML = `\n    <div class=\"setting-row\">\n    <label class=\"col description\">${name}</label>\n    <div class=\"col action\">\n        <input\n          id=\"popup-lyrics-delay-input\"\n          type=\"number\"\n        />\n    </div>\n    </div>`;\n\n\t\tconst input = container.querySelector(\"#popup-lyrics-delay-input\");\n\t\tinput.value = defaultValue;\n\t\tinput.onchange = (e) => {\n\t\t\tcallback(e.target.value);\n\t\t};\n\n\t\treturn container;\n\t}\n\t// if name is null, the element can be used without a description.\n\tfunction createButton(name, defaultValue, callback) {\n\t\tlet container;\n\n\t\tif (name) {\n\t\t\tcontainer = document.createElement(\"div\");\n\t\t\tcontainer.innerHTML = `\n\t\t<div class=\"setting-row\">\n\t\t<label class=\"col description\">${name}</label>\n\t\t<div class=\"col action\">\n\t\t\t<button id=\"popup-lyrics-clickbutton\" class=\"btn\">${defaultValue}</button>\n\t\t</div>\n\t\t</div>`;\n\n\t\t\tconst button = container.querySelector(\"#popup-lyrics-clickbutton\");\n\t\t\tbutton.onclick = () => {\n\t\t\t\tcallback();\n\t\t\t};\n\t\t} else {\n\t\t\tcontainer = document.createElement(\"button\");\n\t\t\tcontainer.innerHTML = defaultValue;\n\t\t\tcontainer.className = \"btn \";\n\n\t\t\tcontainer.onclick = () => {\n\t\t\t\tcallback();\n\t\t\t};\n\t\t}\n\n\t\treturn container;\n\t}\n\t// if name is null, the element can be used without a description.\n\tfunction createTextfield(name, defaultValue, placeholder, callback) {\n\t\tlet container;\n\n\t\tif (name) {\n\t\t\tcontainer = document.createElement(\"div\");\n\t\t\tcontainer.className = \"setting-column\";\n\t\t\tcontainer.innerHTML = `\n\t\t\t<label class=\"row-description\">${name}</label>\n\t\t\t<div class=\"popup-row-option action\">\n\t\t\t\t<input id=\"popup-lyrics-textfield\" placeholder=\"${placeholder}\" value=\"${defaultValue}\" />\n\t\t\t</div>`;\n\n\t\t\tconst textfield = container.querySelector(\"#popup-lyrics-textfield\");\n\t\t\ttextfield.onchange = () => {\n\t\t\t\tcallback();\n\t\t\t};\n\t\t} else {\n\t\t\tcontainer = document.createElement(\"input\");\n\t\t\tcontainer.placeholder = placeholder;\n\t\t\tcontainer.value = defaultValue;\n\n\t\t\tcontainer.onchange = (e) => {\n\t\t\t\tcallback(e.target.value);\n\t\t\t};\n\t\t}\n\n\t\treturn container;\n\t}\n\tfunction descriptiveElement(element, description) {\n\t\tconst desc = document.createElement(\"span\");\n\t\tdesc.innerHTML = description;\n\t\telement.append(desc);\n\t\treturn element;\n\t}\n\n\tfunction resetTokenButton(container) {\n\t\tconst button = container.querySelector(\"#popup-lyrics-refresh-token\");\n\t\tif (button) {\n\t\t\tbutton.innerHTML = \"Refresh token\";\n\t\t\tbutton.disabled = false;\n\t\t}\n\t}\n\n\tfunction musixmatchTokenElements(defaultVal, id) {\n\t\tconst button = createButton(null, \"Refresh token\", clickRefresh);\n\t\tbutton.className += \"popup-config-col-margin\";\n\t\tbutton.id = \"popup-lyrics-refresh-token\";\n\t\tconst textfield = createTextfield(null, defaultVal.token, `Place your ${id} token here`, changeTokenfield);\n\t\ttextfield.className += \"popup-config-col-margin\";\n\n\t\tfunction clickRefresh() {\n\t\t\tbutton.innerHTML = \"Refreshing token...\";\n\t\t\tbutton.disabled = true;\n\n\t\t\tSpicetify.CosmosAsync.get(\"https://apic-desktop.musixmatch.com/ws/1.1/token.get?app_id=web-desktop-app-v1.0\", null, {\n\t\t\t\tauthority: \"apic-desktop.musixmatch.com\",\n\t\t\t})\n\t\t\t\t.then(({ message: response }) => {\n\t\t\t\t\tif (response.header.status_code === 200 && response.body.user_token) {\n\t\t\t\t\t\tbutton.innerHTML = \"Token refreshed\";\n\t\t\t\t\t\ttextfield.value = response.body.user_token;\n\t\t\t\t\t\ttextfield.dispatchEvent(new Event(\"change\"));\n\t\t\t\t\t} else if (response.header.status_code === 401) {\n\t\t\t\t\t\tbutton.innerHTML = \"Too many attempts\";\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbutton.innerHTML = \"Failed to refresh token\";\n\t\t\t\t\t\tconsole.error(\"Failed to refresh token\", response);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch((error) => {\n\t\t\t\t\tbutton.innerHTML = \"Failed to refresh token\";\n\t\t\t\t\tconsole.error(\"Failed to refresh token\", error);\n\t\t\t\t});\n\t\t}\n\n\t\tfunction changeTokenfield(value) {\n\t\t\tuserConfigs.services.musixmatch.token = value;\n\t\t\tLocalStorage.set(\"popup-lyrics:services:musixmatch:token\", value);\n\t\t\tupdateTrack(true);\n\t\t}\n\n\t\tconst container = document.createElement(\"div\");\n\t\tcontainer.append(button);\n\t\tcontainer.append(textfield);\n\t\treturn container;\n\t}\n\n\tfunction createServiceOption(id, defaultVal, switchCallback, posCallback) {\n\t\tconst name = id.replace(/^./, (c) => c.toUpperCase());\n\n\t\tconst container = document.createElement(\"div\");\n\t\tcontainer.dataset.id = id;\n\t\tcontainer.innerHTML = `\n<div class=\"setting-row\">\n    <h3 class=\"col description\">${name}</h3>\n    <div class=\"col action\">\n        <button class=\"switch small\">\n            <svg height=\"10\" width=\"10\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n                ${Spicetify.SVGIcons[\"chart-up\"]}\n            </svg>\n        </button>\n        <button class=\"switch small\">\n            <svg height=\"10\" width=\"10\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n                ${Spicetify.SVGIcons[\"chart-down\"]}\n            </svg>\n        </button>\n        <button class=\"switch\">\n            <svg height=\"16\" width=\"16\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n                ${Spicetify.SVGIcons.check}\n            </svg>\n        </button>\n    </div>\n</div>\n<span>${defaultVal.desc}</span>`;\n\n\t\tif (id === \"musixmatch\") {\n\t\t\tcontainer.append(musixmatchTokenElements(defaultVal));\n\t\t}\n\n\t\tconst [up, down, slider] = container.querySelectorAll(\"button\");\n\n\t\tslider.classList.toggle(\"disabled\", !defaultVal.on);\n\t\tslider.onclick = () => {\n\t\t\tconst state = slider.classList.contains(\"disabled\");\n\t\t\tslider.classList.toggle(\"disabled\");\n\t\t\tswitchCallback(container, state);\n\t\t};\n\n\t\tup.onclick = () => posCallback(container, -1);\n\t\tdown.onclick = () => posCallback(container, 1);\n\n\t\treturn container;\n\t}\n}\n"
  },
  {
    "path": "Extensions/shuffle+.js",
    "content": "// NAME: Shuffle+\n// AUTHORS: khanhas, Tetrax-10\n// DESCRIPTION: True shuffle with no bias.\n\n/// <reference path=\"../globals.d.ts\" />\n\n(async function shufflePlus() {\n\tif (!(Spicetify.CosmosAsync && Spicetify.Platform)) {\n\t\tsetTimeout(shufflePlus, 300);\n\t\treturn;\n\t}\n\n\tconst { React } = Spicetify;\n\tconst { useState } = React;\n\tlet playbarButton = null;\n\n\tfunction getConfig() {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(Spicetify.LocalStorage.get(\"shufflePlus:settings\"));\n\t\t\tif (parsed && typeof parsed === \"object\") {\n\t\t\t\treturn parsed;\n\t\t\t}\n\t\t\tthrow \"\";\n\t\t} catch {\n\t\t\tSpicetify.LocalStorage.set(\"shufflePlus:settings\", \"{}\");\n\t\t\treturn {\n\t\t\t\tartistMode: \"all\",\n\t\t\t\tartistNameMust: false,\n\t\t\t\tenableQueueButton: false,\n\t\t\t};\n\t\t}\n\t}\n\n\tconst CONFIG = getConfig();\n\tsaveConfig();\n\n\tfunction saveConfig() {\n\t\tSpicetify.LocalStorage.set(\"shufflePlus:settings\", JSON.stringify(CONFIG));\n\t}\n\n\tfunction settingsPage() {\n\t\tconst style = React.createElement(\n\t\t\t\"style\",\n\t\t\tnull,\n\t\t\t`.popup-row::after {\n\t\t\t\tcontent: \"\";\n\t\t\t\tdisplay: table;\n\t\t\t\tclear: both;\n\t\t\t}\n\t\t\t.popup-row .col {\n\t\t\t\tdisplay: flex;\n\t\t\t\tpadding: 10px 0;\n\t\t\t\talign-items: center;\n\t\t\t}\n\t\t\t.popup-row .col.description {\n\t\t\t\tfloat: left;\n\t\t\t\tpadding-right: 15px;\n\t\t\t}\n\t\t\t.popup-row .col.action {\n\t\t\t\tfloat: right;\n\t\t\t\ttext-align: right;\n\t\t\t}\n\t\t\t.popup-row .div-title {\n\t\t\t\tcolor: var(--spice-text);\n\t\t\t}\n\t\t\t.popup-row .divider {\n\t\t\t\theight: 2px;\n\t\t\t\tborder-width: 0;\n\t\t\t\tbackground-color: var(--spice-button-disabled);\n\t\t\t}\n\t\t\tbutton.checkbox {\n\t\t\t\talign-items: center;\n\t\t\t\tborder: 0px;\n\t\t\t\tborder-radius: 50%;\n\t\t\t\tbackground-color: rgba(var(--spice-rgb-shadow), 0.7);\n\t\t\t\tcolor: var(--spice-text);\n\t\t\t\tcursor: pointer;\n\t\t\t\tdisplay: flex;\n\t\t\t\tmargin-inline-start: 12px;\n\t\t\t\tpadding: 8px;\n\t\t\t}\n\t\t\tbutton.checkbox.disabled {\n\t\t\t\tcolor: rgba(var(--spice-rgb-text), 0.3);\n\t\t\t}\n\t\t\tselect {\n\t\t\t\tcolor: var(--spice-text);\n\t\t\t\tbackground: rgba(var(--spice-rgb-shadow), 0.7);\n\t\t\t\tborder: 0;\n\t\t\t\theight: 32px;\n\t\t\t}\n\t\t\t::-webkit-scrollbar {\n\t\t\t\twidth: 8px;\n\t\t\t}`\n\t\t);\n\n\t\tfunction DisplayIcon({ icon, size }) {\n\t\t\treturn React.createElement(\"svg\", {\n\t\t\t\twidth: size,\n\t\t\t\theight: size,\n\t\t\t\tviewBox: \"0 0 16 16\",\n\t\t\t\tfill: \"currentColor\",\n\t\t\t\tdangerouslySetInnerHTML: {\n\t\t\t\t\t__html: icon,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\tfunction checkBoxItem({ name, field, onclickFun = () => {} }) {\n\t\t\tconst [value, setValue] = useState(CONFIG[field]);\n\t\t\treturn React.createElement(\n\t\t\t\t\"div\",\n\t\t\t\t{ className: \"popup-row\" },\n\t\t\t\tReact.createElement(\"label\", { className: \"col description\" }, name),\n\t\t\t\tReact.createElement(\n\t\t\t\t\t\"div\",\n\t\t\t\t\t{ className: \"col action\" },\n\t\t\t\t\tReact.createElement(\n\t\t\t\t\t\t\"button\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tclassName: `checkbox${value ? \"\" : \" disabled\"}`,\n\t\t\t\t\t\t\tonClick: () => {\n\t\t\t\t\t\t\t\tCONFIG[field] = !value;\n\t\t\t\t\t\t\t\tsetValue(!value);\n\t\t\t\t\t\t\t\tsaveConfig();\n\t\t\t\t\t\t\t\tonclickFun();\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tReact.createElement(DisplayIcon, {\n\t\t\t\t\t\t\ticon: Spicetify.SVGIcons.check,\n\t\t\t\t\t\t\tsize: 16,\n\t\t\t\t\t\t})\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\tfunction dropDownItem({ name, field, options, onclickFun = () => {} }) {\n\t\t\tconst [value, setValue] = useState(CONFIG[field]);\n\t\t\treturn React.createElement(\n\t\t\t\t\"div\",\n\t\t\t\t{ className: \"popup-row\" },\n\t\t\t\tReact.createElement(\"label\", { className: \"col description\" }, name),\n\t\t\t\tReact.createElement(\n\t\t\t\t\t\"div\",\n\t\t\t\t\t{ className: \"col action\" },\n\t\t\t\t\tReact.createElement(\n\t\t\t\t\t\t\"select\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvalue,\n\t\t\t\t\t\t\tonChange: (e) => {\n\t\t\t\t\t\t\t\tsetValue(e.target.value);\n\t\t\t\t\t\t\t\tCONFIG[field] = e.target.value;\n\t\t\t\t\t\t\t\tsaveConfig();\n\t\t\t\t\t\t\t\tonclickFun();\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tObject.keys(options).map((item) =>\n\t\t\t\t\t\t\tReact.createElement(\n\t\t\t\t\t\t\t\t\"option\",\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tvalue: item,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\toptions[item]\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t)\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\tconst settingsDOMContent = React.createElement(\n\t\t\t\"div\",\n\t\t\tnull,\n\t\t\tstyle,\n\t\t\tReact.createElement(\"div\", { className: \"popup-row\" }, React.createElement(\"h3\", { className: \"div-title\" }, \"Artist Shuffle\")),\n\t\t\tReact.createElement(\"div\", { className: \"popup-row\" }, React.createElement(\"hr\", { className: \"divider\" }, null)),\n\t\t\tReact.createElement(dropDownItem, {\n\t\t\t\tname: \"Shuffle mode Artist Page\",\n\t\t\t\tfield: \"artistMode\",\n\t\t\t\toptions: {\n\t\t\t\t\tall: \"All\",\n\t\t\t\t\talbum: \"Albums\",\n\t\t\t\t\tsingle: \"Singles & EP\",\n\t\t\t\t\tlikedSongArtist: \"Artist's Liked Songs\",\n\t\t\t\t\ttopTen: \"Top 10 Songs\",\n\t\t\t\t},\n\t\t\t}),\n\t\t\tReact.createElement(checkBoxItem, {\n\t\t\t\tname: \"Chosen artist must be included\",\n\t\t\t\tfield: \"artistNameMust\",\n\t\t\t}),\n\t\t\tReact.createElement(checkBoxItem, {\n\t\t\t\tname: \"Enable Shuffle+ Queue Tracks button in Playbar\",\n\t\t\t\tfield: \"enableQueueButton\",\n\t\t\t\tonclickFun: () => renderQueuePlaybarButton(),\n\t\t\t})\n\t\t);\n\n\t\tSpicetify.PopupModal.display({\n\t\t\ttitle: \"Shuffle+\",\n\t\t\tcontent: settingsDOMContent,\n\t\t\tisLarge: true,\n\t\t});\n\t}\n\n\tnew Spicetify.Menu.Item(\"Shuffle+\", false, settingsPage, \"shuffle\").register();\n\n\tconst { Type } = Spicetify.URI;\n\n\tfunction shouldAddShufflePlus(uri) {\n\t\tif (uri.length === 1) {\n\t\t\tconst uriObj = Spicetify.URI.fromString(uri[0]);\n\t\t\tswitch (uriObj.type) {\n\t\t\t\tcase Type.PLAYLIST:\n\t\t\t\tcase Type.PLAYLIST_V2:\n\t\t\t\tcase Type.ALBUM:\n\t\t\t\tcase Type.ARTIST:\n\t\t\t\tcase Type.COLLECTION:\n\t\t\t\tcase Type.FOLDER:\n\t\t\t\tcase Type.SHOW:\n\t\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\n\tfunction shouldAddShufflePlusLiked(uri) {\n\t\tconst uriObj = Spicetify.URI.fromString(uri[0]);\n\t\tif (Spicetify.Platform.History.location.pathname === \"/collection/tracks\") {\n\t\t\tswitch (uriObj.type) {\n\t\t\t\tcase Type.TRACK:\n\t\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\tfunction shouldAddShufflePlusLocal(uri) {\n\t\tconst uriObj = Spicetify.URI.fromString(uri[0]);\n\t\tif (Spicetify.Platform.History.location.pathname === \"/collection/local-files\") {\n\t\t\tswitch (uriObj.type) {\n\t\t\t\tcase Type.TRACK:\n\t\t\t\tcase Type.LOCAL_TRACK:\n\t\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\tnew Spicetify.ContextMenu.Item(\n\t\t\"Play with Shuffle+\",\n\t\tasync (uri) => {\n\t\t\tif (uri.length === 1) {\n\t\t\t\tawait fetchAndPlay(uri[0]);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait fetchAndPlay(uri);\n\t\t},\n\t\tshouldAddShufflePlus,\n\t\t\"shuffle\"\n\t).register();\n\n\tnew Spicetify.ContextMenu.Item(\n\t\t\"Shuffle+ Liked Songs\",\n\t\tasync (uri) => {\n\t\t\tawait fetchAndPlay(uri[0]);\n\t\t},\n\t\tshouldAddShufflePlusLiked,\n\t\t\"heart-active\"\n\t).register();\n\n\tnew Spicetify.ContextMenu.Item(\n\t\t\"Shuffle+ Local Files\",\n\t\tasync (uri) => {\n\t\t\tawait fetchAndPlay(uri[0]);\n\t\t},\n\t\tshouldAddShufflePlusLocal,\n\t\t\"playlist-folder\"\n\t).register();\n\n\trenderQueuePlaybarButton();\n\tfunction renderQueuePlaybarButton() {\n\t\tif (!playbarButton) {\n\t\t\tplaybarButton = new Spicetify.Playbar.Button(\n\t\t\t\t\"Shuffle+ Queue Tracks\",\n\t\t\t\t\"enhance\",\n\t\t\t\tasync () => {\n\t\t\t\t\tawait fetchAndPlay(\"queue\");\n\t\t\t\t},\n\t\t\t\tfalse,\n\t\t\t\tfalse\n\t\t\t);\n\t\t}\n\n\t\tif (CONFIG.enableQueueButton) playbarButton.register();\n\t\telse playbarButton.deregister();\n\t}\n\n\tasync function fetchPlaylistTracks(uri) {\n\t\tconst res = await Spicetify.Platform.PlaylistAPI.getContents(`spotify:playlist:${uri}`, {\n\t\t\tlimit: 9999999,\n\t\t});\n\t\treturn res.items.filter((track) => track.isPlayable).map((track) => track.uri);\n\t}\n\n\tfunction searchFolder(rows, uri) {\n\t\tfor (const r of rows) {\n\t\t\tif (r.type !== \"folder\" || !r.items) continue;\n\n\t\t\tif (r.uri === uri) return r;\n\n\t\t\tconst found = searchFolder(r.items, uri);\n\t\t\tif (found) return found;\n\t\t}\n\t}\n\n\tasync function fetchFolderTracks(uri) {\n\t\tconst res = await Spicetify.Platform.RootlistAPI.getContents();\n\n\t\tconst requestFolder = searchFolder(res.items, uri);\n\t\tif (!requestFolder) throw \"Cannot find folder\";\n\n\t\tconst requestPlaylists = [];\n\t\tasync function fetchNested(folder) {\n\t\t\tif (!folder.items) return;\n\n\t\t\tfor (const i of folder.items) {\n\t\t\t\tif (i.type === \"playlist\") {\n\t\t\t\t\tconst uriObj = Spicetify.URI.fromString(i.uri);\n\t\t\t\t\tconst uri = uriObj._base62Id ?? uriObj.id;\n\t\t\t\t\trequestPlaylists.push(await fetchPlaylistTracks(uri));\n\t\t\t\t} else if (i.type === \"folder\") await fetchNested(i);\n\t\t\t}\n\t\t}\n\n\t\tawait fetchNested(requestFolder);\n\n\t\treturn requestPlaylists.flat();\n\t}\n\n\tasync function fetchAlbumTracks(uri, includeMetadata = false) {\n\t\tconst { queryAlbumTracks } = Spicetify.GraphQL.Definitions;\n\t\tconst { data, errors } = await Spicetify.GraphQL.Request(queryAlbumTracks, {\n\t\t\turi,\n\t\t\toffset: 0,\n\t\t\tlimit: 100,\n\t\t});\n\n\t\tif (errors) throw errors[0].message;\n\t\tif (data.albumUnion.playability.playable === false) throw \"Album is not playable\";\n\n\t\treturn (data.albumUnion?.tracksV2 ?? data.albumUnion?.tracks ?? []).items\n\t\t\t.filter(({ track }) => track.playability.playable)\n\t\t\t.map(({ track }) => (includeMetadata ? track : track.uri));\n\t}\n\n\tconst artistFetchTypeCount = { album: 0, single: 0 };\n\n\tasync function scanForTracksFromAlbums(res, artistName, type) {\n\t\tconst allTracks = [];\n\n\t\tfor (const album of res) {\n\t\t\tlet albumRes;\n\n\t\t\ttry {\n\t\t\t\talbumRes = await fetchAlbumTracks(album.uri, true);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(album, error);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tartistFetchTypeCount[type]++;\n\t\t\tSpicetify.showNotification(`${artistFetchTypeCount[type]} / ${res.length} ${type}s`);\n\n\t\t\tfor (const track of albumRes) {\n\t\t\t\tif (!CONFIG.artistNameMust || track.artists.items.some((artist) => artist.profile.name === artistName)) allTracks.push(track.uri);\n\t\t\t}\n\t\t}\n\n\t\treturn allTracks;\n\t}\n\n\tasync function fetchArtistTracks(uri) {\n\t\t// Definitions from older Spotify version\n\t\tconst queryArtistOverview = {\n\t\t\tname: \"queryArtistOverview\",\n\t\t\toperation: \"query\",\n\t\t\tsha256Hash: \"35648a112beb1794e39ab931365f6ae4a8d45e65396d641eeda94e4003d41497\",\n\t\t\tvalue: null,\n\t\t};\n\t\tconst queryArtistDiscographyAll = {\n\t\t\tname: \"queryArtistDiscographyAll\",\n\t\t\toperation: \"query\",\n\t\t\tsha256Hash: \"9380995a9d4663cbcb5113fef3c6aabf70ae6d407ba61793fd01e2a1dd6929b0\",\n\t\t\tvalue: null,\n\t\t};\n\n\t\tconst discography = await Spicetify.GraphQL.Request(queryArtistDiscographyAll, {\n\t\t\turi,\n\t\t\toffset: 0,\n\t\t\t// Limit 100 since GraphQL has resource limit\n\t\t\tlimit: 100,\n\t\t});\n\t\tif (discography.errors) throw discography.errors[0].message;\n\n\t\tconst overview = await Spicetify.GraphQL.Request(queryArtistOverview, {\n\t\t\turi,\n\t\t\tlocale: Spicetify.Locale.getLocale(),\n\t\t\tincludePrerelease: false,\n\t\t});\n\t\tif (overview.errors) throw overview.errors[0].message;\n\n\t\tconst artistName = overview.data.artistUnion.profile.name;\n\t\tconst releases = discography.data.artistUnion.discography.all.items.flatMap(({ releases }) => releases.items);\n\n\t\tconst artistAlbums = releases.filter((album) => album.type === \"ALBUM\");\n\t\tconst artistSingles = releases.filter((album) => album.type === \"SINGLE\" || album.type === \"EP\");\n\n\t\tif (artistAlbums.length === 0 && artistSingles.length === 0) throw \"Artist has no releases\";\n\n\t\tconst allArtistAlbumsTracks = CONFIG.artistMode !== \"single\" ? await scanForTracksFromAlbums(artistAlbums, artistName, \"album\") : [];\n\t\tconst allArtistSinglesTracks = CONFIG.artistMode !== \"album\" ? await scanForTracksFromAlbums(artistSingles, artistName, \"single\") : [];\n\n\t\treturn allArtistAlbumsTracks.concat(allArtistSinglesTracks);\n\t}\n\n\tasync function fetchArtistLikedTracks(uri) {\n\t\tconst artistRes = await Spicetify.CosmosAsync.get(`sp://core-collection/unstable/@/list/tracks/artist/${uri}?responseFormat=protobufJson`);\n\n\t\tconst allTracks = artistRes.item?.map((artistTrack) => {\n\t\t\tif (artistTrack.trackMetadata.playable) return artistTrack.trackMetadata.link;\n\t\t});\n\n\t\treturn allTracks ?? [];\n\t}\n\n\tasync function fetchArtistTopTenTracks(uri) {\n\t\tconst { queryArtistOverview } = Spicetify.GraphQL.Definitions;\n\t\tconst { data, errors } = await Spicetify.GraphQL.Request(queryArtistOverview, {\n\t\t\turi,\n\t\t\tlocale: Spicetify.Locale.getLocale(),\n\t\t\tincludePrerelease: false,\n\t\t});\n\t\tif (errors) throw errors[0].message;\n\t\treturn data.artistUnion.discography.topTracks.items.map(({ track }) => track.uri);\n\t}\n\n\tasync function fetchLikedTracks() {\n\t\tconst res = await Spicetify.CosmosAsync.get(\"sp://core-collection/unstable/@/list/tracks/all?responseFormat=protobufJson\");\n\n\t\treturn res.item.filter((track) => track.trackMetadata.playable).map((track) => track.trackMetadata.link);\n\t}\n\n\tasync function fetchLocalTracks() {\n\t\tconst res = await Spicetify.Platform.LocalFilesAPI.getTracks();\n\n\t\treturn res.map((track) => track.uri);\n\t}\n\n\tfunction fetchQueue() {\n\t\tconst { _queueState } = Spicetify.Platform.PlayerAPI._queue;\n\t\tconst nextUp = _queueState.nextUp.map((track) => track.uri);\n\t\tconst queued = _queueState.queued.map((track) => track.uri);\n\t\tconst array = [...new Set([...nextUp, ...queued])];\n\t\tconst current = _queueState.current?.uri;\n\t\tif (current) array.push(current);\n\t\treturn array;\n\t}\n\n\tasync function fetchCollection(uriObj) {\n\t\tconst { category, type } = uriObj;\n\t\tconst { pathname } = Spicetify.Platform.History.location;\n\n\t\tswitch (type) {\n\t\t\tcase Type.TRACK:\n\t\t\tcase Type.LOCAL_TRACK:\n\t\t\t\tswitch (pathname) {\n\t\t\t\t\tcase \"/collection/tracks\":\n\t\t\t\t\t\treturn await fetchLikedTracks();\n\t\t\t\t\tcase \"/collection/local-files\":\n\t\t\t\t\t\treturn await fetchLocalTracks();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase Type.COLLECTION:\n\t\t\t\tswitch (category) {\n\t\t\t\t\tcase \"tracks\":\n\t\t\t\t\t\treturn await fetchLikedTracks();\n\t\t\t\t\tcase \"local-files\":\n\t\t\t\t\t\treturn await fetchLocalTracks();\n\t\t\t\t}\n\t\t}\n\t}\n\n\tasync function fetchShows(uri) {\n\t\tconst res = await Spicetify.CosmosAsync.get(`sp://core-show/v1/shows/${uri}?responseFormat=protobufJson`);\n\t\treturn res.items.filter((track) => track.episodePlayState.isPlayable).map((track) => track.episodeMetadata.link);\n\t}\n\n\tfunction shuffle(array) {\n\t\tlet counter = array.length;\n\t\tif (counter <= 1) return array;\n\n\t\t// While there are elements in the array\n\t\twhile (counter > 0) {\n\t\t\t// Pick a random index\n\t\t\tconst index = Math.floor(Math.random() * counter);\n\n\t\t\t// Decrease counter by 1\n\t\t\tcounter--;\n\n\t\t\t// And swap the last element with it\n\t\t\tconst temp = array[counter];\n\t\t\tarray[counter] = array[index];\n\t\t\tarray[index] = temp;\n\t\t}\n\t\treturn array.filter(Boolean);\n\t}\n\n\tasync function Queue(list, context, type) {\n\t\tconst count = list.length;\n\n\t\t// Delimits the end of our list, as Spotify may add new context tracks to the queue\n\t\tlist.push(\"spotify:delimiter\");\n\n\t\tconst { _queue, _client } = Spicetify.Platform.PlayerAPI._queue;\n\t\tconst { prevTracks, queueRevision } = _queue;\n\n\t\t// Format tracks with default values\n\t\tconst nextTracks = list.map((uri) => ({\n\t\t\tcontextTrack: {\n\t\t\t\turi,\n\t\t\t\tuid: \"\",\n\t\t\t\tmetadata: {\n\t\t\t\t\tis_queued: \"false\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tremoved: [],\n\t\t\tblocked: [],\n\t\t\tprovider: \"context\",\n\t\t}));\n\n\t\t// Lowest level setQueue method from vendor~xpui.js\n\t\t_client.setQueue({\n\t\t\tnextTracks,\n\t\t\tprevTracks,\n\t\t\tqueueRevision,\n\t\t});\n\n\t\tif (context) {\n\t\t\tconst { sessionId } = Spicetify.Platform.PlayerAPI.getState();\n\t\t\tSpicetify.Platform.PlayerAPI.updateContext(sessionId, {\n\t\t\t\turi: context,\n\t\t\t\turl: `context://${context}`,\n\t\t\t});\n\t\t}\n\n\t\tSpicetify.Player.next();\n\n\t\tswitch (type) {\n\t\t\tcase Type.ARTIST:\n\t\t\t\tif (CONFIG.artistMode === \"topTen\") {\n\t\t\t\t\tSpicetify.showNotification(`Shuffled Top ${count} Songs`);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (CONFIG.artistMode === \"likedSongArtist\") {\n\t\t\t\t\tSpicetify.showNotification(`Shuffled ${count} Liked Songs`);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (CONFIG.artistMode === \"single\") {\n\t\t\t\t\tSpicetify.showNotification(`Shuffled ${artistFetchTypeCount.single} Singles, Total of ${count} Songs`);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (CONFIG.artistMode === \"album\") {\n\t\t\t\t\tSpicetify.showNotification(`Shuffled ${artistFetchTypeCount.album} Albums, Total of ${count} Songs`);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tSpicetify.showNotification(`Shuffled ${artistFetchTypeCount.album} Albums, ${artistFetchTypeCount.single} Singles, Total of ${count} Songs`);\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tSpicetify.showNotification(`Shuffled ${count} Songs`);\n\t\t}\n\n\t\tartistFetchTypeCount.album = 0;\n\t\tartistFetchTypeCount.single = 0;\n\t}\n\n\tasync function fetchAndPlay(rawUri) {\n\t\tlet list;\n\t\tlet context;\n\t\tlet type = null;\n\t\tlet uri;\n\n\t\ttry {\n\t\t\tif (rawUri === \"queue\") {\n\t\t\t\tlist = fetchQueue();\n\t\t\t\tcontext = null;\n\t\t\t} else if (typeof rawUri === \"object\") {\n\t\t\t\tlist = rawUri;\n\t\t\t\tcontext = null;\n\t\t\t} else {\n\t\t\t\tconst uriObj = Spicetify.URI.fromString(rawUri);\n\t\t\t\ttype = uriObj.type;\n\t\t\t\turi = uriObj._base62Id ?? uriObj.id;\n\n\t\t\t\tswitch (type) {\n\t\t\t\t\tcase Type.PLAYLIST:\n\t\t\t\t\tcase Type.PLAYLIST_V2:\n\t\t\t\t\t\tlist = await fetchPlaylistTracks(uri);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase Type.ALBUM:\n\t\t\t\t\t\tlist = await fetchAlbumTracks(rawUri);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase `${Type.ARTIST}`:\n\t\t\t\t\t\tif (CONFIG.artistMode === \"likedSongArtist\") {\n\t\t\t\t\t\t\tlist = await fetchArtistLikedTracks(uri);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (CONFIG.artistMode === \"topTen\") {\n\t\t\t\t\t\t\tlist = await fetchArtistTopTenTracks(rawUri);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlist = await fetchArtistTracks(rawUri);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase Type.TRACK:\n\t\t\t\t\tcase Type.LOCAL_TRACK:\n\t\t\t\t\tcase Type.COLLECTION:\n\t\t\t\t\t\tlist = await fetchCollection(uriObj);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase Type.FOLDER:\n\t\t\t\t\t\tlist = await fetchFolderTracks(rawUri);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase Type.SHOW:\n\t\t\t\t\t\tlist = await fetchShows(uri);\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tif (!list?.length) {\n\t\t\t\t\tSpicetify.showNotification(\"Nothing to play\", true);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tcontext = rawUri;\n\t\t\t\tif (type === \"folder\" || type === \"collection\" || type === \"local\") {\n\t\t\t\t\tcontext = null;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawait Queue(shuffle(list), context, type);\n\t\t} catch (error) {\n\t\t\tSpicetify.showNotification(String(error), true);\n\t\t\tconsole.error(error);\n\t\t}\n\t}\n})();\n"
  },
  {
    "path": "Extensions/trashbin.js",
    "content": "// NAME: Trashbin\n// AUTHOR: khanhas and OhItsTom\n// DESCRIPTION: Throw songs to trashbin and never hear it again.\n\n/// <reference path=\"../globals.d.ts\" />\n\n(function TrashBin() {\n\tconst skipBackBtn =\n\t\tdocument.querySelector(\".main-skipBackButton-button\") ??\n\t\tdocument.querySelector(\".player-controls__left > button[data-encore-id='buttonTertiary']\");\n\tif (!Spicetify.Player.data || !Spicetify.LocalStorage || !skipBackBtn) {\n\t\tsetTimeout(TrashBin, 1000);\n\t\treturn;\n\t}\n\n\tfunction createButton(text, description, callback) {\n\t\tconst container = document.createElement(\"div\");\n\t\tcontainer.classList.add(\"setting-row\");\n\t\tcontainer.innerHTML = `\n\t\t<label class=\"col description\">${description}</label>\n\t\t<div class=\"col action\"><button class=\"reset\">${text}</button></div>\n\t\t`;\n\n\t\tconst button = container.querySelector(\"button.reset\");\n\t\tbutton.onclick = callback;\n\t\treturn container;\n\t}\n\n\tfunction createSlider(name, desc, defaultVal, callback) {\n\t\tconst container = document.createElement(\"div\");\n\t\tcontainer.classList.add(\"setting-row\");\n\t\tcontainer.innerHTML = `\n\t\t\t<label class=\"col description\">${desc}</label>\n\t\t\t<div class=\"col action\"><button class=\"switch\">\n\t\t\t<svg height=\"16\" width=\"16\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n\t\t\t${Spicetify.SVGIcons.check}\n\t\t\t</svg>\n\t\t\t</button></div>\n\t\t`;\n\n\t\tconst slider = container.querySelector(\"button.switch\");\n\t\tslider.classList.toggle(\"disabled\", !defaultVal);\n\n\t\tslider.onclick = () => {\n\t\t\tconst state = slider.classList.contains(\"disabled\");\n\t\t\tslider.classList.toggle(\"disabled\");\n\t\t\tSpicetify.LocalStorage.set(name, state);\n\t\t\tconsole.log(name, state);\n\t\t\tcallback(state);\n\t\t};\n\n\t\treturn container;\n\t}\n\n\tfunction settingsContent() {\n\t\t// Options\n\t\theader = document.createElement(\"h2\");\n\t\theader.innerText = \"Options\";\n\t\tcontent.appendChild(header);\n\n\t\tcontent.appendChild(createSlider(\"trashbin-enabled\", \"Enabled\", trashbinStatus, refreshEventListeners));\n\t\tcontent.appendChild(\n\t\t\tcreateSlider(\"TrashbinWidgetIcon\", \"Show Widget Icon\", enableWidget, (state) => {\n\t\t\t\tenableWidget = state;\n\t\t\t\tstate && trashbinStatus ? widget.register() : widget.deregister();\n\t\t\t})\n\t\t);\n\n\t\t// Local Storage\n\t\theader = document.createElement(\"h2\");\n\t\theader.innerText = \"Local Storage\";\n\t\tcontent.appendChild(header);\n\n\t\tcontent.appendChild(createButton(\"Copy\", \"Copy all items in trashbin to clipboard.\", copyItems));\n\t\tcontent.appendChild(createButton(\"Export\", \"Save all items in trashbin to a .json file.\", exportItems));\n\t\tcontent.appendChild(createButton(\"Import\", \"Overwrite all items in trashbin via .json file.\", importItems));\n\t\tcontent.appendChild(\n\t\t\tcreateButton(\"Clear \", \"Clear all items from trashbin (cannot be reverted).\", () => {\n\t\t\t\ttrashSongList = {};\n\t\t\t\ttrashArtistList = {};\n\t\t\t\tsetWidgetState(false);\n\t\t\t\tputDataLocal();\n\t\t\t\tSpicetify.showNotification(\"Trashbin cleared!\");\n\t\t\t})\n\t\t);\n\t}\n\n\tfunction styleSettings() {\n\t\tconst style = document.createElement(\"style\");\n\t\tstyle.innerHTML = `\n\t\t.main-trackCreditsModal-container {\n\t\t\twidth: auto !important;\n\t\t\tbackground-color: var(--spice-player) !important;\n\t\t}\n\n\t\t.setting-row::after {\n\t\t  content: \"\";\n\t\t  display: table;\n\t\t  clear: both;\n\t\t}\n\t\t.setting-row {\n\t\t  display: flex;\n\t\t  padding: 10px 0;\n\t\t  align-items: center;\n\t\t  justify-content: space-between;\n\t\t}\n\t\t.setting-row .col.description {\n\t\t  float: left;\n\t\t  padding-right: 15px;\n\t\t  width: 100%;\n\t\t}\n\t\t.setting-row .col.action {\n\t\t  float: right;\n\t\t  text-align: right;\n\t\t}\n\t\tbutton.switch {\n\t\t  align-items: center;\n\t\t  border: 0px;\n\t\t  border-radius: 50%;\n\t\t  background-color: rgba(var(--spice-rgb-shadow), .7);\n\t\t  color: var(--spice-text);\n\t\t  cursor: pointer;\n\t\t  display: flex;\n\t\t  margin-inline-start: 12px;\n\t\t  padding: 8px;\n\t\t}\n\t\tbutton.switch.disabled,\n\t\tbutton.switch[disabled] {\n\t\t  color: rgba(var(--spice-rgb-text), .3);\n\t\t}\n\t\tbutton.reset {\n\t\t  font-weight: 700;\n\t\t  font-size: medium;\n\t\t  background-color: transparent;\n\t\t  border-radius: 500px;\n\t\t  transition-duration: 33ms;\n\t\t  transition-property: background-color, border-color, color, box-shadow, filter, transform;\n\t\t  padding-inline: 15px;\n\t\t  border: 1px solid #727272;\n\t\t  color: var(--spice-text);\n\t\t  min-block-size: 32px;\n\t\t  cursor: pointer;\n\t\t}\n\t\tbutton.reset:hover {\n\t\t  transform: scale(1.04);\n\t\t  border-color: var(--spice-text);\n\t\t}`;\n\t\tcontent.appendChild(style);\n\t}\n\n\tfunction initValue(item, defaultValue) {\n\t\ttry {\n\t\t\tconst value = JSON.parse(Spicetify.LocalStorage.get(item));\n\t\t\treturn value ?? defaultValue;\n\t\t} catch {\n\t\t\treturn defaultValue;\n\t\t}\n\t}\n\n\t// Settings Variables - Initial Values\n\tlet trashbinStatus = initValue(\"trashbin-enabled\", true);\n\tlet enableWidget = initValue(\"TrashbinWidgetIcon\", true);\n\n\t// Settings Menu Initialization\n\tconst content = document.createElement(\"div\");\n\tstyleSettings();\n\tsettingsContent();\n\n\tconst trashbinIcon =\n\t\t'<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"currentcolor\"><path d=\"M5.25 3v-.917C5.25.933 6.183 0 7.333 0h1.334c1.15 0 2.083.933 2.083 2.083V3h4.75v1.5h-.972l-1.257 9.544A2.25 2.25 0 0 1 11.041 16H4.96a2.25 2.25 0 0 1-2.23-1.956L1.472 4.5H.5V3h4.75zm1.5-.917V3h2.5v-.917a.583.583 0 0 0-.583-.583H7.333a.583.583 0 0 0-.583.583zM2.986 4.5l1.23 9.348a.75.75 0 0 0 .744.652h6.08a.75.75 0 0 0 .744-.652L13.015 4.5H2.985z\"/></svg>';\n\n\tconst THROW_TEXT = \"Place in Trashbin\";\n\tconst UNTHROW_TEXT = \"Remove from Trashbin\";\n\n\tnew Spicetify.Menu.Item(\n\t\t\"Trashbin\",\n\t\tfalse,\n\t\t() => {\n\t\t\tSpicetify.PopupModal.display({\n\t\t\t\ttitle: \"Trashbin Settings\",\n\t\t\t\tcontent,\n\t\t\t});\n\t\t},\n\t\ttrashbinIcon\n\t).register();\n\n\tconst widget = new Spicetify.Playbar.Widget(\n\t\tTHROW_TEXT,\n\t\ttrashbinIcon,\n\t\t(self) => {\n\t\t\tconst uri = Spicetify.Player.data.item.uri;\n\t\t\tconst uriObj = Spicetify.URI.fromString(uri);\n\t\t\tconst type = uriObj.type;\n\n\t\t\tif (!trashSongList[uri]) {\n\t\t\t\ttrashSongList[uri] = true;\n\t\t\t\tif (shouldSkipCurrentTrack(uri, type)) Spicetify.Player.next();\n\t\t\t\tSpicetify.showNotification(\"Song added to trashbin\");\n\t\t\t} else {\n\t\t\t\tdelete trashSongList[uri];\n\t\t\t\tsetWidgetState(false);\n\t\t\t\tSpicetify.showNotification(\"Song removed from trashbin\");\n\t\t\t}\n\n\t\t\tputDataLocal();\n\t\t},\n\t\tfalse,\n\t\tfalse,\n\t\tenableWidget\n\t);\n\n\t// LocalStorage Setup\n\tlet trashSongList = initValue(\"TrashSongList\", {});\n\tlet trashArtistList = initValue(\"TrashArtistList\", {});\n\tlet userHitBack = false;\n\tconst eventListener = () => {\n\t\tuserHitBack = true;\n\t};\n\n\tputDataLocal();\n\trefreshEventListeners(trashbinStatus);\n\tsetWidgetState(\n\t\ttrashSongList[Spicetify.Player.data.item.uri],\n\t\tSpicetify.URI.fromString(Spicetify.Player.data.item.uri).type !== Spicetify.URI.Type.TRACK\n\t);\n\n\tfunction refreshEventListeners(state) {\n\t\ttrashbinStatus = state;\n\t\tif (state) {\n\t\t\tskipBackBtn.addEventListener(\"click\", eventListener);\n\t\t\tSpicetify.Player.addEventListener(\"songchange\", watchChange);\n\t\t\tenableWidget && widget.register();\n\t\t\twatchChange();\n\t\t} else {\n\t\t\tskipBackBtn.removeEventListener(\"click\", eventListener);\n\t\t\tSpicetify.Player.removeEventListener(\"songchange\", watchChange);\n\t\t\twidget.deregister();\n\t\t}\n\t}\n\n\tfunction setWidgetState(state, hidden = false) {\n\t\thidden ? widget.deregister() : enableWidget && widget.register();\n\t\twidget.active = !!state;\n\t\twidget.label = state ? UNTHROW_TEXT : THROW_TEXT;\n\t}\n\n\tfunction watchChange() {\n\t\tconst data = Spicetify.Player.data || Spicetify.Queue;\n\t\tif (!data) return;\n\n\t\tconst isBanned = trashSongList[data.item.uri];\n\t\tsetWidgetState(isBanned, Spicetify.URI.fromString(data.item.uri).type !== Spicetify.URI.Type.TRACK);\n\n\t\tif (userHitBack) {\n\t\t\tuserHitBack = false;\n\t\t\treturn;\n\t\t}\n\n\t\tif (isBanned) {\n\t\t\tSpicetify.Player.next();\n\t\t\treturn;\n\t\t}\n\n\t\tlet uriIndex = 0;\n\t\tlet artistUri = data.item.metadata.artist_uri;\n\n\t\twhile (artistUri) {\n\t\t\tif (trashArtistList[artistUri]) {\n\t\t\t\tSpicetify.Player.next();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\turiIndex++;\n\t\t\tartistUri = data.item.metadata[`artist_uri:${uriIndex}`];\n\t\t}\n\t}\n\n\t/**\n\t *\n\t * @param {string} uri\n\t * @param {string} type\n\t * @returns {boolean}\n\t */\n\tfunction shouldSkipCurrentTrack(uri, type) {\n\t\tconst curTrack = Spicetify.Player.data.item;\n\t\tif (type === Spicetify.URI.Type.TRACK) {\n\t\t\tif (uri === curTrack.uri) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\tif (type === Spicetify.URI.Type.ARTIST) {\n\t\t\tlet count = 1;\n\t\t\tlet artUri = curTrack.metadata.artist_uri;\n\t\t\twhile (artUri) {\n\t\t\t\tif (uri === artUri) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tartUri = curTrack.metadata[`artist_uri:${count}`];\n\t\t\t\tcount++;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t *\n\t * @param {string[]} uris\n\t */\n\tfunction toggleThrow(uris) {\n\t\tconst uri = uris[0];\n\t\tconst uriObj = Spicetify.URI.fromString(uri);\n\t\tconst type = uriObj.type;\n\n\t\tconst list = type === Spicetify.URI.Type.TRACK ? trashSongList : trashArtistList;\n\n\t\tif (!list[uri]) {\n\t\t\tlist[uri] = true;\n\t\t\tif (shouldSkipCurrentTrack(uri, type)) Spicetify.Player.next();\n\t\t\tSpicetify.Player.data?.item.uri === uri && setWidgetState(true);\n\t\t\tSpicetify.showNotification(type === Spicetify.URI.Type.TRACK ? \"Song added to trashbin\" : \"Artist added to trashbin\");\n\t\t} else {\n\t\t\tdelete list[uri];\n\t\t\tSpicetify.Player.data?.item.uri === uri && setWidgetState(false);\n\t\t\tSpicetify.showNotification(type === Spicetify.URI.Type.TRACK ? \"Song removed from trashbin\" : \"Artist removed from trashbin\");\n\t\t}\n\n\t\tputDataLocal();\n\t}\n\n\t/**\n\t * Only accept one track or artist URI\n\t * @param {string[]} uris\n\t * @returns {boolean}\n\t */\n\tfunction shouldAddContextMenu(uris) {\n\t\tif (uris.length > 1 || !trashbinStatus) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst uri = uris[0];\n\t\tconst uriObj = Spicetify.URI.fromString(uri);\n\t\tif (uriObj.type === Spicetify.URI.Type.TRACK) {\n\t\t\tthis.name = trashSongList[uri] ? UNTHROW_TEXT : THROW_TEXT;\n\t\t\treturn true;\n\t\t}\n\n\t\tif (uriObj.type === Spicetify.URI.Type.ARTIST) {\n\t\t\tthis.name = trashArtistList[uri] ? UNTHROW_TEXT : THROW_TEXT;\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\tconst cntxMenu = new Spicetify.ContextMenu.Item(THROW_TEXT, toggleThrow, shouldAddContextMenu, trashbinIcon);\n\tcntxMenu.register();\n\n\tfunction putDataLocal() {\n\t\tSpicetify.LocalStorage.set(\"TrashSongList\", JSON.stringify(trashSongList));\n\t\tSpicetify.LocalStorage.set(\"TrashArtistList\", JSON.stringify(trashArtistList));\n\t}\n\n\tfunction copyItems() {\n\t\tconst data = {\n\t\t\tsongs: trashSongList,\n\t\t\tartists: trashArtistList,\n\t\t};\n\t\tSpicetify.Platform.ClipboardAPI.copy(JSON.stringify(data));\n\t\tSpicetify.showNotification(\"Copied to clipboard\");\n\t}\n\n\tasync function exportItems() {\n\t\tconst data = {\n\t\t\tsongs: trashSongList,\n\t\t\tartists: trashArtistList,\n\t\t};\n\n\t\ttry {\n\t\t\tconst handle = await window.showSaveFilePicker({\n\t\t\t\tsuggestedName: \"spicetify-trashbin.json\",\n\t\t\t\ttypes: [\n\t\t\t\t\t{\n\t\t\t\t\t\tdescription: \"Spicetify trashbin backup\",\n\t\t\t\t\t\taccept: {\n\t\t\t\t\t\t\t\"application/json\": [\".json\"],\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t});\n\n\t\t\tconst writable = await handle.createWritable();\n\t\t\tawait writable.write(JSON.stringify(data));\n\t\t\tawait writable.close();\n\n\t\t\tSpicetify.showNotification(\"Backup saved succesfully.\");\n\t\t} catch {\n\t\t\tSpicetify.showNotification(\"Failed to save, try copying trashbin contents to clipboard and creating a backup manually.\");\n\t\t}\n\t}\n\n\tfunction importItems() {\n\t\tconst input = document.createElement(\"input\");\n\t\tinput.type = \"file\";\n\t\tinput.accept = \".json\";\n\t\tinput.onchange = (e) => {\n\t\t\tconst file = e.target.files[0];\n\t\t\tconst reader = new FileReader();\n\t\t\treader.onload = (e) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst data = JSON.parse(e.target.result);\n\t\t\t\t\ttrashSongList = data.songs;\n\t\t\t\t\ttrashArtistList = data.artists;\n\t\t\t\t\tputDataLocal();\n\t\t\t\t\tSpicetify.showNotification(\"File Import Successful!\");\n\t\t\t\t} catch (e) {\n\t\t\t\t\tSpicetify.showNotification(\"File Import Failed!\", true);\n\t\t\t\t\tconsole.error(e);\n\t\t\t\t}\n\t\t\t};\n\t\t\treader.onerror = () => {\n\t\t\t\tSpicetify.showNotification(\"File Read Failed!\", true);\n\t\t\t\tconsole.error(reader.error);\n\t\t\t};\n\t\t\treader.readAsText(file);\n\t\t};\n\t\tinput.click();\n\t}\n})();\n"
  },
  {
    "path": "Extensions/webnowplaying.js",
    "content": "// NAME: WebNowPlaying\n// AUTHOR: khanhas, keifufu (based on https://github.com/keifufu/WebNowPlaying-Redux)\n// DESCRIPTION: Provides media information and controls to WebNowPlaying-Redux-Rainmeter, but also supports WebNowPlaying for Rainmeter 0.5.0 and older.\n\n/// <reference path=\"../globals.d.ts\" />\n\n(function WebNowPlaying() {\n\tif (!Spicetify.CosmosAsync || !Spicetify.Platform.LibraryAPI) {\n\t\tsetTimeout(WebNowPlaying, 500);\n\t\treturn;\n\t}\n\n\tconst socket = new WNPReduxWebSocket();\n\twindow.addEventListener(\"beforeunload\", () => {\n\t\tsocket.close();\n\t});\n})();\n\nclass WNPReduxWebSocket {\n\t_ws = null;\n\tcache = new Map();\n\treconnectCount = 0;\n\tupdateInterval = null;\n\tcommunicationRevision = null;\n\tconnectionTimeout = null;\n\treconnectTimeout = null;\n\tisClosed = false;\n\tspicetifyInfo = {\n\t\tplayer: \"Spotify Desktop\",\n\t\tstate: \"STOPPED\",\n\t\ttitle: \"\",\n\t\tartist: \"\",\n\t\talbum: \"\",\n\t\tcover: \"\",\n\t\tduration: \"0:00\",\n\t\t// position and volume are fetched in sendUpdate()\n\t\tposition: \"0:00\",\n\t\tvolume: 100,\n\t\trating: 0,\n\t\trepeat: \"NONE\",\n\t\tshuffle: false,\n\t};\n\n\tconstructor() {\n\t\tthis.init();\n\n\t\tSpicetify.Player.addEventListener(\"songchange\", ({ data }) => this.updateSpicetifyInfo(data));\n\t\tSpicetify.Player.addEventListener(\"onplaypause\", ({ data }) => this.updateSpicetifyInfo(data));\n\t}\n\n\tupdateSpicetifyInfo(data) {\n\t\tif (!data?.item?.metadata) return;\n\t\tconst meta = data.item.metadata;\n\t\tthis.spicetifyInfo.title = meta.title;\n\t\tthis.spicetifyInfo.album = meta.album_title;\n\t\tthis.spicetifyInfo.duration = timeInSecondsToString(Math.round(Number.parseInt(meta.duration) / 1000));\n\t\tthis.spicetifyInfo.state = !data.isPaused ? \"PLAYING\" : \"PAUSED\";\n\t\tthis.spicetifyInfo.repeat = data.repeat === 2 ? \"ONE\" : data.repeat === 1 ? \"ALL\" : \"NONE\";\n\t\tthis.spicetifyInfo.shuffle = data.shuffle;\n\t\tthis.spicetifyInfo.artist = meta.artist_name;\n\t\tlet artistCount = 1;\n\t\twhile (meta[`artist_name:${artistCount}`]) {\n\t\t\tthis.spicetifyInfo.artist += `, ${meta[`artist_name:${artistCount}`]}`;\n\t\t\tartistCount++;\n\t\t}\n\t\tif (!this.spicetifyInfo.artist) this.spicetifyInfo.artist = meta.album_title; // Podcast\n\n\t\tSpicetify.Platform.LibraryAPI.contains(data.item.uri).then(([added]) => {\n\t\t\tthis.spicetifyInfo.rating = added ? 5 : 0;\n\t\t});\n\n\t\tconst cover = meta.image_xlarge_url;\n\t\tif (cover?.indexOf(\"localfile\") === -1) this.spicetifyInfo.cover = `https://i.scdn.co/image/${cover.substring(cover.lastIndexOf(\":\") + 1)}`;\n\t\telse this.spicetifyInfo.cover = \"\";\n\t}\n\n\tinit() {\n\t\ttry {\n\t\t\tthis._ws = new WebSocket(\"ws://localhost:8974\");\n\t\t\tthis._ws.onopen = this.onOpen.bind(this);\n\t\t\tthis._ws.onclose = this.onClose.bind(this);\n\t\t\tthis._ws.onerror = this.onError.bind(this);\n\t\t\tthis._ws.onmessage = this.onMessage.bind(this);\n\t\t} catch {\n\t\t\tthis.retry();\n\t\t}\n\t}\n\n\tclose(cleanupOnly = false) {\n\t\tif (!cleanupOnly) this.isClosed = true;\n\t\tthis.cache = new Map();\n\t\tthis.communicationRevision = null;\n\t\tif (this.reconnectTimeout) clearTimeout(this.reconnectTimeout);\n\t\tif (this.connectionTimeout) clearTimeout(this.connectionTimeout);\n\t\tif (this.ws) {\n\t\t\tthis.ws.onclose = null;\n\t\t\tthis.ws.close();\n\t\t}\n\t}\n\n\t// Clean up old variables and retry connection\n\tretry() {\n\t\tif (this.isClosed) return;\n\t\tthis.close(true);\n\t\t// Reconnects once per second for 30 seconds, then with a exponential backoff of (2^reconnectAttempts) up to 60 seconds\n\t\tthis.reconnectTimeout = setTimeout(\n\t\t\t() => {\n\t\t\t\tthis.init();\n\t\t\t\tthis.reconnectAttempts += 1;\n\t\t\t},\n\t\t\tMath.min(1000 * (this.reconnectAttempts <= 30 ? 1 : 2 ** (this.reconnectAttempts - 30)), 60000)\n\t\t);\n\t}\n\n\tsend(data) {\n\t\tif (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;\n\t\tthis._ws.send(data);\n\t}\n\n\tonOpen() {\n\t\tthis.reconnectCount = 0;\n\t\tthis.updateInterval = setInterval(this.sendUpdate.bind(this), 500);\n\t\t// If no communication revision is received within 1 second, assume it's WNP for Rainmeter < 0.5.0 (legacy)\n\t\tthis.connectionTimeout = setTimeout(() => {\n\t\t\tif (this.communicationRevision === null) this.communicationRevision = \"legacy\";\n\t\t}, 1000);\n\t}\n\n\tonClose() {\n\t\tthis.retry();\n\t}\n\n\tonError() {\n\t\tthis.retry();\n\t}\n\n\tonMessage(event) {\n\t\tif (this.communicationRevision) {\n\t\t\tswitch (this.communicationRevision) {\n\t\t\t\tcase \"legacy\":\n\t\t\t\t\tOnMessageLegacy(this, event.data);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"1\":\n\t\t\t\t\tOnMessageRev1(this, event.data);\n\t\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Sending an update immediately would normally do nothing, as it takes some time for\n\t\t\t// spicetifyInfo to be updated via the Cosmos subscription. However, we try to\n\t\t\t// optimistically update spicetifyInfo after receiving events.\n\t\t\tthis.sendUpdate();\n\t\t} else {\n\t\t\tif (event.data.startsWith(\"Version:\")) {\n\t\t\t\t// 'Version:' WNP for Rainmeter 0.5.0 (legacy)\n\t\t\t\tthis.communicationRevision = \"legacy\";\n\t\t\t} else if (event.data.startsWith(\"ADAPTER_VERSION \")) {\n\t\t\t\t// Any WNPRedux adapter will send 'ADAPTER_VERSION <version>;WNPRLIB_REVISION <revision>' after connecting\n\t\t\t\tthis.communicationRevision = event.data.split(\";\")[1].split(\" \")[1];\n\t\t\t} else {\n\t\t\t\t// The first message wasn't version related, so it's probably WNP for Rainmeter < 0.5.0 (legacy)\n\t\t\t\tthis.communicationRevision = \"legacy\";\n\t\t\t}\n\t\t}\n\t}\n\n\tsendUpdate() {\n\t\tif (!this._ws || this._ws.readyState !== WebSocket.OPEN) return;\n\t\tswitch (this.communicationRevision) {\n\t\t\tcase \"legacy\":\n\t\t\t\tSendUpdateLegacy(this);\n\t\t\t\tbreak;\n\t\t\tcase \"1\":\n\t\t\t\tSendUpdateRev1(this);\n\t\t\t\tbreak;\n\t\t}\n\t}\n}\n\nfunction OnMessageLegacy(self, message) {\n\t// Quite lengthy functions because we optimistically update spicetifyInfo after receiving events.\n\ttry {\n\t\tconst [type, data] = message.toUpperCase().split(\" \");\n\t\tswitch (type) {\n\t\t\tcase \"PLAYPAUSE\": {\n\t\t\t\tSpicetify.Player.togglePlay();\n\t\t\t\tself.spicetifyInfo.state = self.spicetifyInfo.state === \"PLAYING\" ? \"PAUSED\" : \"PLAYING\";\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"NEXT\":\n\t\t\t\tSpicetify.Player.next();\n\t\t\t\tbreak;\n\t\t\tcase \"PREVIOUS\":\n\t\t\t\tSpicetify.Player.back();\n\t\t\t\tbreak;\n\t\t\tcase \"SETPOSITION\": {\n\t\t\t\t// Example string: SetPosition 34:SetProgress 0,100890207715134:\n\t\t\t\tconst [, positionPercentage] = message.toUpperCase().split(\":\")[1].split(\"SETPROGRESS \");\n\t\t\t\tSpicetify.Player.seek(Number.parseFloat(positionPercentage.replace(\",\", \".\")));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"SETVOLUME\":\n\t\t\t\tSpicetify.Player.setVolume(Number.parseInt(data) / 100);\n\t\t\t\tbreak;\n\t\t\tcase \"REPEAT\": {\n\t\t\t\tSpicetify.Player.toggleRepeat();\n\t\t\t\tself.spicetifyInfo.repeat = self.spicetifyInfo.repeat === \"NONE\" ? \"ALL\" : self.spicetifyInfo.repeat === \"ALL\" ? \"ONE\" : \"NONE\";\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"SHUFFLE\": {\n\t\t\t\tSpicetify.Player.toggleShuffle();\n\t\t\t\tself.spicetifyInfo.shuffle = !self.spicetifyInfo.shuffle;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"TOGGLETHUMBSUP\": {\n\t\t\t\tSpicetify.Player.toggleHeart();\n\t\t\t\tself.spicetifyInfo.rating = self.spicetifyInfo.rating === 5 ? 0 : 5;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t// Spotify doesn't have a negative rating\n\t\t\t// case 'TOGGLETHUMBSDOWN': break\n\t\t\tcase \"RATING\": {\n\t\t\t\tconst rating = Number.parseInt(data);\n\t\t\t\tconst isLiked = self.spicetifyInfo.rating > 3;\n\t\t\t\tif (rating >= 3 && !isLiked) Spicetify.Player.toggleHeart();\n\t\t\t\telse if (rating < 3 && isLiked) Spicetify.Player.toggleHeart();\n\t\t\t\tself.spicetifyInfo.rating = rating;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t} catch (e) {\n\t\tself.send(`Error:Error sending event to ${self.spicetifyInfo.player}`);\n\t\tself.send(`ErrorD:${e}`);\n\t}\n}\n\nfunction SendUpdateLegacy(self) {\n\tif (!Spicetify.Player.data && cache.get(\"state\") !== 0) {\n\t\tcache.set(\"state\", 0);\n\t\tws.send(\"STATE:0\");\n\t\treturn;\n\t}\n\n\tself.spicetifyInfo.position = timeInSecondsToString(Math.round(Spicetify.Player.getProgress() / 1000));\n\tself.spicetifyInfo.volume = Math.round(Spicetify.Player.getVolume() * 100);\n\n\tfor (const key of Object.keys(self.spicetifyInfo)) {\n\t\ttry {\n\t\t\tlet value = self.spicetifyInfo[key];\n\t\t\t// For numbers, round it to an integer\n\t\t\tif (typeof value === \"number\") value = Math.round(value);\n\n\t\t\t// Conversion to legacy values\n\t\t\tif (key === \"state\") value = value === \"PLAYING\" ? 1 : value === \"PAUSED\" ? 2 : 0;\n\t\t\telse if (key === \"repeat\") value = value === \"ALL\" ? 2 : value === \"ONE\" ? 1 : 0;\n\t\t\telse if (key === \"shuffle\") value = value ? 1 : 0;\n\n\t\t\t// Check for null, and not just falsy, because 0 and '' are falsy\n\t\t\tif (value !== null && value !== self.cache.get(key)) {\n\t\t\t\tself.send(`${key.toUpperCase()}:${value}`);\n\t\t\t\tself.cache.set(key, value);\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tself.send(`Error: Error updating ${key} for ${self.spicetifyInfo.player}`);\n\t\t\tself.send(`ErrorD:${e}`);\n\t\t}\n\t}\n}\n\nfunction OnMessageRev1(self, message) {\n\t// Quite lengthy functions because we optimistically update spicetifyInfo after receiving events.\n\tconst [type, data] = message.split(\" \");\n\n\ttry {\n\t\tswitch (type) {\n\t\t\tcase \"TOGGLE_PLAYING\": {\n\t\t\t\tSpicetify.Player.togglePlay();\n\t\t\t\tself.spicetifyInfo.state = self.spicetifyInfo.state === \"PLAYING\" ? \"PAUSED\" : \"PLAYING\";\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"NEXT\":\n\t\t\t\tSpicetify.Player.next();\n\t\t\t\tbreak;\n\t\t\tcase \"PREVIOUS\":\n\t\t\t\tSpicetify.Player.back();\n\t\t\t\tbreak;\n\t\t\tcase \"SET_POSITION\": {\n\t\t\t\tconst [, positionPercentage] = data.split(\":\");\n\t\t\t\tSpicetify.Player.seek(Number.parseFloat(positionPercentage.replace(\",\", \".\")));\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"SET_VOLUME\":\n\t\t\t\tSpicetify.Player.setVolume(Number.parseInt(data) / 100);\n\t\t\t\tbreak;\n\t\t\tcase \"TOGGLE_REPEAT\": {\n\t\t\t\tSpicetify.Player.toggleRepeat();\n\t\t\t\tself.spicetifyInfo.repeat = self.spicetifyInfo.repeat === \"NONE\" ? \"ALL\" : self.spicetifyInfo.repeat === \"ALL\" ? \"ONE\" : \"NONE\";\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"TOGGLE_SHUFFLE\": {\n\t\t\t\tSpicetify.Player.toggleShuffle();\n\t\t\t\tself.spicetifyInfo.shuffle = !self.spicetifyInfo.shuffle;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"TOGGLE_THUMBS_UP\": {\n\t\t\t\tSpicetify.Player.toggleHeart();\n\t\t\t\tself.spicetifyInfo.rating = self.spicetifyInfo.rating === 5 ? 0 : 5;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\t// Spotify doesn't have a negative rating\n\t\t\t// case 'TOGGLE_THUMBS_DOWN': break\n\t\t\tcase \"SET_RATING\": {\n\t\t\t\tconst rating = Number.parseInt(data);\n\t\t\t\tconst isLiked = self.spicetifyInfo.rating > 3;\n\t\t\t\tif (rating >= 3 && !isLiked) Spicetify.Player.toggleHeart();\n\t\t\t\telse if (rating < 3 && isLiked) Spicetify.Player.toggleHeart();\n\t\t\t\tself.spicetifyInfo.rating = rating;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t} catch (e) {\n\t\tself.send(`ERROR Error sending event to ${self.spicetifyInfo.player}`);\n\t\tself.send(`ERRORDEBUG ${e}`);\n\t}\n}\n\nfunction SendUpdateRev1(self) {\n\tif (!Spicetify.Player.data && cache.get(\"state\") !== \"STOPPED\") {\n\t\tcache.set(\"state\", \"STOPPED\");\n\t\tws.send(\"STATE STOPPED\");\n\t\treturn;\n\t}\n\n\tself.spicetifyInfo.position = timeInSecondsToString(Math.round(Spicetify.Player.getProgress() / 1000));\n\tself.spicetifyInfo.volume = Math.round(Spicetify.Player.getVolume() * 100);\n\n\tfor (const key of Object.keys(self.spicetifyInfo)) {\n\t\ttry {\n\t\t\tlet value = self.spicetifyInfo[key];\n\t\t\t// For numbers, round it to an integer\n\t\t\tif (typeof value === \"number\") value = Math.round(value);\n\t\t\t// Check for null, and not just falsy, because 0 and '' are falsy\n\t\t\tif (value !== null && value !== self.cache.get(key)) {\n\t\t\t\tself.send(`${key.toUpperCase()} ${value}`);\n\t\t\t\tself.cache.set(key, value);\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tself.send(`ERROR Error updating ${key} for ${self.spicetifyInfo.player}`);\n\t\t\tself.send(`ERRORDEBUG ${e}`);\n\t\t}\n\t}\n}\n\n// Convert seconds to a time string acceptable to Rainmeter\nfunction pad(num, size) {\n\treturn num.toString().padStart(size, \"0\");\n}\nfunction timeInSecondsToString(timeInSeconds) {\n\tconst timeInMinutes = Math.floor(timeInSeconds / 60);\n\tif (timeInMinutes < 60) return `${timeInMinutes}:${pad(Math.floor(timeInSeconds % 60), 2)}`;\n\n\treturn `${Math.floor(timeInMinutes / 60)}:${pad(Math.floor(timeInMinutes % 60), 2)}:${pad(Math.floor(timeInSeconds % 60), 2)}`;\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "                  GNU LESSER GENERAL PUBLIC LICENSE\n                       Version 2.1, February 1999\n\n Copyright (C) 1991, 1999 Free Software Foundation, Inc.\n 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n[This is the first released version of the Lesser GPL.  It also counts\n as the successor of the GNU Library Public License, version 2, hence\n the version number 2.1.]\n\n                            Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicenses are intended to guarantee your freedom to share and change\nfree software--to make sure the software is free for all its users.\n\n  This license, the Lesser General Public License, applies to some\nspecially designated software packages--typically libraries--of the\nFree Software Foundation and other authors who decide to use it.  You\ncan use it too, but we suggest you first think carefully about whether\nthis license or the ordinary General Public License is the better\nstrategy to use in any particular case, based on the explanations below.\n\n  When we speak of free software, we are referring to freedom of use,\nnot price.  Our General Public Licenses are designed to make sure that\nyou have the freedom to distribute copies of free software (and charge\nfor this service if you wish); that you receive source code or can get\nit if you want it; that you can change the software and use pieces of\nit in new free programs; and that you are informed that you can do\nthese things.\n\n  To protect your rights, we need to make restrictions that forbid\ndistributors to deny you these rights or to ask you to surrender these\nrights.  These restrictions translate to certain responsibilities for\nyou if you distribute copies of the library or if you modify it.\n\n  For example, if you distribute copies of the library, whether gratis\nor for a fee, you must give the recipients all the rights that we gave\nyou.  You must make sure that they, too, receive or can get the source\ncode.  If you link other code with the library, you must provide\ncomplete object files to the recipients, so that they can relink them\nwith the library after making changes to the library and recompiling\nit.  And you must show them these terms so they know their rights.\n\n  We protect your rights with a two-step method: (1) we copyright the\nlibrary, and (2) we offer you this license, which gives you legal\npermission to copy, distribute and/or modify the library.\n\n  To protect each distributor, we want to make it very clear that\nthere is no warranty for the free library.  Also, if the library is\nmodified by someone else and passed on, the recipients should know\nthat what they have is not the original version, so that the original\nauthor's reputation will not be affected by problems that might be\nintroduced by others.\n\n  Finally, software patents pose a constant threat to the existence of\nany free program.  We wish to make sure that a company cannot\neffectively restrict the users of a free program by obtaining a\nrestrictive license from a patent holder.  Therefore, we insist that\nany patent license obtained for a version of the library must be\nconsistent with the full freedom of use specified in this license.\n\n  Most GNU software, including some libraries, is covered by the\nordinary GNU General Public License.  This license, the GNU Lesser\nGeneral Public License, applies to certain designated libraries, and\nis quite different from the ordinary General Public License.  We use\nthis license for certain libraries in order to permit linking those\nlibraries into non-free programs.\n\n  When a program is linked with a library, whether statically or using\na shared library, the combination of the two is legally speaking a\ncombined work, a derivative of the original library.  The ordinary\nGeneral Public License therefore permits such linking only if the\nentire combination fits its criteria of freedom.  The Lesser General\nPublic License permits more lax criteria for linking other code with\nthe library.\n\n  We call this license the \"Lesser\" General Public License because it\ndoes Less to protect the user's freedom than the ordinary General\nPublic License.  It also provides other free software developers Less\nof an advantage over competing non-free programs.  These disadvantages\nare the reason we use the ordinary General Public License for many\nlibraries.  However, the Lesser license provides advantages in certain\nspecial circumstances.\n\n  For example, on rare occasions, there may be a special need to\nencourage the widest possible use of a certain library, so that it becomes\na de-facto standard.  To achieve this, non-free programs must be\nallowed to use the library.  A more frequent case is that a free\nlibrary does the same job as widely used non-free libraries.  In this\ncase, there is little to gain by limiting the free library to free\nsoftware only, so we use the Lesser General Public License.\n\n  In other cases, permission to use a particular library in non-free\nprograms enables a greater number of people to use a large body of\nfree software.  For example, permission to use the GNU C Library in\nnon-free programs enables many more people to use the whole GNU\noperating system, as well as its variant, the GNU/Linux operating\nsystem.\n\n  Although the Lesser General Public License is Less protective of the\nusers' freedom, it does ensure that the user of a program that is\nlinked with the Library has the freedom and the wherewithal to run\nthat program using a modified version of the Library.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.  Pay close attention to the difference between a\n\"work based on the library\" and a \"work that uses the library\".  The\nformer contains code derived from the library, whereas the latter must\nbe combined with the library in order to run.\n\n                  GNU LESSER GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License Agreement applies to any software library or other\nprogram which contains a notice placed by the copyright holder or\nother authorized party saying it may be distributed under the terms of\nthis Lesser General Public License (also called \"this License\").\nEach licensee is addressed as \"you\".\n\n  A \"library\" means a collection of software functions and/or data\nprepared so as to be conveniently linked with application programs\n(which use some of those functions and data) to form executables.\n\n  The \"Library\", below, refers to any such software library or work\nwhich has been distributed under these terms.  A \"work based on the\nLibrary\" means either the Library or any derivative work under\ncopyright law: that is to say, a work containing the Library or a\nportion of it, either verbatim or with modifications and/or translated\nstraightforwardly into another language.  (Hereinafter, translation is\nincluded without limitation in the term \"modification\".)\n\n  \"Source code\" for a work means the preferred form of the work for\nmaking modifications to it.  For a library, complete source code means\nall the source code for all modules it contains, plus any associated\ninterface definition files, plus the scripts used to control compilation\nand installation of the library.\n\n  Activities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning a program using the Library is not restricted, and output from\nsuch a program is covered only if its contents constitute a work based\non the Library (independent of the use of the Library in a tool for\nwriting it).  Whether that is true depends on what the Library does\nand what the program that uses the Library does.\n\n  1. You may copy and distribute verbatim copies of the Library's\ncomplete source code as you receive it, in any medium, provided that\nyou conspicuously and appropriately publish on each copy an\nappropriate copyright notice and disclaimer of warranty; keep intact\nall the notices that refer to this License and to the absence of any\nwarranty; and distribute a copy of this License along with the\nLibrary.\n\n  You may charge a fee for the physical act of transferring a copy,\nand you may at your option offer warranty protection in exchange for a\nfee.\n\n  2. You may modify your copy or copies of the Library or any portion\nof it, thus forming a work based on the Library, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) The modified work must itself be a software library.\n\n    b) You must cause the files modified to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    c) You must cause the whole of the work to be licensed at no\n    charge to all third parties under the terms of this License.\n\n    d) If a facility in the modified Library refers to a function or a\n    table of data to be supplied by an application program that uses\n    the facility, other than as an argument passed when the facility\n    is invoked, then you must make a good faith effort to ensure that,\n    in the event an application does not supply such function or\n    table, the facility still operates, and performs whatever part of\n    its purpose remains meaningful.\n\n    (For example, a function in a library to compute square roots has\n    a purpose that is entirely well-defined independent of the\n    application.  Therefore, Subsection 2d requires that any\n    application-supplied function or table used by this function must\n    be optional: if the application does not supply it, the square\n    root function must still compute square roots.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Library,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Library, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote\nit.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Library.\n\nIn addition, mere aggregation of another work not based on the Library\nwith the Library (or with a work based on the Library) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may opt to apply the terms of the ordinary GNU General Public\nLicense instead of this License to a given copy of the Library.  To do\nthis, you must alter all the notices that refer to this License, so\nthat they refer to the ordinary GNU General Public License, version 2,\ninstead of to this License.  (If a newer version than version 2 of the\nordinary GNU General Public License has appeared, then you can specify\nthat version instead if you wish.)  Do not make any other change in\nthese notices.\n\n  Once this change is made in a given copy, it is irreversible for\nthat copy, so the ordinary GNU General Public License applies to all\nsubsequent copies and derivative works made from that copy.\n\n  This option is useful when you wish to copy part of the code of\nthe Library into a program that is not a library.\n\n  4. You may copy and distribute the Library (or a portion or\nderivative of it, under Section 2) in object code or executable form\nunder the terms of Sections 1 and 2 above provided that you accompany\nit with the complete corresponding machine-readable source code, which\nmust be distributed under the terms of Sections 1 and 2 above on a\nmedium customarily used for software interchange.\n\n  If distribution of object code is made by offering access to copy\nfrom a designated place, then offering equivalent access to copy the\nsource code from the same place satisfies the requirement to\ndistribute the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  5. A program that contains no derivative of any portion of the\nLibrary, but is designed to work with the Library by being compiled or\nlinked with it, is called a \"work that uses the Library\".  Such a\nwork, in isolation, is not a derivative work of the Library, and\ntherefore falls outside the scope of this License.\n\n  However, linking a \"work that uses the Library\" with the Library\ncreates an executable that is a derivative of the Library (because it\ncontains portions of the Library), rather than a \"work that uses the\nlibrary\".  The executable is therefore covered by this License.\nSection 6 states terms for distribution of such executables.\n\n  When a \"work that uses the Library\" uses material from a header file\nthat is part of the Library, the object code for the work may be a\nderivative work of the Library even though the source code is not.\nWhether this is true is especially significant if the work can be\nlinked without the Library, or if the work is itself a library.  The\nthreshold for this to be true is not precisely defined by law.\n\n  If such an object file uses only numerical parameters, data\nstructure layouts and accessors, and small macros and small inline\nfunctions (ten lines or less in length), then the use of the object\nfile is unrestricted, regardless of whether it is legally a derivative\nwork.  (Executables containing this object code plus portions of the\nLibrary will still fall under Section 6.)\n\n  Otherwise, if the work is a derivative of the Library, you may\ndistribute the object code for the work under the terms of Section 6.\nAny executables containing that work also fall under Section 6,\nwhether or not they are linked directly with the Library itself.\n\n  6. As an exception to the Sections above, you may also combine or\nlink a \"work that uses the Library\" with the Library to produce a\nwork containing portions of the Library, and distribute that work\nunder terms of your choice, provided that the terms permit\nmodification of the work for the customer's own use and reverse\nengineering for debugging such modifications.\n\n  You must give prominent notice with each copy of the work that the\nLibrary is used in it and that the Library and its use are covered by\nthis License.  You must supply a copy of this License.  If the work\nduring execution displays copyright notices, you must include the\ncopyright notice for the Library among them, as well as a reference\ndirecting the user to the copy of this License.  Also, you must do one\nof these things:\n\n    a) Accompany the work with the complete corresponding\n    machine-readable source code for the Library including whatever\n    changes were used in the work (which must be distributed under\n    Sections 1 and 2 above); and, if the work is an executable linked\n    with the Library, with the complete machine-readable \"work that\n    uses the Library\", as object code and/or source code, so that the\n    user can modify the Library and then relink to produce a modified\n    executable containing the modified Library.  (It is understood\n    that the user who changes the contents of definitions files in the\n    Library will not necessarily be able to recompile the application\n    to use the modified definitions.)\n\n    b) Use a suitable shared library mechanism for linking with the\n    Library.  A suitable mechanism is one that (1) uses at run time a\n    copy of the library already present on the user's computer system,\n    rather than copying library functions into the executable, and (2)\n    will operate properly with a modified version of the library, if\n    the user installs one, as long as the modified version is\n    interface-compatible with the version that the work was made with.\n\n    c) Accompany the work with a written offer, valid for at\n    least three years, to give the same user the materials\n    specified in Subsection 6a, above, for a charge no more\n    than the cost of performing this distribution.\n\n    d) If distribution of the work is made by offering access to copy\n    from a designated place, offer equivalent access to copy the above\n    specified materials from the same place.\n\n    e) Verify that the user has already received a copy of these\n    materials or that you have already sent this user a copy.\n\n  For an executable, the required form of the \"work that uses the\nLibrary\" must include any data and utility programs needed for\nreproducing the executable from it.  However, as a special exception,\nthe materials to be distributed need not include anything that is\nnormally distributed (in either source or binary form) with the major\ncomponents (compiler, kernel, and so on) of the operating system on\nwhich the executable runs, unless that component itself accompanies\nthe executable.\n\n  It may happen that this requirement contradicts the license\nrestrictions of other proprietary libraries that do not normally\naccompany the operating system.  Such a contradiction means you cannot\nuse both them and the Library together in an executable that you\ndistribute.\n\n  7. You may place library facilities that are a work based on the\nLibrary side-by-side in a single library together with other library\nfacilities not covered by this License, and distribute such a combined\nlibrary, provided that the separate distribution of the work based on\nthe Library and of the other library facilities is otherwise\npermitted, and provided that you do these two things:\n\n    a) Accompany the combined library with a copy of the same work\n    based on the Library, uncombined with any other library\n    facilities.  This must be distributed under the terms of the\n    Sections above.\n\n    b) Give prominent notice with the combined library of the fact\n    that part of it is a work based on the Library, and explaining\n    where to find the accompanying uncombined form of the same work.\n\n  8. You may not copy, modify, sublicense, link with, or distribute\nthe Library except as expressly provided under this License.  Any\nattempt otherwise to copy, modify, sublicense, link with, or\ndistribute the Library is void, and will automatically terminate your\nrights under this License.  However, parties who have received copies,\nor rights, from you under this License will not have their licenses\nterminated so long as such parties remain in full compliance.\n\n  9. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Library or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Library (or any work based on the\nLibrary), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Library or works based on it.\n\n  10. Each time you redistribute the Library (or any work based on the\nLibrary), the recipient automatically receives a license from the\noriginal licensor to copy, distribute, link with or modify the Library\nsubject to these terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties with\nthis License.\n\n  11. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Library at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Library by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Library.\n\nIf any portion of this section is held invalid or unenforceable under any\nparticular circumstance, the balance of the section is intended to apply,\nand the section as a whole is intended to apply in other circumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  12. If the distribution and/or use of the Library is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Library under this License may add\nan explicit geographical distribution limitation excluding those countries,\nso that distribution is permitted only in or among countries not thus\nexcluded.  In such case, this License incorporates the limitation as if\nwritten in the body of this License.\n\n  13. The Free Software Foundation may publish revised and/or new\nversions of the Lesser General Public License from time to time.\nSuch new versions will be similar in spirit to the present version,\nbut may differ in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Library\nspecifies a version number of this License which applies to it and\n\"any later version\", you have the option of following the terms and\nconditions either of that version or of any later version published by\nthe Free Software Foundation.  If the Library does not specify a\nlicense version number, you may choose any version ever published by\nthe Free Software Foundation.\n\n  14. If you wish to incorporate parts of the Library into other free\nprograms whose distribution conditions are incompatible with these,\nwrite to the author to ask for permission.  For software which is\ncopyrighted by the Free Software Foundation, write to the Free\nSoftware Foundation; we sometimes make exceptions for this.  Our\ndecision will be guided by the two goals of preserving the free status\nof all derivatives of our free software and of promoting the sharing\nand reuse of software generally.\n\n                            NO WARRANTY\n\n  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO\nWARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\nEXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\nOTHER PARTIES PROVIDE THE LIBRARY \"AS IS\" WITHOUT WARRANTY OF ANY\nKIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE\nLIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME\nTHE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\nWRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\nAND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU\nFOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR\nCONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE\nLIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING\nRENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A\nFAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF\nSUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH\nDAMAGES.\n\n                     END OF TERMS AND CONDITIONS\n\n           How to Apply These Terms to Your New Libraries\n\n  If you develop a new library, and you want it to be of the greatest\npossible use to the public, we recommend making it free software that\neveryone can redistribute and change.  You can do so by permitting\nredistribution under these terms (or, alternatively, under the terms of the\nordinary General Public License).\n\n  To apply these terms, attach the following notices to the library.  It is\nsafest to attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least the\n\"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the library's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This library is free software; you can redistribute it and/or\n    modify it under the terms of the GNU Lesser General Public\n    License as published by the Free Software Foundation; either\n    version 2.1 of the License, or (at your option) any later version.\n\n    This library is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n    Lesser General Public License for more details.\n\n    You should have received a copy of the GNU Lesser General Public\n    License along with this library; if not, write to the Free Software\n    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301\n    USA\n\nAlso add information on how to contact you by electronic and paper mail.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the library, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the\n  library `Frob' (a library for tweaking knobs) written by James Random\n  Hacker.\n\n  <signature of Ty Coon>, 1 April 1990\n  Ty Coon, President of Vice\n\nThat's all there is to it!\n"
  },
  {
    "path": "README.md",
    "content": "<h3 align=\"center\"><a href=\"https://spicetify.app/\"><img src=\"https://i.imgur.com/iwcLITQ.png\" width=\"600px\"></a></h3>\n<p align=\"center\">\n  <a href=\"https://goreportcard.com/report/github.com/spicetify/cli\"><img src=\"https://goreportcard.com/badge/github.com/spicetify/cli\"></a>\n  <a href=\"https://github.com/spicetify/cli/releases/latest\"><img src=\"https://img.shields.io/github/release/spicetify/cli/all.svg?colorB=97CA00&label=latest%20version\"></a>\n  <a href=\"https://github.com/spicetify/cli/releases\"><img src=\"https://img.shields.io/github/downloads/spicetify/cli/total.svg?colorB=97CA00&label=total%20downloads\"></a>\n  <a href=\"https://discord.gg/VnevqPp2Rr\"><img src=\"https://img.shields.io/discord/842219447716151306?label=chat&logo=discord&logoColor=discord\"></a>\n</p>\n\n---\n\nCommand-line tool to customize the official Spotify client.\nSupports Windows, MacOS and Linux.\n\n<img src=\".github/assets/logo.png\" alt=\"img\" align=\"right\" width=\"560px\" height=\"400px\">\n\n### Features\n\n- Change colors across the User Interface\n- Inject CSS for advanced customization\n- Inject Extensions to extend functionalities, manipulate UI and control player\n- Inject Custom Apps\n- Make yourself in control of the Spotify client\n\n### Links\n\n- [Installation](https://spicetify.app/docs/getting-started)\n- [Basic Usage](https://spicetify.app/docs/getting-started#basic-usage)\n\n---\n\n### Code Signing Policy\n\nFree code signing provided by [SignPath.io](https://signpath.io), certificate by [SignPath Foundation](https://signpath.org/).\n"
  },
  {
    "path": "Themes/SpicetifyDefault/color.ini",
    "content": "; COLORS KEYS DESCRIPTION\n; text               = Main field text; playlist names in main field and sidebar; headings.\n; subtext            = Text in main sidebar buttons; playlist names in sidebar; artist names and mini infos.\n; main               = Main field background.\n; main-elevated      = Backgrounds for objects above the main field.\n; highlight          = Highlight background for hovering over objects.\n; highlight-elevated = Highlight colors for objects above the main field.\n; sidebar            = Sidebar background.\n; player             = Player background.\n; card               = Card background on hover; player area outline.\n; shadow             = Card drop shadow; button background.\n; selected row       = Color of selected song, scrollbar, caption and playlist details, download and options buttons.\n; button             = Playlist button background in sidebar; drop-down menus; now playing song; play button background; like button.\n; button-active      = Active play button background.\n; button-disabled    = Seekbar and volume bar background.\n; tab-active         = Tabbar active item background in header.\n; notification       = Notification toast.\n; notification-error = Error notification toast.\n; misc               = Miscellaneous.\n\n[green-dark]\n; Light green on Dark Blue background\ntext               = FFFFFF\nsubtext            = DEDEDE\nmain               = 2E2837\nsidebar            = 2E2837\nplayer             = 2E2837\ncard               = 483b5b\nshadow             = 202020\nselected-row       = cdcdcd\nbutton             = 00e089\nbutton-active      = 483b5b\nbutton-disabled    = 535353\ntab-active         = 483b5b\nnotification       = 00e089\nnotification-error = e22134\nmisc               = BFBFBF\n\n[nord-light]\ntext               = 2E3440\nsubtext            = 3b4252\nmain               = ECEFF4\nsidebar            = ECEFF4\nplayer             = e5e9f0\ncard               = 88c0d0\nshadow             = eceff4\nselected-row       = 9ea4af\nbutton             = 88c0d0\nbutton-active      = d8dee9\nbutton-disabled    = c0c0c0\ntab-active         = d8dee9\nnotification       = 88c0d0\nnotification-error = e22134\nmisc               = BFBFBF\n\n[nord-dark]\ntext               = D8DEE9\nsubtext            = ECEFF4\nmain               = 2e3440\nsidebar            = 2e3440\nplayer             = 2e3440\ncard               = 3b4252\nshadow             = 4c566a\nselected-row       = e5e9f0\nbutton             = 5E81AC\nbutton-active      = 434c5e\nbutton-disabled    = 434c5e\ntab-active         = 434c5e\nnotification       = 5E81AC\nnotification-error = e22134\nmisc               = BFBFBF\n\n[pink-white]\n; Pink on White background\ntext               = 000000\nsubtext            = 3D3D3D\nmain               = FAFAFA\nsidebar            = FAFAFA\nplayer             = FAFAFA\ncard               = FE6F61\nshadow             = F0F0F0\nselected-row       = 404040\nbutton             = FE6F61\nbutton-active      = e9e9e9\nbutton-disabled    = 535353\ntab-active         = e9e9e9\nnotification       = FE6F61\nnotification-error = e22134\nmisc               = BFBFBF\n\n[purple]\ntext               = FFFFFF\nsubtext            = F0F0F0\nmain               = 0A0E14\nsidebar            = 0A0E14\nplayer             = 0A0E14\ncard               = 6F3C89\nshadow             = 1f1525\nselected-row       = 909090\nbutton             = 6F3C89\nbutton-active      = 795b84\nbutton-disabled    = 535353\ntab-active         = 795b84\nnotification       = 6F3C89\nnotification-error = e22134\nmisc               = BFBFBF\n\n[dracula]\ntext               = FFFFFF\nsubtext            = d8dee9\nmain               = 282a36\nsidebar            = 282a36\nplayer             = 282a36\ncard               = 6272a4\nshadow             = 44475a\nselected-row       = F0F0F0\nbutton             = ffb86c\nbutton-active      = 44475a\nbutton-disabled    = 535353\ntab-active         = 44475a\nnotification       = ffb86c\nnotification-error = e22134\nmisc               = BFBFBF\n\n"
  },
  {
    "path": "Themes/SpicetifyDefault/user.css",
    "content": ":root {\n\t--player-bar-height: 105px;\n}\n\n.main-rootlist-rootlistDividerGradient {\n\tbackground: unset;\n}\n\ninput {\n\tbackground-color: unset !important;\n\tborder-bottom: solid 1px var(--spice-text) !important;\n\tborder-radius: 0 !important;\n\tpadding: 6px 10px 6px 48px;\n\tcolor: var(--spice-text) !important;\n}\n\n.x-searchInput-searchInputSearchIcon,\n.x-searchInput-searchInputClearButton {\n\tcolor: var(--spice-text) !important;\n}\n\n.main-home-homeHeader,\n.x-entityHeader-overlay,\n.x-actionBarBackground-background,\n.main-actionBarBackground-background,\n.main-entityHeader-overlay,\n.main-entityHeader-backgroundColor {\n\tbackground-color: unset !important;\n\tbackground-image: unset !important;\n}\n\n.main-playButton-PlayButton.main-playButton-primary {\n\tcolor: white;\n}\n\n.connect-title,\n.connect-header {\n\tdisplay: none;\n}\n\n.connect-device-list {\n\tmargin: 0px -5px;\n}\n\n/* Remove Topbar background colour */\n.main-topBar-background {\n\tbackground-color: unset !important;\n}\n.main-topBar-overlay {\n\tbackground-color: var(--spice-main);\n}\n\n.main-entityHeader-shadow,\n.connect-device-list-container {\n\tbox-shadow: 0 4px 20px rgba(var(--spice-rgb-shadow), 0.2);\n}\n\n.main-trackList-playingIcon {\n\tfilter: grayscale(1);\n}\n\n.main-trackList-trackListRow.main-trackList-active:hover .main-trackList-rowMarker,\n.main-trackList-trackListRow.main-trackList-active:hover .main-trackList-rowTitle,\n.main-trackList-trackListRow.main-trackList-active:focus-within .main-trackList-rowMarker,\n.main-trackList-trackListRow.main-trackList-active:focus-within .main-trackList-rowTitle {\n\tcolor: var(--spice-button);\n}\n\n.main-entityHeader-metaDataText,\n.main-duration-container {\n\tcolor: var(--spice-subtext);\n}\n\nspan.artist-artistVerifiedBadge-badge svg:nth-child(1) {\n\tfill: black;\n}\n\n/* Full window artist background */\n.main-entityHeader-background.main-entityHeader-gradient {\n\topacity: 0.3;\n}\n\n.main-entityHeader-container.main-entityHeader-withBackgroundImage,\n.main-entityHeader-background,\n.main-entityHeader-background.main-entityHeader-overlay:after {\n\theight: 100vh;\n}\n\n.main-entityHeader-withBackgroundImage .main-entityHeader-headerText {\n\tjustify-content: center;\n}\n\n.main-entityHeader-container.main-entityHeader-nonWrapped.main-entityHeader-withBackgroundImage {\n\tpadding-left: 9%;\n}\n\n.main-entityHeader-background.main-entityHeader-overlay:after {\n\tbackground-image: linear-gradient(transparent, transparent), linear-gradient(var(--spice-main), var(--spice-main));\n}\n\n.artist-artistOverview-overview .main-entityHeader-withBackgroundImage h1 {\n\tfont-size: 175px !important;\n\tline-height: 175px !important;\n}\n\n/** Hightlight selected playlist */\n.main-rootlist-rootlistItemLink.main-rootlist-rootlistItemLinkActive {\n\tbackground: var(--spice-button);\n\tborder-radius: 4px;\n\tpadding: 0 10px;\n\tmargin: 0 5px 0 -10px;\n}\n\n.main-navBar-navBarLinkActive {\n\tbackground: var(--spice-button);\n}\n\ndiv.GlueDropTarget.personal-library > *.active {\n\tbackground: var(--spice-button) !important;\n}\n\n.main-contextMenu-menu {\n\tbackground-color: var(--spice-button-active);\n}\n\n.main-contextMenu-menuHeading,\n.main-contextMenu-menuItemButton,\n.main-contextMenu-menuItemButton:not(.main-contextMenu-disabled):focus,\n.main-contextMenu-menuItemButton:not(.main-contextMenu-disabled):hover {\n\tcolor: var(--spice-text);\n}\n\n.main-playPauseButton-button {\n\tbackground-color: var(--spice-button);\n\tcolor: white;\n}\n\n/** Queue page header */\n.queue-queue-title,\n.queue-playHistory-title {\n\tcolor: var(--spice-text) !important;\n}\n\n/** Cards */\n.main-cardImage-imageWrapper {\n\tbackground-color: transparent;\n}\n\n/** Sidebar */\n.main-rootlist-rootlistDivider {\n\tmargin-bottom: 8px;\n}\n\n.main-rootlist-rootlistPlaylistsScrollNode {\n\tpadding: 0;\n}\n\n#spicetify-playlist-list {\n\tpadding-top: 8px;\n}\n.main-collectionLinkButton-collectionLinkButton .main-collectionLinkButton-icon,\n.main-collectionLinkButton-collectionLinkButton .main-collectionLinkButton-collectionLinkText {\n\topacity: 1;\n}\n\n.link-subtle {\n\tcolor: var(--spice-text);\n}\n\n/** Player bar */\n.main-nowPlayingBar-nowPlayingBar {\n\theight: var(--player-bar-height);\n}\n\n/** Buddy bar */\n.main-buddyFeed-activityMetadata .main-buddyFeed-artistAndTrackName a,\n.main-buddyFeed-activityMetadata .main-buddyFeed-username a,\n.main-buddyFeed-activityMetadata .main-buddyFeed-playbackContextLink {\n\tcolor: var(--spice-text);\n}\n"
  },
  {
    "path": "biome.json",
    "content": "{\n\t\"$schema\": \"https://biomejs.dev/schemas/2.0.0/schema.json\",\n\t\"assist\": {\n\t\t\"actions\": {\n\t\t\t\"source\": {\n\t\t\t\t\"organizeImports\": \"on\"\n\t\t\t}\n\t\t}\n\t},\n\t\"linter\": {\n\t\t\"enabled\": true,\n\t\t\"rules\": {\n\t\t\t\"recommended\": true,\n\t\t\t\"suspicious\": {\n\t\t\t\t\"noExplicitAny\": \"off\",\n\t\t\t\t\"useIterableCallbackReturn\": \"off\"\n\t\t\t}\n\t\t}\n\t},\n\t\"formatter\": {\n\t\t\"enabled\": true,\n\t\t\"formatWithErrors\": true,\n\t\t\"indentStyle\": \"tab\",\n\t\t\"indentWidth\": 2,\n\t\t\"lineWidth\": 150\n\t},\n\t\"javascript\": {\n\t\t\"formatter\": {\n\t\t\t\"trailingCommas\": \"es5\",\n\t\t\t\"arrowParentheses\": \"always\"\n\t\t}\n\t},\n\t\"vcs\": {\n\t\t\"enabled\": true,\n\t\t\"clientKind\": \"git\",\n\t\t\"useIgnoreFile\": true,\n\t\t\"defaultBranch\": \"main\"\n\t}\n}\n"
  },
  {
    "path": "css-map.json",
    "content": "{\n\t\"n8Bz0c0v17whD3KfMdOk\": \"album-albumPage-sectionWrapper\",\n\t\"HPNSn7d7aZf4nfre61sk\": \"artist-artistAbout-about\",\n\t\"xaeunxBdlShScWay5mQR\": \"artist-artistAbout-artistGridContainer\",\n\t\"RHlPX8sl9gW1cmKdm1E9\": \"artist-artistAbout-artistOverviewContainer\",\n\t\"TPcD_1UTh6DS32DfZxhA\": \"artist-artistAbout-artistOverviewContent\",\n\t\"Ev5jVfYlqmjaZh6ziail\": \"artist-artistAbout-artistShelfSpacer\",\n\t\"N3mpQyWevHz7lrgLkOBM\": \"artist-artistAbout-artistSides\",\n\t\"TxASgfgEtA4JmUkhkNUN\": \"artist-artistAbout-artistSidesBlock\",\n\t\"PAlX44btlstKkPJfe2uU\": \"artist-artistAbout-avatar\",\n\t\"k4MNlyGrhsL0qgnENxqh\": \"artist-artistAbout-backgroundImage\",\n\t\"DRXonbAbVN5Vg9anDL1X\": \"artist-artistAbout-backgroundImage\",\n\t\"CjnwbSTpODW56Gerg7X6\": \"artist-artistAbout-bio\",\n\t\"xbKOOJ_NjLijBvdpAudQ\": \"artist-artistAbout-bio\",\n\t\"TV2j1oIRIkKH_6D1xP82\": \"artist-artistAbout-bioContainer\",\n\t\"Fzu8dfizHyYedxyDjaQ2\": \"artist-artistAbout-bioContainer\",\n\t\"NPv26QCDgdnwsPOlYJmQ\": \"artist-artistAbout-cityBlock\",\n\t\"Q_OUHp7iDNLBcO2ZYI2x\": \"artist-artistAbout-cityBlock\",\n\t\"mKUj4tXOjumlcv0q9Qta\": \"artist-artistAbout-close\",\n\t\"MHIOvvlSYRmF7VAJDLWy\": \"artist-artistAbout-close\",\n\t\"y_GaLKy76zj71zPYkLrs\": \"artist-artistAbout-columnContainer\",\n\t\"ml8qANY77Ah3fx5VTjoe\": \"artist-artistAbout-columnContainer\",\n\t\"uhDzVbFHyCQDH6WrWZaC\": \"artist-artistAbout-container\",\n\t\"jW4eWdr_LUeOXwPpKhWG\": \"artist-artistAbout-container\",\n\t\"BPDHTIIFueJMvtDPZttw\": \"artist-artistAbout-content\",\n\t\"ejNsts52hRq0uZcc_NXi\": \"artist-artistAbout-content\",\n\t\"H3lKAypjVfPoxoQkkCJQ\": \"artist-artistAbout-events\",\n\t\"XbP5GUst__JF8uVtbozd\": \"artist-artistAbout-heading\",\n\t\"oMsgQk2LPTIE4yinSHOQ\": \"artist-artistAbout-image\",\n\t\"e2S7FcJ1QgVLlcUbl2Kn\": \"artist-artistAbout-merch\",\n\t\"Er5rIejLMh22M0OiF8wH\": \"artist-artistAbout-merchShelfContent\",\n\t\"iZ_BsoftrZBB78FRQ3Yq\": \"artist-artistAbout-merchSmall\",\n\t\"NiqTdDnFi5KAXoGukrdo\": \"artist-artistAbout-modal\",\n\t\"pSn5vOGhTHswAWTC_3tE\": \"artist-artistAbout-modal\",\n\t\"KUUFCMOavSKiLbBMXu0f\": \"artist-artistAbout-modalAfterOpen\",\n\t\"hIhnD0UMvYHG6TnluwnX\": \"artist-artistAbout-modalBase\",\n\t\"Xr7SnQlqJFl6FSMeiSsw\": \"artist-artistAbout-modalBeforeClose\",\n\t\"yzXX2lW3wGayC_cdup_m\": \"artist-artistAbout-overview\",\n\t\"fPw1FkQQDEJvxJwTalo5\": \"artist-artistAbout-popularTracks\",\n\t\"Apg1EshcUJs6y4ITDWwV\": \"artist-artistAbout-popularTracksBlock\",\n\t\"hdS8jsvbJhsJXHAZlWwz\": \"artist-artistAbout-postedBy\",\n\t\"OyLgnahIHw63684ABRBF\": \"artist-artistAbout-postedBy\",\n\t\"KguYS3oppxDrMNpTUJcB\": \"artist-artistAbout-postedByAvatar\",\n\t\"mM_me6VjRfHLPkqAMqEI\": \"artist-artistAbout-postedByAvatar\",\n\t\"tLjX9htIKD_OCmEX01UN\": \"artist-artistAbout-rank\",\n\t\"tQp8UOu8jGduQXUTcv0c\": \"artist-artistAbout-rank\",\n\t\"ZjfaJlGQZ42nCWjD3FDm\": \"artist-artistAbout-scrollbars\",\n\t\"pHrwZOFBdT8FNXnmcPPI\": \"artist-artistAbout-scrollbars\",\n\t\"y72RJ2LhCkAyAfZ8tzUt\": \"artist-artistAbout-sideBlock\",\n\t\"muHL0_3HjlqTZDoapgc9\": \"artist-artistAbout-social\",\n\t\"DVWIV41y6daOMjQKR8Zj\": \"artist-artistAbout-socialsWrapper\",\n\t\"TkEnV1bItcSb11Yb8h7h\": \"artist-artistAbout-stats\",\n\t\"upZIQtiBy1Tr0ZbvhnSL\": \"artist-artistAbout-stats\",\n\t\"T_AmQPlZ6wvE819I7A0D\": \"artist-artistAbout-statsContainer\",\n\t\"ndIZG_atdpv_tBZtqQhk\": \"artist-artistAbout-statsContainer\",\n\t\"GB_nwkJosBf_NLdZAgAx\": \"artist-artistDiscography-active\",\n\t\"cFKW94askf6sbyOYlRqe\": \"artist-artistDiscography-artistName\",\n\t\"Ihlsi5QU3AnIfSgnhorS\": \"artist-artistDiscography-button\",\n\t\"h8z21OhjsxoY1IvpR1QP\": \"artist-artistDiscography-cardGrid\",\n\t\"XJrBgo4PVAKtiOwymZAH\": \"artist-artistDiscography-cardGrid\",\n\t\"Rz6JI4bCxJVeSWe6LtEX\": \"artist-artistDiscography-firstAlbum\",\n\t\"mtGNWthH4lVSrpVPuOZh\": \"artist-artistDiscography-firstAlbum\",\n\t\"O61y0bIacmTFxhhbL1Bl\": \"artist-artistDiscography-headerButtons\",\n\t\"W4CDdWdxBiLFn6s7y3y4\": \"artist-artistDiscography-headerButtons\",\n\t\"fEvxx8vl3zTNWsuC8lpx\": \"artist-artistDiscography-headerContainer\",\n\t\"tMm_GLWkp1_AE8JeAU8l\": \"artist-artistDiscography-headerContainer\",\n\t\"n1EzbHQahSKztskTUAm3\": \"artist-artistDiscography-headerImage\",\n\t\"zHSMgfu24oyo70WbnxJh\": \"artist-artistDiscography-headerImage\",\n\t\"hyHkMMynp3uUsmEtOkSN\": \"artist-artistDiscography-headerMetadata\",\n\t\"dPZaWJR4cdScdIwM6Sjx\": \"artist-artistDiscography-headerMetadata\",\n\t\"kRb29P1Fo5blFMPRDSdq\": \"artist-artistDiscography-headerTitle\",\n\t\"tAFzDJjlr14lei2t_KYl\": \"artist-artistDiscography-headerTitle\",\n\t\"P7nfAtcLXgPiUCJrlUjA\": \"artist-artistDiscography-topBar\",\n\t\"EvQHNTBhaU3rGCRRlAWj\": \"artist-artistDiscography-topBar\",\n\t\"mxGhI8Lr7MoGOnFrbsUa\": \"artist-artistDiscography-topBar\",\n\t\"jOcv0cG2WD_3xsPvxwOe\": \"artist-artistDiscography-topBar\",\n\t\"QplCuuGSoV4updqTSLq9\": \"artist-artistDiscography-topBarScrolled\",\n\t\"oxpxKtAxG4E2DCK0AJo9\": \"artist-artistDiscography-tracklist\",\n\t\"JjWADiu6y90rHjdue6qI\": \"artist-artistDiscography-tracklist\",\n\t\"QQasfrfLlxtbcdiJP40s\": \"artist-artistOffers-description\",\n\t\"F6RR_jytWUwdg_jiv0Ss\": \"artist-artistOffers-icon\",\n\t\"TXVCwyY2iLFPRMSav251\": \"artist-artistOffers-info\",\n\t\"RmO04klyCgpCtMspkDMH\": \"artist-artistOffers-item\",\n\t\"crweb403jWwGSZngXpHU\": \"artist-artistOffers-name\",\n\t\"nMgXBztFcmHCjT0dRQDx\": \"artist-artistOffers-pic\",\n\t\"N5_bW94EwvHFbWWoRucu\": \"artist-artistOnTour-condensed\",\n\t\"gyHBzoYLUrTsDmijyGQ7\": \"artist-artistOnTour-date\",\n\t\"J8F7vs26LDSof1E_qwmN\": \"artist-artistOnTour-dateDay\",\n\t\"VbaPExA4ISCqk6SuSe2H\": \"artist-artistOnTour-dateMonth\",\n\t\"P5l28MUwDXvStz4OF0IE\": \"artist-artistOnTour-elevated\",\n\t\"rTTxX5loXbtazDI0NLAj\": \"artist-artistOnTour-eventName\",\n\t\"j1MetdstqbpfRVFl1W01\": \"artist-artistOnTour-info\",\n\t\"viRsT27AlJDd4bZyiH_k\": \"artist-artistOnTour-item\",\n\t\"h3FvLvcLCaK12strxe2F\": \"artist-artistOnTour-location\",\n\t\"ERcmpF077M1XT1X0fIxB\": \"artist-artistOnTour-metadata\",\n\t\"Oq3xjFrLaBAabx847xea\": \"artist-artistOnTour-onTourShelfGrid\",\n\t\"n46Gs0Xh2zys0a2KwSRS\": \"artist-artistOnTour-time\",\n\t\"IUF5ct6RQtva_f_DQqg0\": \"artist-artistOnTour-timeAndVenue\",\n\t\"dvPKZDevT0Llia4bmaJX\": \"artist-artistOnTour-timeAndVenueText\",\n\t\"RpOhZK9h4ccL8wsEgdwQ\": \"artist-artistOnTour-title\",\n\t\"kgR8s9v7IzY4G17ZtLbw\": \"artist-artistOverview-about\",\n\t\"Sl7tzXKh5stpn_y5prU5\": \"artist-artistOverview-artistOverviewContainer\",\n\t\"BL__GuO2JsHMR6RgNfwY\": \"artist-artistOverview-artistOverviewContent\",\n\t\"_7rRmILTMMuCiW77ERpvL\": \"artist-artistOverview-artistShelfRelatedVideos\",\n\t\"jJ1PYBjdJy5plPbVprT1\": \"artist-artistOverview-artistShelfSpacer\",\n\t\"EsgR8WWJIEa9L2wBAKxg\": \"artist-artistOverview-artistSides\",\n\t\"_yUo81yeoON6wNqCV_ud\": \"artist-artistOverview-artistSidesBlock\",\n\t\"qsfp4wSDOv6LBCbFApI1\": \"artist-artistOverview-events\",\n\t\"hLkn_NwPkY5VIAETCqCK\": \"artist-artistOverview-heading\",\n\t\"_7MwXzTGa7JMhVlJCeDJ9\": \"artist-artistOverview-merch\",\n\t\"NOFtFAj4dIAsXQgVHJcA\": \"artist-artistOverview-merchShelfContent\",\n\t\"ODvt8oN68o6Iy8nxwOuY\": \"artist-artistOverview-merchSmall\",\n\t\"NLaDALU71zxOtBUbsrfH\": \"artist-artistOverview-overview\",\n\t\"q9GR66ZTM4HH42Z_qroQ\": \"artist-artistOverview-popularTracks\",\n\t\"yTBLU_z7yk9Xp_oN48Q2\": \"artist-artistOverview-popularTracksBlock\",\n\t\"uVh80THccLWIzZYwgUaQ\": \"artist-artistOverview-sideBlock\",\n\t\"b0NcxAbHvRbqgs2S8QDg\": \"artist-artistVerifiedBadge-badge\",\n\t\"bn2UNQDs5GLY5rjp5Ljh\": \"artist-artistVerifiedBadge-fillColor\",\n\t\"CmR9tHJ5ta6oWJlKBm3k\": \"artist-artistVerifiedBadge-wrapper\",\n\t\"idI9vydtCzXVhU1BaKLw\": \"artist-followButton-button\",\n\t\"Qq16641w1flRfBavPaAn\": \"artist-followButton-disabled\",\n\t\"fEbcweEiUPPy2eaIaD3F\": \"artist-followButton-followed\",\n\t\"wi2HeHXOI471ZOh8ncCG\": \"artist-popularTrackList-seeMore\",\n\t\"YV8aNrRwNwFaMozM_Bfo\": \"collection-collection-adBarEnabled\",\n\t\"Hbwta1_s02edtdkgJ7qZ\": \"collection-collection-AddToPlaylistIcon\",\n\t\"nHGnfkVXCk9YYVitV6eu\": \"collection-collection-emptyStateContainer\",\n\t\"fLqEiE5HwKr9eFCbJgeu\": \"collection-collection-header\",\n\t\"tUg9tRrSVbtjHJ4WHnxw\": \"collection-collection-tabBar\",\n\t\"ZzUvBwE6uj5CAU2pjlQX\": \"collection-collectionEntityHeroCard-container\",\n\t\"xhPzQCyks9pfizJj_PVK\": \"collection-collectionEntityHeroCard-descriptionContainer\",\n\t\"DY2YrJ5MH3ddIhZGORBq\": \"collection-collectionEntityHeroCard-headerContainer\",\n\t\"NGSgqDuaOH2VDApkzoxN\": \"collection-collectionEntityHeroCard-likedSongs\",\n\t\"QoUF6etsDFiL979QR7aC\": \"collection-collectionEntityHeroCard-moreText\",\n\t\"d8ifuAZX8mK644AwlRZK\": \"collection-collectionEntityHeroCard-opacityText\",\n\t\"lBs1v3T3HAPrOxfyWemr\": \"collection-collectionEntityHeroCard-skeletonRow\",\n\t\"ZXOyJPokUIObMnvTOJvc\": \"collection-collectionEntityHeroCard-tracksContainer\",\n\t\"kST1INfbHSxzydJffBq_\": \"collection-collectionEntityHeroCard-yourEpisodes\",\n\t\"VuUpAVlUW_OfMfOcDEID\": \"collection-searchBar-searchBar\",\n\t\"Agho45p8dD6jGmTpdSlp\": \"cover-art\",\n\t\"H0HbpIM3UrcupWIAjLWu\": \"cover-art\",\n\t\"W0TACB7OY0iXtKVOtEhY\": \"cover-art-auto-height\",\n\t\"dyzKbJJHavNOBo6bGSpK\": \"cover-art-icon\",\n\t\"zmOtW0vqqn1qpZrtQ_w9\": \"cover-art-icon\",\n\t\"_bmrtgr4_Tgsoiaz4c85\": \"cover-art-image\",\n\t\"FqmFsMhuF4D0s35Z62Js\": \"cover-art-image\",\n\t\"tAcAbTTcWl8G7S0wL4E8\": \"desktopmodals-aboutSpotifyModal-closeButton\",\n\t\"GSFvITwD84dS2JA62Mtj\": \"desktopmodals-aboutSpotifyModal-closeButton\",\n\t\"DIO4lfVN1pwro7Yg360w\": \"desktopmodals-aboutSpotifyModal-container\",\n\t\"UnLGG6p932k7WyjkB9Vo\": \"desktopmodals-aboutSpotifyModal-container\",\n\t\"_h3KfYcbuvm0TopPfACQ\": \"desktopmodals-aboutSpotifyModal-content\",\n\t\"KlzblASEYfUfaykBFZgM\": \"desktopmodals-aboutSpotifyModal-content\",\n\t\"_Hx5g6RUtUkydJULdjOA\": \"desktopmodals-aboutSpotifyModal-copyright\",\n\t\"XF1XXenkrbdAK2rRoxoU\": \"desktopmodals-aboutSpotifyModal-copyright\",\n\t\"FNHhAAEYgKErDNsJevtS\": \"desktopmodals-aboutSpotifyModal-licensing\",\n\t\"Ifnz1lh1jjvqPqJ4KPo8\": \"desktopmodals-aboutSpotifyModal-licensing\",\n\t\"qsKpcFrhrA8KtuTVIN_y\": \"desktopmodals-licensesModal-buttonContainer\",\n\t\"uYKs_kQMPOziaeDj877B\": \"desktopmodals-licensesModal-container\",\n\t\"i8qeSJJVx4PXb7fsvOTd\": \"desktopmodals-licensesModal-content\",\n\t\"WhIzm3S3R6Ker3XvpYW6\": \"desktopmodals-licensesModal-licensesFrame\",\n\t\"dn48DOL23H8N3jN80yFW\": \"desktopmodals-versionStatus-container\",\n\t\"qi0hX8uXrbQyS6tvdDBt\": \"desktopmodals-versionStatus-container\",\n\t\"O7Pq_TuNMtSn0zigOncV\": \"desktopmodals-versionStatus-content\",\n\t\"WomzHWnDO_yFyjnkd49P\": \"desktopmodals-versionStatus-content\",\n\t\"sU_Wp3eWSzMrHnkUmbdI\": \"desktopmodals-versionStatus-copyButton\",\n\t\"R83hOohwVshnd6bEkDO4\": \"desktopmodals-versionStatus-copyButton\",\n\t\"YJMECPbMHWgMUs8RFdcV\": \"folder-folderPage-sectionWrapper\",\n\t\"zogFp9G1AEqb8AKOd5B0\": \"GenericModal\",\n\t\"I3zkdnuhFFrZ1Rr1BJhb\": \"GenericModal__overlay\",\n\t\"ISA4xMVe89BiwxjIENN1\": \"GenericModal__overlay\",\n\t\"cUwQnQoE3OqXqSYLT0hv\": \"link-subtle\",\n\t\"VUXMMFKWudUWE1kIXZoS\": \"link-subtle\",\n\t\"rC9xwL4gaksmshIjHbNn\": \"link-subtle\",\n\t\"iKgf4UDhbRTHxmZSuAEc\": \"lyrics-lyrics-adLeaderboardIsEnabled\",\n\t\"L9xhJOJnV2OL5Chm3Jew\": \"lyrics-lyrics-background\",\n\t\"o4GE4jG5_QICak2JK_bn\": \"lyrics-lyrics-background\",\n\t\"RFThkjLuWfPUO9shrMOZ\": \"lyrics-lyrics-background\",\n\t\"tr8V5eHsUaIkOYVw7eSG\": \"lyrics-lyrics-container\",\n\t\"FUYNhisXTCmbzt9IDxnT\": \"lyrics-lyrics-container\",\n\t\"lofIAg8Ixko3mfBrbfej\": \"lyrics-lyrics-container\",\n\t\"Q2RPoHcoxygOoPLXLMww\": \"lyrics-lyrics-contentContainer\",\n\t\"gqaWFmQeKNYnYD5gRv3x\": \"lyrics-lyrics-contentContainer\",\n\t\"_EzvsrEJ47TI8hxzRoKx\": \"lyrics-lyrics-contentContainer\",\n\t\"esRByMgBY3TiENAsbDHA\": \"lyrics-lyrics-contentWrapper\",\n\t\"_Wna90no0o0dta47Heiw\": \"lyrics-lyrics-contentWrapper\",\n\t\"t_dtt9KL1wnNRvRO_y5L\": \"lyrics-lyrics-contentWrapper\",\n\t\"iPBJpp5EVkRE24N6vmGA\": \"lyrics-lyrics-coverTopBar\",\n\t\"SaEkeiyzAoXnWVSDiTR7\": \"lyrics-lyrics-vocalRemoval\",\n\t\"arY01KDGhWNgzlAHlhpd\": \"lyrics-lyricsContent-active\",\n\t\"EhKgYshvOwpSrTv399Mw\": \"lyrics-lyricsContent-active\",\n\t\"_gZrl2ExJwyxPy1pEUG2\": \"lyrics-lyricsContent-active\",\n\t\"iq4cgi0YEKr6DGaTtzUj\": \"lyrics-lyricsContent-description\",\n\t\"MEjuIn9iTBQbnCqHpkoQ\": \"lyrics-lyricsContent-highlight\",\n\t\"aeO5D7ulxy19q4qNBrkk\": \"lyrics-lyricsContent-highlight\",\n\t\"ve52ddYhoAd3Xf27Zxfm\": \"lyrics-lyricsContent-highlight\",\n\t\"_LKG3z7SnerR0eigPCoK\": \"lyrics-lyricsContent-isInteractive\",\n\t\"vapgYYF2HMEeLJuOWGq5\": \"lyrics-lyricsContent-isInteractive\",\n\t\"FQYXZaa0aDIrse54YlYO\": \"lyrics-lyricsContent-isInteractive\",\n\t\"NiCdLCpp3o2z6nBrayOn\": \"lyrics-lyricsContent-lyric\",\n\t\"nw6rbs8R08fpPn7RWW2w\": \"lyrics-lyricsContent-lyric\",\n\t\"BJ1zQ_ReY3QPaS7SW46s\": \"lyrics-lyricsContent-lyric\",\n\t\"o69qODXrbOkf6Tv7fa51\": \"lyrics-lyricsContent-lyric\",\n\t\"kGR_hu4tdj9PnUlSPaRL\": \"lyrics-lyricsContent-provider\",\n\t\"LomBcMvfM8AEmZGquAdj\": \"lyrics-lyricsContent-provider\",\n\t\"adSF6zkjcpNDto9qhTdV\": \"lyrics-lyricsContent-provider\",\n\t\"A3ohAQNHsDIMv2EM3Ytp\": \"lyrics-lyricsContent-text\",\n\t\"BXlQFspJp_jq9SKhUSP3\": \"lyrics-lyricsContent-text\",\n\t\"MmIREVIj8A2aFVvBZ2Ev\": \"lyrics-lyricsContent-text\",\n\t\"HxblHEsl2WX2yhubfVIc\": \"lyrics-lyricsContent-unsynced\",\n\t\"SruqsAzX8rUtY2isUZDF\": \"lyrics-lyricsContent-unsynced\",\n\t\"E4q8ogfdWtye7YgotBlN\": \"main-actionBar-ActionBar\",\n\t\"AKrNYHB1rFTyzXauXAtQ\": \"main-actionBar-ActionBar\",\n\t\"eSg4ntPU2KQLfpLGXAww\": \"main-actionBar-ActionBarRow\",\n\t\"WWMs8ddvWoRMkjWcRY2W\": \"main-actionBar-ActionBarRow\",\n\t\"K06ol8ltPT_atXE_JjUP\": \"main-actionBar-exploreButton\",\n\t\"pRm6ynptw_JVaNIGT44l\": \"main-actionBar-exploreButton\",\n\t\"CoLO4pdSl8LGWyVZA00t\": \"main-actionBarBackground-background\",\n\t\"PkOz5g82CaoKk1J3GX0e\": \"main-actionBarBackground-background\",\n\t\"us5p2PlqI8IJvxWYakKj\": \"main-actionBarBackground-background\",\n\t\"GTAFfOA_w5vh_bDaGJAG\": \"main-actionButtons\",\n\t\"NPvLJSRWfv1Joo8dF0D8\": \"main-actionButtons\",\n\t\"POrR9X4pOmhyjTr88Eag\": \"main-actionButtons\",\n\t\"P83IYsmHg0RH_STU1ENU\": \"main-actionButtons\",\n\t\"WIGgdAaAzrXm7f_loaXi\": \"main-actionButtons-button\",\n\t\"rRF_r7EyCHjZv058JACi\": \"main-addButton-active\",\n\t\"SPC4uzYXJmknkCGKpxHw\": \"main-addButton-active\",\n\t\"Fm7C3gdh5Lsc9qSXrQwO\": \"main-addButton-button\",\n\t\"RbsCNNM9a0WkFCM2UzBA\": \"main-addButton-button\",\n\t\"nzUMgk_XBD4uFFgA_LOI\": \"main-addButton-disabled\",\n\t\"GolAZOzxNIKbfZdIApC_\": \"main-addButton-helmet\",\n\t\"LuK6ZGXdwxSW92X3FodG\": \"main-addButton-zoomInAnimation\",\n\t\"uKyQmBSHiTyLvZlrFHLE\": \"main-appShell-card\",\n\t\"fGX1fnzBkGAAMfMxDXDe\": \"main-appShell-cards\",\n\t\"uMdsIJ2RlIvMQObefThj\": \"main-appShell-cardsHeader\",\n\t\"ucE6L4XRyB9F_RXDPVuR\": \"main-appShell-cardsWrapper\",\n\t\"FKzolQvIeUcXEOsjtw8l\": \"main-appShell-container\",\n\t\"vIGYXwggXAn1Xc_qD5LA\": \"main-appShell-logo\",\n\t\"HMyLin1O_6Ae2odnit1u\": \"main-appShell-mainContent\",\n\t\"qqVgbIIvqVxAT7KPTjM6\": \"main-appShell-navItems\",\n\t\"D1z00v8mUgOxx_my43Yq\": \"main-appShell-playbackBar\",\n\t\"rLrSnwagmkEsLoI1InYT\": \"main-appShell-sideBar\",\n\t\"Fe4_ZnxnNSMxu7vzbntV\": \"main-appShell-topBar\",\n\t\"tp8rO9vtqBGPLOhwcdYv\": \"main-avatar-avatar\",\n\t\"LKfKy7bXKmlkMEANVJMS\": \"main-avatar-avatar\",\n\t\"_4JsFsdrKufWvrC44apn\": \"main-avatar-avatar\",\n\t\"Xz3tlahv16UpqKBW5HdK\": \"main-avatar-image\",\n\t\"Xgnpn7u01MvGEhekIGLT\": \"main-avatar-image\",\n\t\"yzLEMgdzXgAnIZZwMHO1\": \"main-avatar-piled\",\n\t\"ENopS3htmKy15q_QCR2j\": \"main-avatar-piledIcon\",\n\t\"m95Ymx847hCaxHjmyXKX\": \"main-avatar-placeholderTransparent\",\n\t\"KdxlBanhDJjzmHfqhP0X\": \"main-avatar-placeholderWrapper\",\n\t\"BzunmwrVMyWGpopPJRt2\": \"main-avatar-withBadge\",\n\t\"YEaaNScT6lyJCVBeQoxd\": \"main-buddyFeed-actions\",\n\t\"EaZyLTjK9Rd_s6A8aYVw\": \"main-buddyFeed-actions\",\n\t\"_Xe25F2aC59Kljgqyw3G\": \"main-buddyFeed-activityMetadata\",\n\t\"j7K7_Zly3G1HS9MlKoao\": \"main-buddyFeed-addFriendPlaceholder\",\n\t\"gY0qK1gmEdhq5idwQc8C\": \"main-buddyFeed-addFriendPlaceholderBtn\",\n\t\"cBPbfBWIIPDzhIT6i3ih\": \"main-buddyFeed-addFriendPlaceholderText\",\n\t\"mthrc5U9wb8F4zMBqlPy\": \"main-buddyFeed-artistAndTrackName\",\n\t\"dLg5WMgjh1kfYtZ_MnZz\": \"main-buddyFeed-avatarContainer\",\n\t\"ythYrlFSBm1P_ltHc8e1\": \"main-buddyFeed-buddyFeed\",\n\t\"t345U9kQY1pF704d79oY\": \"main-buddyFeed-closeButton\",\n\t\"fjCfUvlYgfexObyYQqFM\": \"main-buddyFeed-closeButton\",\n\t\"eZCD3dqbvZaABVAhIniT\": \"main-buddyFeed-closeContainer\",\n\t\"Uoc48ia3df_vZxWCLFDB\": \"main-buddyFeed-closeContainer\",\n\t\"AzO2ondhaHJntbGy_3_S\": \"main-buddyFeed-container\",\n\t\"NmPFqFYcYmtFfPShmtj3\": \"main-buddyFeed-container\",\n\t\"zuwPpHAEtIqahnB2u9NR\": \"main-buddyFeed-content\",\n\t\"dRp9nXvINyo7LOvJtuUC\": \"main-buddyFeed-content\",\n\t\"MObmOrMxbQpO10ebAtZA\": \"main-buddyFeed-emptyBuddyFeed\",\n\t\"gj9SOnCIzruGWAM5m3XO\": \"main-buddyFeed-friendActivity\",\n\t\"oWQvtc5QZlmB60A9ejJx\": \"main-buddyFeed-friendsFeedContainer\",\n\t\"nGInMrf62TCFD9MBnEzz\": \"main-buddyFeed-friendsList\",\n\t\"NdQkQZhcYIEcJnRdAYcQ\": \"main-buddyFeed-header\",\n\t\"vJNLkecIsGhwDaslvslX\": \"main-buddyFeed-header\",\n\t\"BeABJha8PrxMcJmlBzcH\": \"main-buddyFeed-headerTitle\",\n\t\"tli6RLZf7DdPtClCKK6_\": \"main-buddyFeed-link\",\n\t\"n6rSk6R7nfmSGSgTRR5_\": \"main-buddyFeed-loadingFriends\",\n\t\"VBQoaks4ZkfIknTKmxXZ\": \"main-buddyFeed-overlay\",\n\t\"ylTErJiI2Ir_LqmrsaXV\": \"main-buddyFeed-panelTransition\",\n\t\"kE5DI3BkNlo9zuO7K3aO\": \"main-buddyFeed-playbackContext\",\n\t\"dvb9y9hcP8sUfMhUASKO\": \"main-buddyFeed-playbackContextIcon\",\n\t\"hnEmEncF7kB2TnE_Uyja\": \"main-buddyFeed-playbackContextLink\",\n\t\"Irsd58UNEmDPxdhXKXCs\": \"main-buddyFeed-playIcon\",\n\t\"AWUxW13rbpNdQkvJJg13\": \"main-buddyFeed-scrollableArea\",\n\t\"v_YrAYQP6fHG_z0hyg7C\": \"main-buddyFeed-scrollBarContainer\",\n\t\"scnRZypJsyXH1tjPg6uM\": \"main-buddyFeed-scrollBarContainer\",\n\t\"gWUxbU2cIHAajHxsVLMZ\": \"main-buddyFeed-section\",\n\t\"WjW1oRtpaNrY37daDP6Y\": \"main-buddyFeed-sectionFadeEnter\",\n\t\"C3bqciZSM7rPG_L3ohdC\": \"main-buddyFeed-sectionFadeEnterActive\",\n\t\"T27BYdtZ9ugE_X_JpP1A\": \"main-buddyFeed-sectionFadeExit\",\n\t\"RLdiPAZ3grZgi7lfHjXA\": \"main-buddyFeed-sectionFadeExitActive\",\n\t\"LywlKLgNaEtHZzLM3EX5\": \"main-buddyFeed-timestamp\",\n\t\"Lcc_yFPFIr7HSbt6aQbQ\": \"main-buddyFeed-title\",\n\t\"cwcgvNz4amFDKqakIqxc\": \"main-buddyFeed-titleContainer\",\n\t\"ktMuChpoidaEvECE7y8f\": \"main-buddyFeed-username\",\n\t\"P7j2kCLc27vLybuzy5XB\": \"main-buddyFeed-usernameAndTimestamp\",\n\t\"LunqxlFIupJw_Dkx6mNx\": \"main-card-card\",\n\t\"_hPU2Qo7dicv7cnikDRl\": \"main-card-card\",\n\t\"aAYpzGljXQv1_zfopxaH\": \"main-card-cardContainer\",\n\t\"RKstfK7T5nPbsDOYT6sT\": \"main-card-cardContainer\",\n\t\"tsv7E_RBBw6v0XTQlcRo\": \"main-card-cardLink\",\n\t\"oboNtEJNVzUoiaCWZRti\": \"main-card-cardLink\",\n\t\"E1N1ByPFWo4AJLHovIBQ\": \"main-card-cardMetadata\",\n\t\"K2GmhKpSMGL9aDJbOJrD\": \"main-card-cardMetadata\",\n\t\"xHz124sSHSCYHecLCTfi\": \"main-card-cardTitle\",\n\t\"sq_3oR2TTWUK3HEVWaxx\": \"main-card-cardTitle\",\n\t\"w9Tpa4Y111UM5u1WMkEl\": \"main-card-DownloadStatusIndicator\",\n\t\"vCaVlmqbg5B36V6PzUsm\": \"main-card-DownloadStatusIndicator\",\n\t\"XiVwj5uoqqSFpS4cYOC6\": \"main-card-draggable\",\n\t\"kpO5z7v_Nr22lge440TY\": \"main-card-hero\",\n\t\"xBV4XgMq0gC5lQICFWY_\": \"main-card-imageContainer\",\n\t\"YnehZkoE2hpA5Tniju8J\": \"main-card-imageContainer\",\n\t\"NH6UaoYYe47eIHA2Rmal\": \"main-card-imageContainerOld\",\n\t\"xxLDW4qlPuNbzB9jIJKc\": \"main-card-imageContainerOld\",\n\t\"AYwATC_zEPwCkmO1yc8R\": \"main-card-imageContainerSkeleton\",\n\t\"DcLEXCfwMp91VNH5R4m9\": \"main-card-imageContainerSkeleton\",\n\t\"UB3cP9wsqoAMAHaeBGDt\": \"main-card-imagePlaceholder\",\n\t\"r9Oj1LabijReMkOyeUxw\": \"main-card-newEpisodeIndicator\",\n\t\"XpvI6Yl46lYYdMY6SnvK\": \"main-card-newEpisodeIndicator\",\n\t\"woJQ5t2YiaJhjTv_KE7p\": \"main-card-PlayButtonContainer\",\n\t\"EFrqkoAHdmfRItUj8H8H\": \"main-card-PlayButtonContainer\",\n\t\"zuKTW9yEI9rToECOcWG3\": \"main-card-PlayButtonContainerVisible\",\n\t\"_0rtnZ9bGhwcfS5kspzw\": \"main-card-PlayButtonContainerVisible\",\n\t\"iwsv8i7rNxA2c_VvC4CO\": \"main-card-type\",\n\t\"c6FJaCManaNcchyUwvoL\": \"main-card-withWaves\",\n\t\"MxnDSnm9KnJu4xcsinKh\": \"main-card-withWavesWrapper\",\n\t\"QO_OHdGt6X6luPH6_Lfg\": \"main-cardHeader-hasNewEpisodeIndicator\",\n\t\"Nqa6Cw3RkDMV8QnYreTr\": \"main-cardHeader-link\",\n\t\"nk6UgB4GUYNoAcPtAQaG\": \"main-cardHeader-text\",\n\t\"yYflTYbufy7rATGQiZfq\": \"main-cardImage-circular\",\n\t\"SKJSok3LfyedjZjujmFt\": \"main-cardImage-image\",\n\t\"g4PZpjkqEh5g7xDpCr2K\": \"main-cardImage-imageWrapper\",\n\t\"mKWCDBYvel1BrZvMNY09\": \"main-cardImage-imageWrapper\",\n\t\"qucSNjx66ofSGZDzCuEk\": \"main-cardSubHeader-isHero\",\n\t\"Za_uNH8nTZ0qCuIqbPLZ\": \"main-cardSubHeader-root\",\n\t\"RArlOXg8S6l3NgRKrGsO\": \"main-cardSubHeader-text\",\n\t\"r9YzlaAPnM2LGK97GSfa\": \"main-collectionLinkButton-collectionLinkButton\",\n\t\"ot6VAZq1Xfbw2Vh8Qt_A\": \"main-collectionLinkButton-collectionLinkText\",\n\t\"v1Gs5xaHy8aBhUMEobSn\": \"main-collectionLinkButton-dragEnter\",\n\t\"bFQ9NOIn1bDs8tTH0ebQ\": \"main-collectionLinkButton-icon\",\n\t\"GGx57b6ZwGgzUeTpynUw\": \"main-collectionLinkButton-iconWrapper\",\n\t\"zDK1f3nL_R49a1mOvaO1\": \"main-collectionLinkButton-selected\",\n\t\"MIsUJlamzLYuAlvPbmZz\": \"main-confirmDialog-button\",\n\t\"g9vSqjD_GCegXg2jn5pM\": \"main-confirmDialog-button\",\n\t\"X05XDhpQJ7THPHfgbUk1\": \"main-confirmDialog-buttonContainer\",\n\t\"HqYq0HYtZvHn1QGqPBoz\": \"main-confirmDialog-buttonContainer\",\n\t\"RVgHI2ejYct8LjT1AO7m\": \"main-confirmDialog-container\",\n\t\"f2wxfjDHQON3z6amVbrW\": \"main-confirmDialog-container\",\n\t\"m0yIuS1Q6XRA5R4PNEhl\": \"main-confirmDialog-overlay\",\n\t\"Z2BqWbMx6LJjXzVWDAbZ\": \"main-confirmDialog-overlay\",\n\t\"gQoa8JTSpjSmYyABcag2\": \"main-connectBar-connectBar\",\n\t\"UCkwzKM66KIIsICd6kew\": \"main-connectBar-connectBar\",\n\t\"T3hkVxXuSbCYOD2GIeQd\": \"main-connectBar-connected\",\n\t\"DOemiVbX4CbcfgSDqiiJ\": \"main-connectBar-connected\",\n\t\"GcHojieewpdN1c8vbtwk\": \"main-connectBar-connecting\",\n\t\"KL8t9WB65UfUEPuTFAhO\": \"main-content-view\",\n\t\"Cm3jbLBimhqdYEcNTVPj\": \"main-contextMenu-addToPlaylistSubtitle\",\n\t\"OVv1uDz67TLurN8o6LtQ\": \"main-contextMenu-addToPlaylistTitle\",\n\t\"EyDGMdJOp8ktTzmRFcQM\": \"main-contextMenu-clearButton\",\n\t\"pzkhLqffqF_4hucrVVQA\": \"main-contextMenu-disabled\",\n\t\"jJzb9peSGYsUDKbc5QBy\": \"main-contextMenu-disabled\",\n\t\"Y98_oiegQgSpY_o7hoKG\": \"main-contextMenu-disabled\",\n\t\"NmbeQabkSLXf0mTAhSLl\": \"main-contextMenu-dividerAfter\",\n\t\"pXsnH0sMlWUGBffe24cr\": \"main-contextMenu-dividerAfter\",\n\t\"yO0rvyTlM0Hu4iDIycZE\": \"main-contextMenu-dividerAfter\",\n\t\"baIGYewvFQfSAOWmZURZ\": \"main-contextMenu-dividerAfter\",\n\t\"QgtQw2NJz7giDZxap2BB\": \"main-contextMenu-dividerBefore\",\n\t\"hYwu7ZpcbQIP6iM5fzAu\": \"main-contextMenu-dividerBefore\",\n\t\"a18VwKy6bTWgj2tAWVmB\": \"main-contextMenu-dividerBefore\",\n\t\"jh5Yq5ylGtEpGtbz1UNg\": \"main-contextMenu-dividerBefore\",\n\t\"pc7Mq0FHh8Nlho4sOJH7\": \"main-contextMenu-expandButton\",\n\t\"IErtLy9qyhR17riTrzYh\": \"main-contextMenu-expanded\",\n\t\"qCnh1KNFmroitPazoXOc\": \"main-contextMenu-expandRight\",\n\t\"ycGdaksV3Z5Y7eav3ZyQ\": \"main-contextMenu-filterInput\",\n\t\"dJzYRtCWK2U6k08EtqAg\": \"main-contextMenu-filterInputContainer\",\n\t\"zfZEbT8RJbcAg13pTMDl\": \"main-contextMenu-filterInputFullWidth\",\n\t\"eG930DCaQXDFqjhxRGIs\": \"main-contextMenu-filterPlaylistSearch\",\n\t\"i8EjndRQjYlli0aLGYEm\": \"main-contextMenu-filterPlaylistSearchContainer\",\n\t\"M0E2Al6URHV3iyoDbvi_\": \"main-contextMenu-highlightedText\",\n\t\"uEPiT_llP0oFNDYu_QZR\": \"main-contextMenu-loadingContainer\",\n\t\"ibA08TpSVrM0wThmotVd\": \"main-contextMenu-loadingContainer\",\n\t\"SboKmDrCTZng7t4EgNoM\": \"main-contextMenu-menu\",\n\t\"k4sYYpEpX2f7RMAPHv3F\": \"main-contextMenu-menu\",\n\t\"NbcaczStd8vD2rHWwaKv\": \"main-contextMenu-menu\",\n\t\"wlb3dYO07PZuYfmNfmkS\": \"main-contextMenu-menu\",\n\t\"LJej9EszIMJShPMMExpj\": \"main-contextMenu-menu\",\n\t\"re2d5HDzt6T4XBgqcBhi\": \"main-contextMenu-menuHeading\",\n\t\"qUeWph4VP9DwR4xOfabh\": \"main-contextMenu-menuHeading\",\n\t\"DuEPSADpSwCcO880xjUG\": \"main-contextMenu-menuItem\",\n\t\"GqvHlGax1jo6vO9D0wHH\": \"main-contextMenu-menuItem\",\n\t\"rQ6LXqVlEOGZdGIG0LgP\": \"main-contextMenu-menuItem\",\n\t\"jzMBaEByD6M9xRmS9mv8\": \"main-contextMenu-menuItem\",\n\t\"ZTmkuXOdPbjhHuLg5How\": \"main-contextMenu-menuItem\",\n\t\"wC9sIed7pfp47wZbmU6m\": \"main-contextMenu-menuItemButton\",\n\t\"SN9k988q2Seb_joCaEny\": \"main-contextMenu-menuItemButton\",\n\t\"mWj8N7D_OlsbDgtQx5GW\": \"main-contextMenu-menuItemButton\",\n\t\"niXChlbt7kxslMUdfwu9\": \"main-contextMenu-menuItemButton\",\n\t\"dx1wWcqtuxz4HubHAyh_\": \"main-contextMenu-menuItemButton\",\n\t\"PDPsYDh4ntfQE3B4duUI\": \"main-contextMenu-menuItemLabel\",\n\t\"ctAknuakI8idqf_S9tvT\": \"main-contextMenu-menuItemLabel\",\n\t\"qUnVdUHbBFoYmLhT0_OC\": \"main-contextMenu-menuItemLabel\",\n\t\"llTZj1tjDr5ZnIOkKdHv\": \"main-contextMenu-menuItemStatic\",\n\t\"Cjga8q3TFvtKCu9qfm27\": \"main-contextMenu-menuItemStatic\",\n\t\"T2BaFHODss7KUUqG8Ryq\": \"main-contextMenu-overlay\",\n\t\"_PqnQJddudWUtaIxOzo7\": \"main-contextMenu-searchIcon\",\n\t\"dgc81JRAlkNQTsZae3Bz\": \"main-contextMenu-searchIconContainer\",\n\t\"iGw72tLfCJI5bdpgf7JQ\": \"main-contextMenu-subMenuIcon\",\n\t\"IJYRnAk_0OE4UPQABQcl\": \"main-contextMenu-subMenuIcon\",\n\t\"D7Eob912u_NzU8SZkYPA\": \"main-contextMenu-subMenuIcon\",\n\t\"twYA62xkZMNWmd5FXvqi\": \"main-contextMenu-subMenuIcon\",\n\t\"abJqsjKClbniwiTCZ7bC\": \"main-contextMenu-subMenuLeading\",\n\t\"wtEUrk4Sxa5e3QZhvbrs\": \"main-contextMenu-subMenuLeading\",\n\t\"X8yW2lJbFCQfV5GjoRwL\": \"main-contextMenu-tippy\",\n\t\"nYdM55iHFByRTzJUmx9X\": \"main-contextMenu-tippy\",\n\t\"wRjmZh6e7KCL09qchtC9\": \"main-contextMenu-tippy\",\n\t\"mph1R_QkS44EPi4lrhxd\": \"main-contextMenu-tippyEnter\",\n\t\"sNOxi993vqYCWYgo_N3H\": \"main-contextMenu-tippyEnter\",\n\t\"v5IUMJNPJgol0273zQXD\": \"main-contextMenu-tippyEnterActive\",\n\t\"z_29i58eLFLK50jAs_4a\": \"main-contextMenu-tippyEnterActive\",\n\t\"bkFQH4uasL3pXqN9eDSi\": \"main-contextMenu-tippyWrapper\",\n\t\"ExGt4YQfmcwvVFGM7tpN\": \"main-contextMenu-trigger\",\n\t\"rVxzkDirgkuRPv5V1HYF\": \"main-coverSlotCollapsed-container\",\n\t\"GQ5_gIWzIqAfBdmQm8yJ\": \"main-coverSlotCollapsed-container\",\n\t\"cOaOhDavcy4wvE4llkwl\": \"main-coverSlotCollapsed-container\",\n\t\"qWcH8e2laY9sYOuCsOAx\": \"main-coverSlotCollapsed-expandButton\",\n\t\"_9sCL61nGvQFXv2u02jXw\": \"main-coverSlotCollapsed-expandButton\",\n\t\"DUONoWRlKBDq3Ob0DXda\": \"main-coverSlotCollapsed-expandButton\",\n\t\"IcyWfMS5VkeOhaI7OWIx\": \"main-coverSlotCollapsed-navAltContainer\",\n\t\"pE08KWp56_bcLi_DddD9\": \"main-coverSlotCollapsed-navAltContainer\",\n\t\"jtRqaoDIpIR6fEATUTyY\": \"main-coverSlotExpanded-container\",\n\t\"LROBF2WtGaVryVpVbSOu\": \"main-coverSlotExpanded-containerExpanding\",\n\t\"FegbnTtU6poHbemMzmBP\": \"main-coverSlotExpanded-enter\",\n\t\"i1SMAJ9KRyK_muq63Pmg\": \"main-coverSlotExpanded-enterActive\",\n\t\"Q4cc5RktWgz2H8_vDrIS\": \"main-coverSlotExpanded-exitActive\",\n\t\"Fih6l1HD6F3NRrdCEMFE\": \"main-coverSlotExpanded-expanding\",\n\t\"g6ZgzRfiHjsTLskeyI0J\": \"main-coverSlotExpandedCollapseButton-collapseButton\",\n\t\"IPVjkkhh06nan7aZK7Bx\": \"main-createPlaylistButton-button\",\n\t\"q3ABXYJT9JZIzXOOtVuO\": \"main-createPlaylistButton-createPlaylistIcon\",\n\t\"Bwc9jlVb7HWs8JJupnBB\": \"main-createPlaylistButton-icon\",\n\t\"J4xXuqyaJnnwS6s2p3ZB\": \"main-createPlaylistButton-text\",\n\t\"PrhIVExjBkmjHt6Ea4XE\": \"main-devicePicker-button\",\n\t\"tyZF5iwaJ6J5raHWkxwu\": \"main-devicePicker-connectBarVisible\",\n\t\"IbmaxRtjqCjqTBpFwCgw\": \"main-devicePicker-connected\",\n\t\"l79vgNZqvs1q9nw6Q8A2\": \"main-devicePicker-connecting\",\n\t\"E3EJEgTSJdjF1NrDf9GB\": \"main-devicePicker-connectingIcon\",\n\t\"INitzTSjokOMEJOc6P2H\": \"main-devicePicker-controlButton\",\n\t\"hwP4Oum2PB765sb8jigI\": \"main-devicePicker-devices\",\n\t\"h3qSlLeqACMUaASiKDHa\": \"main-devicePicker-devicesHeading\",\n\t\"zFqMGX3h5z2CO3f2uEiL\": \"main-devicePicker-devicesList\",\n\t\"MMjN8VsyKsAlLjFw4RMa\": \"main-devicePicker-devicesOtherNetworksHeading\",\n\t\"AXkwHpGa_BG7Dy4v7o2V\": \"main-devicePicker-header\",\n\t\"ppzUk3Yw4Opnu95vCliV\": \"main-devicePicker-header\",\n\t\"tm3lCLoFzk25Q_df5g5K\": \"main-devicePicker-heading\",\n\t\"Zb1Lbll48zpXP9k_0m1N\": \"main-devicePicker-heading\",\n\t\"pXrRjiuo3ZpGwIvDAGzJ\": \"main-devicePicker-headingAction\",\n\t\"gz7xPnsQVKVMnQY2KjJs\": \"main-devicePicker-headingConnected\",\n\t\"SzZayhxQEuTPMFgNrLOG\": \"main-devicePicker-headingContent\",\n\t\"fCe03n0XPP2ljLlWgfh3\": \"main-devicePicker-headingContent\",\n\t\"TJ5Bjp6vgnWVbh6mGN0n\": \"main-devicePicker-headingSubtitle\",\n\t\"fMNrKmRiR7PwVComoqfb\": \"main-devicePicker-headingSubtitle\",\n\t\"YY5a3DPS6akIYNqJdwn3\": \"main-devicePicker-headingTechIcon\",\n\t\"E9qsFEgJZJnARk6hOCsj\": \"main-devicePicker-headingTitle\",\n\t\"wC7XHKBOz7EiENs4anj4\": \"main-devicePicker-headingTitle\",\n\t\"WFRr38dFOxh75JyzSTj5\": \"main-devicePicker-indicator\",\n\t\"ntdp2T_przGKYzhwYSGz\": \"main-devicePicker-menuOpen\",\n\t\"LVp1auH4vtD3hvb1s4gl\": \"main-devicePicker-moreButton\",\n\t\"uWvwXlS0Da1bWsRX6KOw\": \"main-devicePicker-nowPlayingActiveIcon\",\n\t\"KdGUk56d2SlrvrUsFjB1\": \"main-devicePicker-nowPlayingActiveIcon\",\n\t\"pUkuSEO5HGdvTiujyI6H\": \"main-devicePicker-section\",\n\t\"mkQEqlUoJ9kghcMfT49m\": \"main-devicePicker-section\",\n\t\"IdxmFS96lyE7c5uiTnLM\": \"main-devicePicker-sectionHeading\",\n\t\"Ci8IvJAESoDM_7t8FZWy\": \"main-devicePicker-sectionHeading\",\n\t\"HVCCFeUiHVwZVv74p34a\": \"main-devicePicker-sectionWrapper\",\n\t\"bk509U3ZhZc9YBJAmoPB\": \"main-devicePicker-tooltip\",\n\t\"YIJxiTuPgMQav316cRqP\": \"main-devicePicker-tooltip\",\n\t\"b46WEWO1Sc_P4WK5RTg0\": \"main-devicePicker-tooltipArrow\",\n\t\"ItaYxdM8MiuHsa2VXuGQ\": \"main-devicePicker-tooltipContent\",\n\t\"CgzneMEIFUgv7Gxkf_pM\": \"main-devicePicker-tooltipContent\",\n\t\"YUyFyiI58gL8VuLdbOD6\": \"main-devicePicker-troubleshooting\",\n\t\"bZ9fezHRckzRB7RKhQYv\": \"main-devicePicker-troubleshooting\",\n\t\"QClIatTm05fvjVzODr8X\": \"main-devicePicker-troubleshootingItemIcon\",\n\t\"gq10jri_eDZTLB32XBkA\": \"main-devicePicker-troubleshootingItemIcon\",\n\t\"OTuSSciCS3NJqFG8OKX2\": \"main-devicePicker-troubleshootingItemSubtitle\",\n\t\"cxUSS5WmBfLzDUfriVLA\": \"main-devicePicker-troubleshootingItemSubtitle\",\n\t\"AN2rvWrkrs7UsnY12hL8\": \"main-devicePicker-troubleshootingList\",\n\t\"L6IYWy6qSZEiOh0pp4ZX\": \"main-devicePicker-troubleshootingList\",\n\t\"Fyc_tPyPKyRIT_59VZ2B\": \"main-downloadClient-actionContainer\",\n\t\"Foyk_HJx16yh22JYmQ56\": \"main-downloadClient-container\",\n\t\"UalNRoO1omHtEEniypS5\": \"main-downloadClient-container\",\n\t\"_w3sHVCUhYgvQar5WNHw\": \"main-dragAndDrop-dndImage\",\n\t\"vb8kSzIiZbfkwqWZROkW\": \"main-dragAndDrop-dndImageShelter\",\n\t\"zrvvPyoxE6wQNqnu0yWA\": \"main-dropDown-dropDown\",\n\t\"FQupgLGfMkp1dOYvUeuQ\": \"main-dropDown-dropDown\",\n\t\"jmu6DFPvhxRl0wSfmv2O\": \"main-dropDown-isSafari\",\n\t\"dmKa90mTDwgspMBRMHNX\": \"main-dropDown-isSafari\",\n\t\"T596E9OFZtarwLZV_Opk\": \"main-duplicateTrackModal-buttonContainer\",\n\t\"AdF5F5BxQXGeWkfceg9A\": \"main-duplicateTrackModal-container\",\n\t\"XBJ5gUPLDUdlCFkWV7PZ\": \"main-duplicateTrackModal-description\",\n\t\"YDfNb_CMrwg2Z6FeLyNu\": \"main-duplicateTrackModal-title\",\n\t\"MF2rLXp4d_JPNs2t0bbj\": \"main-editImage-buttonContainer\",\n\t\"CRPBj66L4XSUXfKxADb5\": \"main-editImage-buttonContainer\",\n\t\"jN7ZUHc7IxpwvWsjb4jo\": \"main-editImageButton-copy\",\n\t\"vU48ZWHmoYvFFaF3r7US\": \"main-editImageButton-copy\",\n\t\"w3w0DS8atwcgOQJAKAV2\": \"main-editImageButton-icon\",\n\t\"L6kTuOfrnQA9bTuOLnhR\": \"main-editImageButton-icon\",\n\t\"xfQXUkj6ThzTYbfF8ilt\": \"main-editImageButton-image\",\n\t\"OxzbfYatnoSAuGKOF1Up\": \"main-editImageButton-image\",\n\t\"Usk1cPR7qbn7RudRN3td\": \"main-editImageButton-overlay\",\n\t\"VvRwJyaExuSefaBWg8FJ\": \"main-editImageButton-overlay\",\n\t\"CdHBSRh3RhPwBNIBQtkD\": \"main-editImageButton-rounded\",\n\t\"PvesGiAqdV7dazP6Qulv\": \"main-embedWidgetGenerator-active\",\n\t\"M05dfNqaFoVZSopsDt0p\": \"main-embedWidgetGenerator-active\",\n\t\"oBoIIlKrwQjxXpvOiOa0\": \"main-embedWidgetGenerator-closeBtn\",\n\t\"Yk_RYwNJgyasJs5coavS\": \"main-embedWidgetGenerator-closeBtn\",\n\t\"U_nX_1rJoqQSWPJRE6zb\": \"main-embedWidgetGenerator-code\",\n\t\"BHz0zyI26ES05fSpuj26\": \"main-embedWidgetGenerator-code\",\n\t\"uUYNnjSt8m3EqVjsnHgh\": \"main-embedWidgetGenerator-container\",\n\t\"JvgoJtzDPOYDcX4dX4V7\": \"main-embedWidgetGenerator-container\",\n\t\"IJHNf0vxPSbPE1egoG4N\": \"main-embedWidgetGenerator-content\",\n\t\"sBqvmlzlMok_45AcZJ0k\": \"main-embedWidgetGenerator-content\",\n\t\"dbKIqeJaR5jYCnOeeDJt\": \"main-embedWidgetGenerator-contentCode\",\n\t\"aOFEOf2KdtjWKoECSAtZ\": \"main-embedWidgetGenerator-contentCode\",\n\t\"VaChXV5vRmHB24UihCAG\": \"main-embedWidgetGenerator-contentFooter\",\n\t\"db7YZ348Hp38w1pBGvbo\": \"main-embedWidgetGenerator-contentFooter\",\n\t\"zwGMyD7hN0H1Xx8hV5MH\": \"main-embedWidgetGenerator-contentHeader\",\n\t\"ePALfjiwnEAylS2dC2Vi\": \"main-embedWidgetGenerator-contentHeader\",\n\t\"NYCZnquuE8_PvM2bxZ3r\": \"main-embedWidgetGenerator-contentIframe\",\n\t\"DCmP4sRAZe1WedTXGEoQ\": \"main-embedWidgetGenerator-contentIframe\",\n\t\"gIsqpLXuGzGw3zRTolrW\": \"main-embedWidgetGenerator-copyBtn\",\n\t\"cNyP6OMku0Y1k9V6I6YA\": \"main-embedWidgetGenerator-copyBtn\",\n\t\"MnqXew54JWlqw2TG7vDK\": \"main-embedWidgetGenerator-copyWrapper\",\n\t\"xJroturP3VLDmhMVmH2Q\": \"main-embedWidgetGenerator-copyWrapper\",\n\t\"LeDataq3AAGozahFmBJL\": \"main-embedWidgetGenerator-crossSep\",\n\t\"l5aQDGKG1Ri23cOttct6\": \"main-embedWidgetGenerator-crossSep\",\n\t\"XPFcOWyZPRej4gH_M5XQ\": \"main-embedWidgetGenerator-darkControl\",\n\t\"ASWMZEQeEdTpnEB1Fe9c\": \"main-embedWidgetGenerator-darkControl\",\n\t\"tWZ0D98BTxSAFIgp4_tP\": \"main-embedWidgetGenerator-dimensionField\",\n\t\"Ojyzxzemrirh385czlR6\": \"main-embedWidgetGenerator-dimensionField\",\n\t\"TSQvyxomq15adJzfCj3j\": \"main-embedWidgetGenerator-dimensionLabel\",\n\t\"Xw2qZXdaTtQmBi1C3EZW\": \"main-embedWidgetGenerator-dimensionLabel\",\n\t\"dNNjNzGLV8tjhIuats54\": \"main-embedWidgetGenerator-dimensionsContainer\",\n\t\"E4pMVn86RUX_bli2MFc8\": \"main-embedWidgetGenerator-dimensionsContainer\",\n\t\"bOIRpQiHUAEfp8ntStTo\": \"main-embedWidgetGenerator-header\",\n\t\"a7vBGhnLeEXqDyzDtLQm\": \"main-embedWidgetGenerator-header\",\n\t\"UrcPMgHLfxfJesFPrNvJ\": \"main-embedWidgetGenerator-loadingIndicator\",\n\t\"I95R5oKvjPlRocXK04uq\": \"main-embedWidgetGenerator-loadingIndicator\",\n\t\"nT444OUxO6Vammm8GUVl\": \"main-embedWidgetGenerator-startAt\",\n\t\"SfLe8NXeES5ls02VNodp\": \"main-embedWidgetGenerator-startAt\",\n\t\"wVDmvGlxMo4VisD89XUI\": \"main-embedWidgetGenerator-terms\",\n\t\"RONPdb2SVzKH9L0MI7m9\": \"main-embedWidgetGenerator-terms\",\n\t\"mKjr_3b27jFzEoJBJYQw\": \"main-embedWidgetGenerator-theme\",\n\t\"mA2povcLaNEMTwFkbkRA\": \"main-embedWidgetGenerator-theme\",\n\t\"gAlrxXSfi5DpMcVMiUsz\": \"main-embedWidgetGenerator-themeDescription\",\n\t\"dwoJTbHyEYJwtW9Sv_sL\": \"main-embedWidgetGenerator-themeDescription\",\n\t\"mBGUavHPMlD5mfAmiy5g\": \"main-embedWidgetGenerator-themeRadio\",\n\t\"aNmXQxrSRjmN6iz25rzV\": \"main-embedWidgetGenerator-themeRadio\",\n\t\"bps2gHcEE9dOV6djVLWF\": \"main-embedWidgetGenerator-timestampInput\",\n\t\"sd7sAILjjhbR8VcoTtD1\": \"main-embedWidgetGenerator-timestampInput\",\n\t\"h7qbIvm2yIiZVYqONyFG\": \"main-embedWidgetGenerator-tooltip\",\n\t\"fV9zaLzqxhNrDHsShUyv\": \"main-embedWidgetGenerator-tooltip\",\n\t\"xwNumE3szip6EhU74j3f\": \"main-embedWidgetGenerator-tooltipInitiator\",\n\t\"_zl1TqB0dymzANa9Mg38\": \"main-embedWidgetGenerator-tooltipInitiator\",\n\t\"ObwDQms6Cu8h_8FMWzAT\": \"main-embedWidgetGenerator-transControl\",\n\t\"itbtoqlEj6jmbrhNsVgo\": \"main-embedWidgetGenerator-transControl\",\n\t\"gB6AcMixPEmdr96SUSBM\": \"main-embedWidgetGenerator-visible\",\n\t\"_7n3LwjOANBxLFcGULJMg\": \"main-embedWidgetGenerator-visible\",\n\t\"P4iG24p5ttZDxkQJsiDb\": \"main-embedWidgetGenerator-widthField\",\n\t\"sP9LYlDy3KkYglIvABtL\": \"main-embedWidgetGenerator-widthField\",\n\t\"MyW8tKEekj9lKQsviDdP\": \"main-entityHeader-background\",\n\t\"i_kMOBXfnweAkv8s97Yc\": \"main-entityHeader-background\",\n\t\"wozXSN04ZBOkhrsuY5i2\": \"main-entityHeader-background\",\n\t\"gHImFiUWOg93pvTefeAD\": \"main-entityHeader-backgroundColor\",\n\t\"PeLrpasyfBW8ql_bmoAi\": \"main-entityHeader-backgroundColor\",\n\t\"XVz4BMGP5zAEE5p90mYK\": \"main-entityHeader-backgroundColor\",\n\t\"NIEO4GCY9P49NrtTWlhP\": \"main-entityHeader-backgroundColor\",\n\t\"LzVINwqiLdMt_bgS5psf\": \"main-entityHeader-backgroundColor\",\n\t\"H0vWBc23fJOetym6NudG\": \"main-entityHeader-bold\",\n\t\"ta4ePOlmGXjBYPTd90lh\": \"main-entityHeader-circle\",\n\t\"NXiYChVp4Oydfxd7rT5r\": \"main-entityHeader-container\",\n\t\"tmHIlrxw7_8S0lWnceeq\": \"main-entityHeader-container\",\n\t\"kuSGKO1BuKZ3fgas7_T7\": \"main-entityHeader-creatorButton\",\n\t\"NO_VO3MRVl9z3z56d8Lg\": \"main-entityHeader-creatorWrapper\",\n\t\"bMmO2GCdsRzxLgVMSGvM\": \"main-entityHeader-creatorWrapper\",\n\t\"Ydwa1P5GkCggtLlSvphs\": \"main-entityHeader-detailsText\",\n\t\"n4hTP7ZeAOT_UQEkRUR7\": \"main-entityHeader-divider\",\n\t\"k2I8B0MzXkAJ6_s8okM7\": \"main-entityHeader-gradient\",\n\t\"sv5suqIPUwjgUF_BzM41\": \"main-entityHeader-gradient\",\n\t\"XUwMufC5NCgIyRMyGXLD\": \"main-entityHeader-gradient\",\n\t\"PUIUCdIR_h05BC2EDgIP\": \"main-entityHeader-gray\",\n\t\"RP2rRchy4i8TIp1CTmb7\": \"main-entityHeader-headerText\",\n\t\"c55UACltdzzDDQVfoF18\": \"main-entityHeader-headerText\",\n\t\"CmkY1Ag0tJDfnFXbGgju\": \"main-entityHeader-image\",\n\t\"bFtVZZnZgTWjjyzkPA5k\": \"main-entityHeader-image\",\n\t\"_gLjHpwOxHFwo5nLM8hb\": \"main-entityHeader-imageContainer\",\n\t\"D0QehabsepiTU_FhDZOQ\": \"main-entityHeader-imageContainer\",\n\t\"GoU8CT9Vm_TP_LyYJTsf\": \"main-entityHeader-imageContainerClickable\",\n\t\"_osiFNXU9Cy1X0CYaU9Z\": \"main-entityHeader-imageContainerNew\",\n\t\"ylr6LvLf5L8yLiqErDES\": \"main-entityHeader-imageContainerNew\",\n\t\"MclJZ2TMkhjdlqqQfmQd\": \"main-entityHeader-imagePlaceholder\",\n\t\"C7eyib8lynZrycU2Eh_A\": \"main-entityHeader-large\",\n\t\"k9LEjzjnGgVaEm3BvhAb\": \"main-entityHeader-largeHeader\",\n\t\"Jnt9_XezZCMkZmtgBtlL\": \"main-entityHeader-medium\",\n\t\"bQy_F9QVENlCAL8Qan9_\": \"main-entityHeader-mermaidGradientOverlay\",\n\t\"Fb61sprjhh75aOITDnsJ\": \"main-entityHeader-metaData\",\n\t\"blfR_YJUsKUvdgTejBSb\": \"main-entityHeader-metaData\",\n\t\"JWDnag2Mepdf9QE0cNbg\": \"main-entityHeader-metaData\",\n\t\"Yc6ftz7mCqbsTN4ha_ke\": \"main-entityHeader-metaDataAuthor\",\n\t\"RANLXG3qKB61Bh33I0r2\": \"main-entityHeader-metaDataText\",\n\t\"NULzZTkd4w0TSVS4HKux\": \"main-entityHeader-metaDataText\",\n\t\"w1TBi3o5CTM7zW1EB3Bm\": \"main-entityHeader-metaDataTextSubdued\",\n\t\"gRs15KFAiBRH4FFbVvVr\": \"main-entityHeader-metaDataTextSubdued\",\n\t\"lD5bMttyrRNzsKch6ysa\": \"main-entityHeader-newEntityHeaders\",\n\t\"ByDYHUfzUKVgFwT5bTxp\": \"main-entityHeader-newEntriesIndicator\",\n\t\"RMDSGDMFrx8eXHpFphqG\": \"main-entityHeader-nonWrapped\",\n\t\"xYgjMpAjE5XT05aRIezb\": \"main-entityHeader-overlay\",\n\t\"I0bVSxvqA3rm5HvciMap\": \"main-entityHeader-overlay\",\n\t\"yhlH4Dsjqw56Z58EOwvQ\": \"main-entityHeader-overlay\",\n\t\"dhq75ObRGjfBoBlz6clW\": \"main-entityHeader-overlay\",\n\t\"ehHB7Xk2_FPwYofqZ9k2\": \"main-entityHeader-overlay\",\n\t\"YW4dYEf5ZuLzMfSjsqZk\": \"main-entityHeader-piled\",\n\t\"lp9Tfm4rsM9_pfbIE0zd\": \"main-entityHeader-pretitle\",\n\t\"K4l4RICNZv9cxx9ag0u5\": \"main-entityHeader-pretitle\",\n\t\"YbDIZ84mS7tzHr1tgWE9\": \"main-entityHeader-roundedCorners\",\n\t\"_EShSNaBK1wUIaZQFJJQ\": \"main-entityHeader-shadow\",\n\t\"VPnrctjNWVzCtyD7DZAG\": \"main-entityHeader-shadow\",\n\t\"D5X2O0j5dhTZJIkgH8mz\": \"main-entityHeader-small\",\n\t\"U1ypKorrS1qiWD1uQpAD\": \"main-entityHeader-smallHeader\",\n\t\"zjsGbrMpvbdA1HJ4rpfi\": \"main-entityHeader-smallMadeForIcon\",\n\t\"gSx70PISJg6PSRafbOXd\": \"main-entityHeader-subtitle\",\n\t\"YxJ3zwH0R8K8njVQgMcw\": \"main-entityHeader-subtitleButton\",\n\t\"RmWKJG2G0fTrx11zKv_j\": \"main-entityHeader-theyFollowUs\",\n\t\"rEN7ncpaUeSGL9z0NGQR\": \"main-entityHeader-title\",\n\t\"__NC_butOiOksXo2E3M1\": \"main-entityHeader-title\",\n\t\"wCkmVGEQh3je1hrbsFBY\": \"main-entityHeader-titleButton\",\n\t\"vamOGPv1eDxaQS4qflcg\": \"main-entityHeader-titleButton\",\n\t\"o4KVKZmeHsoRZ2Ltl078\": \"main-entityHeader-titleInner\",\n\t\"vdZMPj4FaYLWBoyRTzBQ\": \"main-entityHeader-titleInner\",\n\t\"HcA9WjbLc4x02X8Ty0uO\": \"main-entityHeader-topbarContent\",\n\t\"URymDGVda51vMGGEWsH1\": \"main-entityHeader-topbarContent\",\n\t\"lro6AjUrZFH6zxjmOGg0\": \"main-entityHeader-topbarContentFadeIn\",\n\t\"ITu7QAFWEP4R0HMkDOMU\": \"main-entityHeader-topbarContentFadeIn\",\n\t\"G7zO58ORUHxcUw0sXktM\": \"main-entityHeader-topbarTitle\",\n\t\"BOjeWsq7rAtXYUaR86bq\": \"main-entityHeader-topbarTitle\",\n\t\"E0MlERsQ_zvY3BX7ZzPp\": \"main-entityHeader-uppercase\",\n\t\"L29j6que7xOiNSH_EOvQ\": \"main-entityHeader-wavesBackground\",\n\t\"XPjEhsPyuOvMZ9NsDrxT\": \"main-entityHeader-withBackgroundImage\",\n\t\"DFuKwT5bvhKZoYr2gfZe\": \"main-entityHeader-withBackgroundImage\",\n\t\"KAZD28usA1vPz5GVpm63\": \"main-genericButton-button\",\n\t\"pJ7RQa2Lqdi9JOvfKGAA\": \"main-genericButton-button\",\n\t\"RK45o6dbvO1mb0wQtSwq\": \"main-genericButton-buttonActive\",\n\t\"eFrQ8hr8h9gs3tIFTSWV\": \"main-genericButton-buttonActive\",\n\t\"EHxL6K_6WWDlTCZP6x5w\": \"main-genericButton-buttonActiveDot\",\n\t\"WOIKebyj47byTnTaucoA\": \"main-genericButton-buttonActiveDot\",\n\t\"OomFKn3bsxs5JfNUoWhz\": \"main-globalNav-buddyFeed\",\n\t\"VizXsWMIuNfKGN5pMyox\": \"main-globalNav-historyButtons\",\n\t\"K1Ve0b6y28X7myMURsKS\": \"main-globalNav-historyButtons\",\n\t\"KkJlQWSJM6Cu2GJJSBQ7\": \"main-globalNav-historyButtons\",\n\t\"Z6t_8rA6LOBrX3huqRJG\": \"main-globalNav-historyButtonsContainer\",\n\t\"pIM9jg__39NIpOvXG89b\": \"main-globalNav-historyButtonsContainer\",\n\t\"VphhNp8Q7R2U552LryhP\": \"main-globalNav-historyButtonsContainer\",\n\t\"rBX1EWVZ2EaPwP4y1Gkd\": \"main-globalNav-icon\",\n\t\"jdlOKroADlFeZZQeTdp8\": \"main-globalNav-link-icon\",\n\t\"dIfr5oVr5kotAi0HsIsW\": \"main-globalNav-link-icon\",\n\t\"bWBqSiXEceAj1SnzqusU\": \"main-globalNav-navLink\",\n\t\"obd_bH64Snp1npdw29XM\": \"main-globalNav-navLink\",\n\t\"YEAFPNm87XbzS4sF5dDe\": \"main-globalNav-navLink\",\n\t\"voA9ZoTTlPFyLpckNw3S\": \"main-globalNav-navLinkActive\",\n\t\"ETjtwGvAB4lRVqSzm8nA\": \"main-globalNav-navLinkActive\",\n\t\"AonZ39aVKATRTjY28Uww\": \"main-globalNav-navLinkActive\",\n\t\"Ufz621LN174DTRDis7EY\": \"main-globalNav-navLinkActive\",\n\t\"QrpHSphgBSqzODEHqr_t\": \"main-globalNav-searchContainer\",\n\t\"lj0eGI6WEtfxFX7irC03\": \"main-globalNav-searchContainer\",\n\t\"v8JHoFMumOgbCn8vsTvC\": \"main-globalNav-searchContainer\",\n\t\"fksI89zEXwqKWm1O6sJm\": \"main-globalNav-searchInputContainer\",\n\t\"W9VXOkC43GP_7ULClgxQ\": \"main-globalNav-searchInputContainer\",\n\t\"b7r2WRiu5f9Q99qmyreh\": \"main-globalNav-searchInputContainer\",\n\t\"RpMCAf8TjF6HpQSI7hdx\": \"main-globalNav-searchInputContainer\",\n\t\"PvsV2JgJRDME1vDn6IJL\": \"main-globalNav-searchInputSection\",\n\t\"tDFP1X98EgqQIPaumxRt\": \"main-globalNav-searchInputSection\",\n\t\"_b3hhmbWtOY8_1M1mM1H\": \"main-globalNav-searchInputSection\",\n\t\"NykkfCmZVlyRYNAqZg35\": \"main-globalNav-searchInputSection\",\n\t\"W02pQCvfy5Bin7z4EAzo\": \"main-globalNav-searchSection\",\n\t\"gj5VcIUC9oD2p4BsxzGE\": \"main-globalNav-searchSection\",\n\t\"jGghIqFVK6VrBUP9FLIq\": \"main-globalNav-searchSection\",\n\t\"soGhxDX6VjS7dBxX9Hbd\": \"main-gridContainer-fixedWidth\",\n\t\"iKwGKEfAfW7Rkx2_Ba4E\": \"main-gridContainer-gridContainer\",\n\t\"dZZDxz44v5EOD33wbCZ3\": \"main-gridContainer-gridContainer\",\n\t\"PmW2UCL9vlTcNPD7J0KJ\": \"main-gridContainer-uniformRowHeight\",\n\t\"HkbHLcqgUfXruL5xVi28\": \"main-heroCard-card\",\n\t\"_gB1lxCfXeR8_Wze5Cx9\": \"main-heroCard-cardLink\",\n\t\"sm7ZnbOO1Zfg9cupYgPN\": \"main-heroCard-cardMetadata\",\n\t\"kVHQenQh3yKEk2n7Ere4\": \"main-heroCard-draggable\",\n\t\"NkDkQMd75JY5xes9xFVe\": \"main-heroCard-isDownloadable\",\n\t\"liYe8rZ0FEQBy8j8XGJH\": \"main-heroCard-isPlaying\",\n\t\"pgwIORyBdf4nbb4G5_Jx\": \"main-heroCard-PlayButtonContainer\",\n\t\"I3EivnXTjYMpSbPUiYEg\": \"main-home-content\",\n\t\"ZLSAuA1tn0bSdQRwhhj6\": \"main-home-content\",\n\t\"Le0q6vXGEvilJEjOqgF9\": \"main-home-content\",\n\t\"YDYIuDcqNWY3gq9hxh5P\": \"main-home-content\",\n\t\"zbU90jX5VWUhVlpUda7B\": \"main-home-filterChipsContainer\",\n\t\"cj6vRk3nFAi80HSVqX91\": \"main-home-filterChipsContainer\",\n\t\"c8Z2jJUocJTdV9g741cp\": \"main-home-filterChipsContainer\",\n\t\"rX_OmqCngvY5ZCoYBZgb\": \"main-home-filterChipsSection\",\n\t\"hIFR8WDm_54EEIa1gwpC\": \"main-home-filterChipsSection\",\n\t\"aBTcK0jHBextE7fGnKiw\": \"main-home-filterChipsSection\",\n\t\"uIJTvxFOg2izOY7aRRiU\": \"main-home-home\",\n\t\"HsbczDqu9qjcYr7EIdHR\": \"main-home-homeHeader\",\n\t\"S4OmZ_IZexmZ5dasPqW5\": \"main-home-homeHeader\",\n\t\"HnVkTECZ2a98QALFTkdq\": \"main-home-homeHeader\",\n\t\"DQRst2WAECYq2xsrsPRA\": \"main-home-isOffline\",\n\t\"QbD9zl7z3AhEQu_TGmo8\": \"main-home-subfeedSection\",\n\t\"_kVOt2H6WxXzURx2FRLM\": \"main-home-withAds\",\n\t\"LBM25IAoFtd0wh7k3EGM\": \"main-image-image\",\n\t\"mMx2LUixlnN_Fu45JpFB\": \"main-image-image\",\n\t\"Yn2Ei5QZn19gria6LjZj\": \"main-image-loaded\",\n\t\"PgTMmU2Gn7AESFMYhw4i\": \"main-image-loaded\",\n\t\"yOKoknIYYzAE90pe7_SE\": \"main-image-loading\",\n\t\"wcftliF4QjZKB1CYgEON\": \"main-imagePicker-fileInput\",\n\t\"IgNI7rYsyjJ5_Xtpap4a\": \"main-imagePicker-image\",\n\t\"IuU_JLhFTfKPXfDkmAaF\": \"main-keyboardShortcutsHelpModal-closeBtn\",\n\t\"YyxAoeTuu0BHU1OcahFK\": \"main-keyboardShortcutsHelpModal-container\",\n\t\"EhyK_jJzB2PcWXd5lg24\": \"main-keyboardShortcutsHelpModal-container\",\n\t\"e4ETsc5zxjzyF9nyb4LI\": \"main-keyboardShortcutsHelpModal-header\",\n\t\"hykQHtPI6EeFREwqRrOR\": \"main-keyboardShortcutsHelpModal-key\",\n\t\"ARw2f2PkF29n9Ek_eWu3\": \"main-keyboardShortcutsHelpModal-sectionHeading\",\n\t\"umavpIt6VOGqirdlUYWs\": \"main-keyboardShortcutsHelpModal-sectionItem\",\n\t\"dYBZmh_ZIyvBZfaoducd\": \"main-keyboardShortcutsHelpModal-sectionItemName\",\n\t\"KDlcc1SFTcA90eMUcn5P\": \"main-keyboardShortcutsHelpModal-sections\",\n\t\"cyXplMovoowBozEe4r2x\": \"main-keyboardShortcutsHelpModal-sectionsContainer\",\n\t\"X871RxPwx9V0MqpQdMom\": \"main-leaderboardComponent-container\",\n\t\"Nd_DeCpszONzyaLe5Wd1\": \"main-likedSongsButton-likedSongsIcon\",\n\t\"BuzoTjBZd1UqCn6DmFJr\": \"main-loadingIndicator-circle\",\n\t\"HKamyJi9H31s99erfVyG\": \"main-loadingIndicator-loadingIcon\",\n\t\"jM1dnq2_qjViaXxa6WD7\": \"main-loadingIndicator-loadingIcon\",\n\t\"K7fGF95OD9aI3zdYnFXg\": \"main-loadingPage-container\",\n\t\"AcuplATLhi6LNeu6_PK7\": \"main-loadingPage-container\",\n\t\"y7xcnM6yyOOrMwI77d5t\": \"main-lyricsCinema-container\",\n\t\"TITRkcJffQbL60GWevgh\": \"main-lyricsCinema-content\",\n\t\"xnPS9Fa6efctzBjM05O4\": \"main-lyricsCinema-controls\",\n\t\"AptbKyUcObu7QQ1sxqgb\": \"main-lyricsCinema-lyricsCinemaVisible\",\n\t\"YtsqA6txqmCqkKq0G2Ta\": \"main-lyricsCinema-nonDisplayedArea\",\n\t\"T0anrkk_QA4IAQL29get\": \"main-moreButton-button\",\n\t\"NyIynkmMpZXSoaE3XGhA\": \"main-navBar-banner\",\n\t\"WvLkmOVB2R2vzI2ibR_r\": \"main-navBar-downloadItem\",\n\t\"RSg3qFREWrqWCuUvDpJR\": \"main-navBar-entryPoints\",\n\t\"WJsKJXEbycxxq8OcGHM1\": \"main-navBar-logo\",\n\t\"sqKERfoKl4KwrtHqcKOd\": \"main-navBar-mainNav\",\n\t\"lYpiKR_qEjl1jGGyEvsA\": \"main-navBar-mainNav\",\n\t\"F2o99ns3FQFoDThE7QiV\": \"main-navBar-mainNav\",\n\t\"tUwyjggD2n5KvEtP5z1B\": \"main-navBar-navBar\",\n\t\"eNs6P3JYpf2LScgTDHc6\": \"main-navBar-navBarItem\",\n\t\"b2KVTiBUcXV1kT0OjL2p\": \"main-navBar-navBarItemDropTarget\",\n\t\"ATUzFKub89lzvkmvhpyE\": \"main-navBar-navBarLink\",\n\t\"moDRd9td0KtitPDzR7OJ\": \"main-navBar-navBarLinkActive\",\n\t\"GKnnhbExo0U9l7Jz2rdc\": \"main-navBar-premiumLink\",\n\t\"uhxXhw9alI7KR1YTc904\": \"main-navBar-premiumNavItem\",\n\t\"IebnAuNOhIG5mDVNJQ5M\": \"main-noConnection-isError\",\n\t\"N3juGUCH1EhEzmffNHAp\": \"main-noConnection-isNotice\",\n\t\"w3PliL6VjTaj2VfscF_k\": \"main-notificationBubble-closeIcon\",\n\t\"ZKFK00olAYy6LtYMjEIa\": \"main-notificationBubble-enter\",\n\t\"QlivAoYbLCn5nS3zU331\": \"main-notificationBubble-enterActive\",\n\t\"o_GEOPl4MEZXycGnTN69\": \"main-notificationBubble-exit\",\n\t\"oJET9u4Y7Vz69Mpp5syC\": \"main-notificationBubble-exitActive\",\n\t\"w_XZqc9pSOOjMMBej0c9\": \"main-notificationBubble-horizontal\",\n\t\"dvZM2BeYewZHdINeWuh6\": \"main-notificationBubble-isClickable\",\n\t\"_cDysv1Z9Ihw848shb92\": \"main-notificationBubble-isCloseable\",\n\t\"sgWxJyCSuqvdP5VZhqts\": \"main-notificationBubble-isError\",\n\t\"hmqCvVdVEXlW52i3efqa\": \"main-notificationBubble-isNotice\",\n\t\"vQNZsbpzoUFSW5fsFOAw\": \"main-notificationBubble-NotificationBubble\",\n\t\"mtmSGKkBYNwbd49V_8fS\": \"main-notificationBubble-withPointer\",\n\t\"zrZgCe7tURBRdKkFGc7S\": \"main-notificationBubble-withSidePointer\",\n\t\"a3nLBllaudGWnYzNTND_\": \"main-notificationBubble-withTopLeftPointer\",\n\t\"_XeODCkWznZi5csbdPAe\": \"main-notificationBubble-withTopRightPointer\",\n\t\"AOaoydTb5lrGytHbTAAy\": \"main-notificationBubbleContainer-NotificationBubbleContainer\",\n\t\"wh7q2LxxjhyXLJvxRQGG\": \"main-nowPlayingBar-buddyFeedIcon\",\n\t\"E526E3G50lCRjDpGVG5B\": \"main-nowPlayingBar-center\",\n\t\"P4eSEARM2h24PZxMHz1T\": \"main-nowPlayingBar-center\",\n\t\"sVv2OQORCQ4kf6iKfUTF\": \"main-nowPlayingBar-center\",\n\t\"fFvsrUhS_NiwbMFmj0fB\": \"main-nowPlayingBar-container\",\n\t\"GD2gbRtcs5dOjMGAM_Y4\": \"main-nowPlayingBar-container\",\n\t\"yglmI5m3fCc8baD1Kwdw\": \"main-nowPlayingBar-container\",\n\t\"RtrMo_s0acvzbSRLovVW\": \"main-nowPlayingBar-enter\",\n\t\"D4el1GTrNd7l_TFyRGO8\": \"main-nowPlayingBar-enterActive\",\n\t\"HOZJyu9UCJodOQSVvduV\": \"main-nowPlayingBar-enterDone\",\n\t\"mwpJrmCgLlVkJVtWjlI1\": \"main-nowPlayingBar-extraControls\",\n\t\"NClDR4CG_J8nuqy2uGn9\": \"main-nowPlayingBar-extraControls\",\n\t\"tT6x7wFZmjldiCeh6HzO\": \"main-nowPlayingBar-isAnonymous\",\n\t\"KkmXuF5h8DmzQUwGhT2u\": \"main-nowPlayingBar-left\",\n\t\"OgkbKIVYE_mrNpYESylB\": \"main-nowPlayingBar-left\",\n\t\"snFK6_ei0caqvFI6As9Q\": \"main-nowPlayingBar-left\",\n\t\"Xmv2oAnTB85QE4sqbK00\": \"main-nowPlayingBar-lyricsButton\",\n\t\"OB4Vm26X_3tYkgqkKrDm\": \"main-nowPlayingBar-lyricsButton\",\n\t\"DLwH4stkW06ZbHFstpq0\": \"main-nowPlayingBar-nowPlayingBar\",\n\t\"OCY4jHBlCVZEyGvtSv0J\": \"main-nowPlayingBar-nowPlayingBar\",\n\t\"udArIAqnfUQPQew2VAns\": \"main-nowPlayingBar-nowPlayingBar\",\n\t\"Y6soMMBElF7EQDbJv8Xb\": \"main-nowPlayingBar-right\",\n\t\"jOKLc29vP0Bz1K0TsDtX\": \"main-nowPlayingBar-right\",\n\t\"pLifNBuHRY8cZkZyEqwL\": \"main-nowPlayingBar-right\",\n\t\"uVRRxsH6RKj3Dzhl40Ok\": \"main-nowPlayingBar-topButton\",\n\t\"ExuDUBJ7bk8vT6INnm9F\": \"main-nowPlayingBar-volumeBar\",\n\t\"niz7IVQi5Id4arbkuPmM\": \"main-nowPlayingBar-volumeBar\",\n\t\"U3kNTAyv7lhF9nBuwgB6\": \"main-nowPlayingView-aboutArtist\",\n\t\"muTn937T_T9l0xqjlN8A\": \"main-nowPlayingView-aboutArtistBio\",\n\t\"Tk7SUvI_ULiUuC5gZsIx\": \"main-nowPlayingView-aboutArtistButton\",\n\t\"ldx08BCI74rTPhgD3vbj\": \"main-nowPlayingView-aboutArtistContent\",\n\t\"HISuyqmMLx0amzWHxgN1\": \"main-nowPlayingView-aboutArtistHasImage\",\n\t\"RJvcrFChbhxGRIi8mBXJ\": \"main-nowPlayingView-aboutArtistPlaceholderWrapper\",\n\t\"DjnRJuC2FcNrQ6Q6DerZ\": \"main-nowPlayingView-aboutArtistTextContent\",\n\t\"vbsB4OQJkFHLU8SbSGzS\": \"main-nowPlayingView-aboutArtistV2\",\n\t\"jLPxNlznfpZHUtITCFnb\": \"main-nowPlayingView-aboutArtistV2Avatar\",\n\t\"r9m6lHy7RyIPDzW1Youe\": \"main-nowPlayingView-aboutArtistV2Bio\",\n\t\"hd6a3g_3QyF8MFL0wWs1\": \"main-nowPlayingView-aboutArtistV2Button\",\n\t\"kVP43jHrJeS7afn8mOgX\": \"main-nowPlayingView-aboutArtistV2FollowButton\",\n\t\"z9CDQr2gnyXDtcc1uF05\": \"main-nowPlayingView-aboutArtistV2HasImage\",\n\t\"ouorHKa6NI5cm666H3tp\": \"main-nowPlayingView-aboutArtistV2Image\",\n\t\"GTmlByXpJj7V6AwVq0Vk\": \"main-nowPlayingView-aboutArtistV2ImageContainer\",\n\t\"iWpZp7Ab_9h7s_U1SsLN\": \"main-nowPlayingView-aboutArtistV2Listeners\",\n\t\"zhQX2DOI2muMo8EKsZ6h\": \"main-nowPlayingView-aboutArtistV2ListenersCount\",\n\t\"COJ84QbXPrd4jkO1HU2N\": \"main-nowPlayingView-aboutArtistV2Name\",\n\t\"yIPdY6L6pcwR4L5Xf0vY\": \"main-nowPlayingView-aboutArtistV2PlaceholderWrapper\",\n\t\"QkOkUShDYWFx5Cz40Bcn\": \"main-nowPlayingView-aboutArtistV2TextContent\",\n\t\"vkS_Ks0svKls4w2s2ppT\": \"main-nowPlayingView-aboutArtistV2Title\",\n\t\"IgTMXVbZtqtZwu3GZASd\": \"main-nowPlayingView-artistOnTour\",\n\t\"uvIvZ4XqfEFs88BAPaI8\": \"main-nowPlayingView-artistOnTourItem\",\n\t\"svHFeMC3Ef_TpSdRyvsM\": \"main-nowPlayingView-artistOnTourShowAll\",\n\t\"QIuMX9iPlMiflBPUkrEQ\": \"main-nowPlayingView-container\",\n\t\"jtqtOeRP46XAlHWx4C0D\": \"main-nowPlayingView-content\",\n\t\"aaFQbW0j0N40v_siz0kX\": \"main-nowPlayingView-contextItemInfo\",\n\t\"UUydeXMsXVZofB0YAOgm\": \"main-nowPlayingView-contextItemInfo\",\n\t\"j9I5h3Z4o0fKNgI1fIjb\": \"main-nowPlayingView-coverArt\",\n\t\"T5w6KXWFZ5aBsuquOADG\": \"main-nowPlayingView-coverArt\",\n\t\"zL6hQR4mukVUUQaa_7K1\": \"main-nowPlayingView-coverArtContainer\",\n\t\"pRIQxez4Q9UdpQsmrwGB\": \"main-nowPlayingView-coverArtContainer\",\n\t\"l2PpoXJouAgqFCuNT3iB\": \"main-nowPlayingView-credits\",\n\t\"G6WmxixPKmCYMNxmUNPT\": \"main-nowPlayingView-credits\",\n\t\"g5zF2gHZOarew6ApvZB6\": \"main-nowPlayingView-creditsGroup\",\n\t\"kUyRPckYBgHDaJp8bmXi\": \"main-nowPlayingView-creditsHeader\",\n\t\"bBldZtWu4QtzmrTfHOKm\": \"main-nowPlayingView-creditsShowAll\",\n\t\"PqjIyA05rhDaDg2S1qIQ\": \"main-nowPlayingView-creditsSource\",\n\t\"MPBLLykSgRJIlLSbQVgy\": \"main-nowPlayingView-gradient\",\n\t\"SfAYznqZyNk_AvvxIkUx\": \"main-nowPlayingView-header\",\n\t\"hzUuLPdH48AzgQun5NYQ\": \"main-nowPlayingView-lyricsContent\",\n\t\"KzMnBC9eFK8cAfcFTg9b\": \"main-nowPlayingView-lyricsControls\",\n\t\"I2WIloMMjsBeMaIS8H3v\": \"main-nowPlayingView-lyricsGradient\",\n\t\"N9Xjnxz8vGgWwbEBE5g7\": \"main-nowPlayingView-lyricsTitle\",\n\t\"wpJvLvrrnyP0_C7hLkqg\": \"main-nowPlayingView-merch\",\n\t\"n4_WcnoVeg2SeDJPWnKK\": \"main-nowPlayingView-nextInQueue\",\n\t\"cIUedsmg_cTnTxvOYTKR\": \"main-nowPlayingView-nowPlayingGrid\",\n\t\"XHoCGTR6RVHuq2o36icg\": \"main-nowPlayingView-nowPlayingGrid\",\n\t\"fIpDXK7M3W0Bn3FgLSRe\": \"main-nowPlayingView-nowPlayingWidget\",\n\t\"ehLi5oxIfUbTLk2NPPB4\": \"main-nowPlayingView-nowPlayingWidget\",\n\t\"PqL625rkFi7CBiMggYTP\": \"main-nowPlayingView-openQueue\",\n\t\"wkl1CJw1cTKpqlKDAiln\": \"main-nowPlayingView-panelOpenDiv\",\n\t\"byIN5OSjNcJHipcI9kuf\": \"main-nowPlayingView-playNext\",\n\t\"LT6lpp3S4Tx8VsbnPzuA\": \"main-nowPlayingView-playNextButton\",\n\t\"A4e013b7hUST0QPHFqKr\": \"main-nowPlayingView-playNextButtonIcon\",\n\t\"V6Fgup3wwQdhZVYVntrH\": \"main-nowPlayingView-playNextIcon\",\n\t\"qbOrWcMUhSri1nPkZLQA\": \"main-nowPlayingView-queue\",\n\t\"ccC6ZoV_TBWVmq_wdgzc\": \"main-nowPlayingView-queue\",\n\t\"Mj718TwbPAUi_iNAcsmz\": \"main-nowPlayingView-queueItem\",\n\t\"Nyxk_izrYbGecpgmtp91\": \"main-nowPlayingView-queueItemEntityImage\",\n\t\"Ai_McRq9wJEYK21w8nX_\": \"main-nowPlayingView-section\",\n\t\"gXpVKubH7jLZ4sQ2CUBn\": \"main-nowPlayingView-section\",\n\t\"EVqc6HChiM9pEqBYAiUE\": \"main-nowPlayingView-sectionHeader\",\n\t\"mdEMa1ZY6qt201KkKL0F\": \"main-nowPlayingView-sectionHeader\",\n\t\"gpDSOimnzH4zTJmE7UR5\": \"main-nowPlayingView-sectionHeaderSpacing\",\n\t\"zZdI03asKaUCNlbhjDAv\": \"main-nowPlayingView-sectionHeaderText\",\n\t\"BInqOrncCcclcBp4uBnY\": \"main-nowPlayingView-sectionHeaderText\",\n\t\"hfdkySA4kiUldFsPj9lD\": \"main-nowPlayingView-trackInfo\",\n\t\"fjCyBC5HnIDoDPrpbbv8\": \"main-nowPlayingView-trackInfo\",\n\t\"BFR9Zt3zpL8BATBMiwQB\": \"main-nowPlayingWidget-coverArt\",\n\t\"vBMZGgINJ_BbsQM82LhK\": \"main-nowPlayingWidget-coverArt\",\n\t\"bYHWD_eQ1jAh3sAKTHtr\": \"main-nowPlayingWidget-coverExpanded\",\n\t\"deomraqfhIAoSB3SgXpu\": \"main-nowPlayingWidget-nowPlaying\",\n\t\"OhVah4L2N7DWZ8VnPhea\": \"main-nowPlayingWidget-nowPlaying\",\n\t\"j96cpCtZAIdqxcDrYHPI\": \"main-nowPlayingWidget-trackInfo\",\n\t\"V98keH9h9rnepsa_qe_r\": \"main-nowPlayingWidget-trackInfo\",\n\t\"fFv7yCuLuIO1dAGZHcVf\": \"main-pageErrorTemplate-errorBody\",\n\t\"fDD5IxaW7WW8LZTlwzs4\": \"main-playbackBarRemainingTime-container\",\n\t\"npFSJSO1wsu3mEEGb5bh\": \"main-playbackBarRemainingTime-container\",\n\t\"kQqIrFPM5PjMWb5qUS56\": \"main-playbackBarRemainingTime-container\",\n\t\"f4XfIkH9v3tBTnI8AEDj\": \"main-playButton-lockIcon\",\n\t\"IeLnf2wUHVKqxhzBcBoM\": \"main-playButton-PlayButton\",\n\t\"PFgcCoJSWC3KjhZxHDYH\": \"main-playButton-PlayButton\",\n\t\"ix_8kg3iUb9VS5SmTnBY\": \"main-playButton-PlayButton\",\n\t\"vIRREgHGvNoc_fRsISih\": \"main-playButton-PlayButton\",\n\t\"KOoUMuC7IxI_1Pi4r4m5\": \"main-playButton-primary\",\n\t\"VgweZbpfbMSfOcTeGNj_\": \"main-playButton-secondary\",\n\t\"D7HQ50jRBGU8qaAGHRTw\": \"main-playButton-transparent\",\n\t\"ke5Pf1zkbk0eGnRWZYyg\": \"main-playlistEditDetailsModal-albumCover\",\n\t\"jBtrrjZB6NApIt7dtMQD\": \"main-playlistEditDetailsModal-albumCover\",\n\t\"CMQlNrl4E4TNHVowBoZ9\": \"main-playlistEditDetailsModal-characterCounter\",\n\t\"SHNkS_d5PbgJ6CgcYaUk\": \"main-playlistEditDetailsModal-characterCounter\",\n\t\"MQQEonum615k8mGkliT_\": \"main-playlistEditDetailsModal-closeBtn\",\n\t\"qNP5_KI5WgVdKUUrdOk6\": \"main-playlistEditDetailsModal-closeBtn\",\n\t\"PiyAiXdQULEnWAHP0tu1\": \"main-playlistEditDetailsModal-container\",\n\t\"UJRUb6VMsdk5NZboCmg4\": \"main-playlistEditDetailsModal-container\",\n\t\"CU0wnmWejIvyEsRRtSac\": \"main-playlistEditDetailsModal-content\",\n\t\"fIsU1JbqyjMITbYOgJph\": \"main-playlistEditDetailsModal-content\",\n\t\"_qRr3jb13hOiy1EvSL_r\": \"main-playlistEditDetailsModal-description\",\n\t\"aeeOEB8Rw3sgCRiOmHik\": \"main-playlistEditDetailsModal-description\",\n\t\"lXzpMHpJSt7uRB1DXwen\": \"main-playlistEditDetailsModal-descriptionCharacterCounter\",\n\t\"hrJ75E6d_5JE_s_w75dA\": \"main-playlistEditDetailsModal-descriptionCharacterCounter\",\n\t\"c0CddR8wF7kDxvU6uM8B\": \"main-playlistEditDetailsModal-descriptionTextarea\",\n\t\"s5cUBUk0EfYa2xMxSSPD\": \"main-playlistEditDetailsModal-descriptionTextarea\",\n\t\"B_4ndHDFJnCj4dxQmvKA\": \"main-playlistEditDetailsModal-descriptionTextareaWithPadding\",\n\t\"g4NoSm4nlTuArQvfAaDa\": \"main-playlistEditDetailsModal-disclaimer\",\n\t\"krfRqxdeDAttst7COcD0\": \"main-playlistEditDetailsModal-disclaimer\",\n\t\"R2w_sH83CJU9Yhnu0xyt\": \"main-playlistEditDetailsModal-header\",\n\t\"u91SehceIJM9fyoWEn3Q\": \"main-playlistEditDetailsModal-header\",\n\t\"UxrKbK6rUr4vmo6SmCab\": \"main-playlistEditDetailsModal-imageChangeButton\",\n\t\"ZKDvF1q4yO4yUkfI0rFS\": \"main-playlistEditDetailsModal-imageChangeButton\",\n\t\"mtGn3ylACuEcnBDCuR2g\": \"main-playlistEditDetailsModal-imageDropDownButton\",\n\t\"Y7_Q_VL5y7FKVYzILZlR\": \"main-playlistEditDetailsModal-imageDropDownButton\",\n\t\"i2x0uFyIEg3YN6njOngZ\": \"main-playlistEditDetailsModal-imageDropDownContainer\",\n\t\"hF2x3Nk34N0RVwSOfRqu\": \"main-playlistEditDetailsModal-imageDropDownContainer\",\n\t\"St_O5qpuAv8_Rt8xLueG\": \"main-playlistEditDetailsModal-imageLoadingContainer\",\n\t\"_rBnbQXOUaS8hxF05Vvj\": \"main-playlistEditDetailsModal-insertLinkButton\",\n\t\"Up_Ke_BKTraatSMY_Po_\": \"main-playlistEditDetailsModal-save\",\n\t\"EMwLV6xnTcOVTKEV2vaK\": \"main-playlistEditDetailsModal-save\",\n\t\"KHbA1pftwmeRxPgRj6XW\": \"main-playlistEditDetailsModal-sectionsContainer\",\n\t\"f0GjZQZc4c_bKpqdyKbq\": \"main-playlistEditDetailsModal-textElement\",\n\t\"CPK2Xj5o66p6ipEmsVqZ\": \"main-playlistEditDetailsModal-textElement\",\n\t\"UCj7uEr7vR_0DO3cQHcX\": \"main-playlistEditDetailsModal-textElementError\",\n\t\"JqI3LI5Df0M4YNOvUSCw\": \"main-playlistEditDetailsModal-textElementInfo\",\n\t\"rosHlzYfiO0UfpmOhP4I\": \"main-playlistEditDetailsModal-textElementLabel\",\n\t\"LibRpovtQwu0kbIGh9vK\": \"main-playlistEditDetailsModal-textElementLabel\",\n\t\"RLzMolC7kIdp65LyfQPb\": \"main-playlistEditDetailsModal-title\",\n\t\"HutS10br7QppJJ5_XHya\": \"main-playlistEditDetailsModal-title\",\n\t\"qOBo9jzgJoMkt2Ad8fur\": \"main-playlistEditDetailsModal-titleCharacterCounter\",\n\t\"ZfCG8e1oAWX1FroDbeFF\": \"main-playlistEditDetailsModal-titleCharacterCounter\",\n\t\"JaGLdeBa2UaUMBT44vqI\": \"main-playlistEditDetailsModal-titleInput\",\n\t\"ZSbwUhw8fvOgwrnjZSbv\": \"main-playlistEditDetailsModal-titleInput\",\n\t\"SuHDp5IvWoCq1P8yJQmo\": \"main-playlistEditDetailsModal-titleInputWithPadding\",\n\t\"AytCc2WKUld6N212Pcpu\": \"main-playlistRemoveMultipleModal-buttonContainer\",\n\t\"J0xJcBaKhwl9EIuzvhLg\": \"main-playlistRemoveMultipleModal-container\",\n\t\"tlBLfMv0fCxd31jPTQhL\": \"main-playlistRemoveMultipleModal-description\",\n\t\"ce3qMCnc2kDVSi7k74fh\": \"main-playlistRemoveMultipleModal-title\",\n\t\"vnCew8qzJq3cVGlYFXRI\": \"main-playPauseButton-button\",\n\t\"QavgDs_52SpJ2rw0LNYz\": \"main-popper-arrow\",\n\t\"aCtCKL9BxAoHeVZS0uRs\": \"main-popper-container\",\n\t\"kMVnZekiwoVgg0Vjd418\": \"main-progressRing-circleColor\",\n\t\"PcjBXoPwLAnvPiTQx79g\": \"main-progressRing-circleShadow\",\n\t\"tP0mccyU1WAa7I9PevC1\": \"main-repeatButton-active\",\n\t\"Vz6yjzttS0YlLcwrkoUR\": \"main-repeatButton-button\",\n\t\"qnYVzttodnzg9WdrVQ1p\": \"main-rootlist-bottomSentinel\",\n\t\"xkHiFJhykpOWcVMMPufq\": \"main-rootlist-dropIndicator\",\n\t\"I_aApN9pSlbGcpLtFQWw\": \"main-rootlist-expandArrow\",\n\t\"NTJM_mh36C5kJ5oO8eac\": \"main-rootlist-expandArrowActive\",\n\t\"LKgm9fCDTO7wqig_5U1q\": \"main-rootlist-rootlist\",\n\t\"EY6S7vlkxB7SF_OjjF_Y\": \"main-rootlist-rootlistContent\",\n\t\"FBPrcmuqo3yv5UfWSRl5\": \"main-rootlist-rootlistDivider\",\n\t\"McwcCfBLSuXa5UDU1IMw\": \"main-rootlist-rootlistDividerContainer\",\n\t\"Y8edH1Yjo4xrW_58czQj\": \"main-rootlist-rootlistDividerGradient\",\n\t\"whXv9jYuEgS1DPTmPCe_\": \"main-rootlist-rootlistItem\",\n\t\"utSR0FVkHnII_aL8TOcu\": \"main-rootlist-rootlistItemLink\",\n\t\"K8Rs3qAYirS8wJ1hR8gn\": \"main-rootlist-rootlistItemLinkActive\",\n\t\"AINMAUImkAYJd4ertQxy\": \"main-rootlist-rootlistItemOverlay\",\n\t\"tojGvx6tcIBmKlICMJAZ\": \"main-rootlist-rootlistPlaylistsScrollNode\",\n\t\"g_jOSq3pLY5p4tldskrw\": \"main-rootlist-statusIcons\",\n\t\"gtuJjD43VjwtP8ii3H3P\": \"main-rootlist-statusIcons\",\n\t\"VjIb8SfYTkc4wMpqqj3f\": \"main-rootlist-textWrapper\",\n\t\"lyVkg68L7ycnwyOcO3vj\": \"main-rootlist-topSentinel\",\n\t\"PVcM2tzEirZrUPGhsuaQ\": \"main-rootlist-topSentinel\",\n\t\"JUa6JJNj7R_Y3i4P8YUX\": \"main-rootlist-wrapper\",\n\t\"DeV0zPbzpbB31xcBc9gz\": \"main-rootlist-wrapper\",\n\t\"rKdWluhLAGpdUFBWN8sK\": \"main-seeAll-link\",\n\t\"MRfNcNMd_djj3KOg_VOB\": \"main-seekBackButton-button\",\n\t\"XGCdw_LcXQHrxmvpxjwi\": \"main-seekForwardButton-button\",\n\t\"r4Qmv_YM7IHogP2i1tmJ\": \"main-shelf-browseGridSection\",\n\t\"q8AZzDc_1BumBHZg0tZb\": \"main-shelf-header\",\n\t\"HOgue4Eg4UdBR58M0633\": \"main-shelf-seeAll\",\n\t\"QyANtc_r7ff_tqrf5Bvc\": \"main-shelf-shelf\",\n\t\"HVBIEiiVvehha3rRJA78\": \"main-shelf-shelf\",\n\t\"Z4InHgCs2uhk0MU93y_a\": \"main-shelf-shelfGrid\",\n\t\"KMMqgUnvRAqSSii6zn7L\": \"main-shelf-shelfGrid\",\n\t\"Sdmk6QLCvk5EuAP54IN5\": \"main-shelf-shelfGridResponsive\",\n\t\"PodseQtNc4pWHyOfGkgA\": \"main-shelf-shelfGridResponsiveMultiRows\",\n\t\"BtbiwMynlB4flsYu_hA2\": \"main-shelf-showAll\",\n\t\"WkSW1U6jr3HDz3vApM56\": \"main-shelf-skeletonShelf\",\n\t\"hWGxHSAKACFWXowXPDTP\": \"main-shelf-subHeader\",\n\t\"MfVrtIzQJ7iZXfRWg6eM\": \"main-shelf-title\",\n\t\"onVWL7MW4PW9FyVajBAc\": \"main-shelf-titleWrapper\",\n\t\"OMuRYOdpUbGif12_lRJl\": \"main-shelf-topRow\",\n\t\"OF_3F0SQCsBtL1jSTlTA\": \"main-shuffleButton-active\",\n\t\"KVKoQ3u4JpKTvSSFtd6J\": \"main-shuffleButton-button\",\n\t\"fn72ari9aEmKo4JcwteT\": \"main-skipBackButton-button\",\n\t\"mnipjT4SLDMgwiDCEnRC\": \"main-skipForwardButton-button\",\n\t\"d89qxCxbCRj4y1Woft8j\": \"main-smartShuffleButton-active\",\n\t\"kpGMQq1KFz620g_BD_dS\": \"main-tag-container\",\n\t\"T1xI1RTSFU7Wu94UuvE6\": \"main-topBar-background\",\n\t\"PfgTAe4hVhuNFZRuuKQG\": \"main-topBar-background\",\n\t\"IAyWaeDamLJLjxuPeVKw\": \"main-topBar-buddyFeed\",\n\t\"WtC1lGbmQRplD6JBhNFU\": \"main-topBar-buddyFeed\",\n\t\"pefa1_ZALRn90eYLANvw\": \"main-topBar-buddyFeed\",\n\t\"W0bXxvPV_DhyzwdJWRuU\": \"main-topBar-buddyFeedActive\",\n\t\"CE17OcVsW2svuK7ebBcx\": \"main-topBar-buddyFeedIcon\",\n\t\"ql0zZd7giPXSnPg75NR0\": \"main-topBar-button\",\n\t\"J6VTd7VdGN2PM_oXCAyH\": \"main-topBar-button\",\n\t\"qvXMfQh1CjESoKKX49Bl\": \"main-topBar-buttonActive\",\n\t\"facDIsOQo9q7kiWc4jSg\": \"main-topBar-container\",\n\t\"VCH3TJP5s27cQwnxWKnA\": \"main-topBar-container\",\n\t\"MIX_wd0K1tVHme_pwV2F\": \"main-topBar-contentArea\",\n\t\"BkpKedcdaMGbvgXMlmcg\": \"main-topBar-contentArea\",\n\t\"OOsg_GCQDERDXc1d0EmC\": \"main-topBar-entryPoints\",\n\t\"pfMoD1MbelMuF1m8QeMc\": \"main-topBar-forward\",\n\t\"Oq5wiHCwgjjMg_VYy068\": \"main-topBar-fullscreenHistoryButtons\",\n\t\"VgSbatGBB9XwTH2_dsxg\": \"main-topBar-historyButtons\",\n\t\"fWwn9sakqBBjgiNti7LD\": \"main-topBar-historyButtons\",\n\t\"IYDlXmBmmUKHveMzIPCF\": \"main-topBar-icon\",\n\t\"ou0osOf3R1ZRWQ1xzFd9\": \"main-topBar-icon\",\n\t\"cqO5c3gPyN6tXIddpWfr\": \"main-topBar-left\",\n\t\"qxbaGYC8rgMLfyOuYRCM\": \"main-topBar-loggedOut\",\n\t\"LKFFk88SIRC9QKKUWR5u\": \"main-topBar-loginButtons\",\n\t\"zuf9gCpkf86KkzzCtkJN\": \"main-topBar-navLink\",\n\t\"EvIR4O7jOSbNmxtMdIQ0\": \"main-topBar-overlay\",\n\t\"fKbzlgPXfvClLUUKYLxj\": \"main-topBar-overlay\",\n\t\"CWFTCu03cdDALodezHmA\": \"main-topBar-responsiveForward\",\n\t\"fl1Ov5aB9YCKnMkJYpEu\": \"main-topBar-right\",\n\t\"VGKBvschhennrwdTibUE\": \"main-topBar-rightSidebarVisible\",\n\t\"g3Xinb8x23n81ejvS9Uj\": \"main-topBar-searchBar\",\n\t\"CVuGEUIxLkNKpMds8AFS\": \"main-topBar-searchBar\",\n\t\"CFfhVwF52u3lETJ8lRWE\": \"main-topBar-searchBar\",\n\t\"btOYheZlYlaVEyO9iEBk\": \"main-topBar-sectionWrapper\",\n\t\"sibxBMlr_oxWTfBrEz2G\": \"main-topBar-signupButton\",\n\t\"eBrbJuUWgMoCkOgWs5uw\": \"main-topBar-topBarContainer\",\n\t\"qHWqOt_TYlFxiF0Dm2fD\": \"main-topBar-topbarContent\",\n\t\"SgtTbe7qcK0Rae6rXLWm\": \"main-topBar-topbarContent\",\n\t\"rwdnt1SmeRC_lhLVfIzg\": \"main-topBar-topbarContentRight\",\n\t\"VHvOWFJZz7l5Py6Vud3A\": \"main-topBar-topbarContentRight\",\n\t\"CuBx12mEGmMQ1XAXHZCs\": \"main-topBar-topbarContentRight\",\n\t\"rovbQsmAS_mwvpKHaVhQ\": \"main-topBar-topbarContentWrapper\",\n\t\"QoAE5SWKbvYt9ogonr6e\": \"main-topBar-topbarContentWrapper\",\n\t\"sNde2kloCY28V4GB9AvP\": \"main-topBar-topNavBarLinks\",\n\t\"k0vXhOdr0XE83lAQaJ1O\": \"main-topBar-topNavBarSeparator\",\n\t\"Upqw01TOXETOmR5Td7Dj\": \"main-topBar-UpgradeButton\",\n\t\"rqZoelG5u2vHb3kedEph\": \"main-topBar-UpgradeButton\",\n\t\"I4p8r1UNjIGk9yv3H2Ms\": \"main-topBar-whatsNewFeed\",\n\t\"t794tYAiOJib_IAmmdqq\": \"main-topBar-whatsNewFeedActive\",\n\t\"t93PZphItuM19kPhX7tC\": \"main-topBar-whatsNewFeedIcon\",\n\t\"RJGA3d0jzObTXwoMUH4p\": \"main-topBar-whatsNewFeedNotification\",\n\t\"kwTbLplLLsW6T_LrtR7_\": \"main-topBar-whatsNewFeedNotificationDot\",\n\t\"dmF7or26BvPJs5kQtYVH\": \"main-topBar-whatsNewFeedNotificationTitle\",\n\t\"coBkWVskipFo8KxLKief\": \"main-topBar-withBackgroundBlur\",\n\t\"g_OLJCea3ISA_OEZXMld\": \"main-topBarStatusIndicator-enter\",\n\t\"eUtkhxZmvSKe4G9vJbvG\": \"main-topBarStatusIndicator-enterActive\",\n\t\"y0CwI5JkA0h0OyLFf53Q\": \"main-topBarStatusIndicator-exitActive\",\n\t\"uObDRTsYYQmLhK7QzuFF\": \"main-topBarStatusIndicator-hasTooltip\",\n\t\"JbIx9RVHwxVXRbXAJaeN\": \"main-topBarStatusIndicator-notMinimized\",\n\t\"FannkPV_e2vWawF9QgVA\": \"main-topBarStatusIndicator-tooltipEnter\",\n\t\"CtNz_OQHOZ5Z0mJMHjzw\": \"main-topBarStatusIndicator-tooltipEnterActive\",\n\t\"rSvmoStBWe2Cft5bw9ya\": \"main-topBarStatusIndicator-tooltipIsError\",\n\t\"Y22srYwXssaWnfD1MXis\": \"main-topBarStatusIndicator-TopBarStatusIndicatorContainer\",\n\t\"mqZLFQb6fadgKQww5AFV\": \"main-trackCreditsModal-additionalCredits\",\n\t\"HJGB9Fo4Xs8h5BkBBeDu\": \"main-trackCreditsModal-additionalCredits\",\n\t\"xd9f6OsPqmyb6CefhTRR\": \"main-trackCreditsModal-clickableCreditsEntry\",\n\t\"XrHz4A9uD7ZGIKQsSyNQ\": \"main-trackCreditsModal-clickableCreditsEntry\",\n\t\"VKCcyYujazVPj6VkksPM\": \"main-trackCreditsModal-closeBtn\",\n\t\"QQ13a8MJiuYLu0q6rZ2A\": \"main-trackCreditsModal-closeBtn\",\n\t\"uV8q95GGAb2VDtL3gpYa\": \"main-trackCreditsModal-container\",\n\t\"KslWdQXWjQsaid2M3oM8\": \"main-trackCreditsModal-container\",\n\t\"IpshWHA6nc9nJxRssAlb\": \"main-trackCreditsModal-creditsEntry\",\n\t\"nv9qp4kKPjm8xaSYV1Of\": \"main-trackCreditsModal-creditsEntry\",\n\t\"beyOcd3p0PEzhrlKIbU1\": \"main-trackCreditsModal-creditsGroup\",\n\t\"PK1NwDda3K9m_l1F57mQ\": \"main-trackCreditsModal-creditsGroup\",\n\t\"pGU_qEtNT1qWKjrRbvan\": \"main-trackCreditsModal-header\",\n\t\"QsEMdNpWb7BvcopiZBY7\": \"main-trackCreditsModal-header\",\n\t\"Nw1INlIyra3LT1JjvoqH\": \"main-trackCreditsModal-mainSection\",\n\t\"VEh7w_UmdiXh8yRLKVmB\": \"main-trackCreditsModal-mainSection\",\n\t\"iGT1RlMPCwUlIiRPbOqg\": \"main-trackCreditsModal-originalCredits\",\n\t\"caPUJnD8oMW_y3rXBllV\": \"main-trackCreditsModal-originalCredits\",\n\t\"bpaf11XBuMN_hd95L9ol\": \"main-trackCreditsModal-sectionTitle\",\n\t\"syZlEQP36Qn7I50V6d4N\": \"main-trackCreditsModal-sectionTitle\",\n\t\"DAIH23Yj6NeioZ6jooQ6\": \"main-trackCreditsModal-sourceNames\",\n\t\"vK_WgBGfaNGiJjeNvajh\": \"main-trackCreditsModal-sourceNames\",\n\t\"gpNta6i8q3KYJC6WBZQC\": \"main-trackInfo-artists\",\n\t\"w_TTPh4y9H1YD6UrTMHa\": \"main-trackInfo-artists\",\n\t\"QKm_g41pGRkjUcr_2cIM\": \"main-trackInfo-artists\",\n\t\"ZcNcu7WZgOAz_Mkcoff3\": \"main-trackInfo-container\",\n\t\"iZrIHsls0lCEhoMDA9kc\": \"main-trackInfo-container\",\n\t\"QZfVz1fBRtwZIIrMKL6v\": \"main-trackInfo-container\",\n\t\"W5cB_o0XkkU7Q8tlTGxq\": \"main-trackInfo-contentContainer\",\n\t\"cpltqpeZsQmmXy7qZgb9\": \"main-trackInfo-contentContainer\",\n\t\"IkAuhlmCofCxyvdphgkv\": \"main-trackInfo-contentWrapper\",\n\t\"PGSe59fD1Hwc9yUM2d3U\": \"main-trackInfo-contentWrapper\",\n\t\"upgEerbOWZz66oZhE5G5\": \"main-trackInfo-contentWrapper\",\n\t\"s1jyNJBxq16eqkqCf6Ax\": \"main-trackInfo-enhanced\",\n\t\"p2ya1fQ3o9pY4alcW0o4\": \"main-trackInfo-enhanced\",\n\t\"LDY4KpSvSSGHG6jRU5QA\": \"main-trackInfo-enhanced\",\n\t\"y6bXimbi8JAcblOFxTap\": \"main-trackInfo-equalGradientWidth\",\n\t\"Q_174taY6n64ZGC3GsKj\": \"main-trackInfo-name\",\n\t\"l3ePjQ6SwNdQQCnLpywl\": \"main-trackInfo-name\",\n\t\"PcH6VnzkkDqD36P93i9Q\": \"main-trackInfo-name\",\n\t\"K9Nj3oI7bTNFh5AGp5GA\": \"main-trackInfo-name\",\n\t\"e_FyYW4DtJAcPdudjghs\": \"main-trackInfo-name\",\n\t\"FYny4fSXzbXG65hAD1Pn\": \"main-trackInfo-overlay\",\n\t\"eSMjmiD29Ox35O95waw6\": \"main-trackInfo-overlay\",\n\t\"ztTFEVvpDUywSVxJw4t7\": \"main-trackInfo-overlay\",\n\t\"aMhBfOqJxn4jKzFN07Lg\": \"main-trackInfo-rightToLeft\",\n\t\"Ty1q4GZz5EJ7Yl2jHs5I\": \"main-trackInfo-xsmallBadges\",\n\t\"MrkH0O1OzmNv_oCQdvI8\": \"main-trackInfo-xsmallBadges\",\n\t\"lvEvs7EuvpagAgPzLl_B\": \"main-trackInfo-xsmallBadges\",\n\t\"Ry7zokVNFKXaDxKp1Qf1\": \"main-trackList-actionsHeader\",\n\t\"iSbqnFdjb1SuyJ3uWydl\": \"main-trackList-active\",\n\t\"ZgAJecvDDVREPXktThbA\": \"main-trackList-active\",\n\t\"eWNJl03RSvGa9VsxkJ_7\": \"main-trackList-addedBy\",\n\t\"zxSdNpIMNoVh8g3F9fqt\": \"main-trackList-arrow\",\n\t\"wE9dp6W8uInhrlbWPMsR\": \"main-trackList-chartTrackList\",\n\t\"ASYv4mEu1lXEHVa04HqY\": \"main-trackList-column\",\n\t\"rGujAXjCLKEd_N6yTwds\": \"main-trackList-column\",\n\t\"Q3gtrgbIqUuVTw9350Us\": \"main-trackList-column\",\n\t\"r53kBgGOdrCYj2Jux0iN\": \"main-trackList-columnResizing\",\n\t\"Wn5NpEuR1Qw99DelnfxX\": \"main-trackList-concertTrackList\",\n\t\"KJeaWI3jCZemsmu4eYL3\": \"main-trackList-curationButton\",\n\t\"otqy2yIt_BVXLjoundpp\": \"main-trackList-curationButton\",\n\t\"SzsIJoBzlexhelPsHXnn\": \"main-trackList-delayedVisibility\",\n\t\"Ar1CZ7qjPHuIJY0cI56W\": \"main-trackList-disabled\",\n\t\"Unhd72dSzMriFEuvX2UU\": \"main-trackList-disabled\",\n\t\"UuAaE00MS64uibJyxXxK\": \"main-trackList-discRow\",\n\t\"xmIMj8Eo8ZixOkuRQWd3\": \"main-trackList-discTitle\",\n\t\"WTbn2dLWM9fMMb3O4uKZ\": \"main-trackList-dropTargetAfter\",\n\t\"xhgesf8qPsbyZY2NaYdH\": \"main-trackList-dropTargetBefore\",\n\t\"AgiCqnZUliKs_dafpdyi\": \"main-trackList-durationHeader\",\n\t\"kxxyFjKz2levImEvxq48\": \"main-trackList-durationHeader\",\n\t\"qcny9ih7ulue1dEOh48T\": \"main-trackList-durationHeader\",\n\t\"m6Bvw7PNXw2WXqruLOLE\": \"main-trackList-enhanced\",\n\t\"vr04lRmH66LzICUXnwCq\": \"main-trackList-eventDate\",\n\t\"OINF2mTM4cu2JjtkmPiz\": \"main-trackList-eventTicketIcon\",\n\t\"vL_PgycspyjCBwfFg9j9\": \"main-trackList-eventVenue\",\n\t\"dZiDwJihz32GwQunXhnB\": \"main-trackList-facepile\",\n\t\"uWYVhrrTnV2V_VYhThDl\": \"main-trackList-facepileAvatar\",\n\t\"FCzIz5e0Lpt4xa5zz2F1\": \"main-trackList-icon\",\n\t\"xEtoX9iOYS58uDtKLmzR\": \"main-trackList-icon\",\n\t\"vBFTtFW3Co9F_yJ_HjF4\": \"main-trackList-icon\",\n\t\"Ss6hr6HYpN4wjHJ9GHmi\": \"main-trackList-indexable\",\n\t\"oYS_3GP9pvVjqbFlh9tq\": \"main-trackList-indexable\",\n\t\"itxBsCHKFwTOvmERT1cg\": \"main-trackList-indexable\",\n\t\"Kb365Ykr7fUEvnvOH0vl\": \"main-trackList-isNextRowSelected\",\n\t\"dXWJ1DecZeg_dpXZUbHL\": \"main-trackList-isPreviousRowSelected\",\n\t\"vDk3w0iWhhczk8PS_K3B\": \"main-trackList-isRecommendedTrackListRow\",\n\t\"jDgf8MzZRbApYE6BW1qL\": \"main-trackList-isRecommendedTrackListRow\",\n\t\"HOP1JqKm27djuzPVbaRl\": \"main-trackList-nineteen\",\n\t\"w304euOUWkI5A8qAqFj8\": \"main-trackList-notificationDot\",\n\t\"VrRwdIZO0sRX1lsWxJBe\": \"main-trackList-number\",\n\t\"xNyTkXEncSjszLNI65Nq\": \"main-trackList-number\",\n\t\"YCOwZBWtN6TZawPOEVI9\": \"main-trackList-number\",\n\t\"nEZjuVeUuGSmYaQWgXTd\": \"main-trackList-ownedBySelf\",\n\t\"z0zJ798TVq97lZgdRT2_\": \"main-trackList-ownedBySelf\",\n\t\"Iy7vi9cVsy6pS6wF8Dud\": \"main-trackList-placeholder\",\n\t\"Dj9_CzXA7IbUFIz4wOsA\": \"main-trackList-placeholderEnd\",\n\t\"cF8vKdBGYRZs_SpaG0Yz\": \"main-trackList-placeholderIndex\",\n\t\"J0lnyV5H9q4Jj7FK0A72\": \"main-trackList-placeholderStart\",\n\t\"BciIfT5b6BSIPIr6feK4\": \"main-trackList-placeholderStartWithCoverImage\",\n\t\"noANc1fQSmaQNKYVDEDQ\": \"main-trackList-placeholderVariable\",\n\t\"n5XwsUqagSoVk8oMiw1x\": \"main-trackList-playingIcon\",\n\t\"DZJJ5SCypi2mREbjy5bx\": \"main-trackList-playingIcon\",\n\t\"TYVy_QqiFWgzw0WkUiHb\": \"main-trackList-playsHeader\",\n\t\"s6jK2TtsvpysB_LjjS7g\": \"main-trackList-playsHeader\",\n\t\"B2Dwok3Y07k2ttjGDssH\": \"main-trackList-queuePanelTracklist\",\n\t\"Bob5Qz4qd2ApsH6o1loA\": \"main-trackList-resizeHandle\",\n\t\"sQcIERaiZKFhOM1LrSmX\": \"main-trackList-rowBadges\",\n\t\"_7_yPy5jfb9kzk3gijq6A\": \"main-trackList-rowBadges\",\n\t\"UeRAfdSphrPaohh2HWmB\": \"main-trackList-rowCompactMode\",\n\t\"N7GZp8IuWPJvCPz_7dOg\": \"main-trackList-rowCompactMode\",\n\t\"Btg2qHSuepFGBG6X0yEN\": \"main-trackList-rowDuration\",\n\t\"l5CmSxiQaap8rWOOpEpk\": \"main-trackList-rowDuration\",\n\t\"bnolo3jJ8KBxI6jyN7bD\": \"main-trackList-rowFeedback\",\n\t\"U_mTGq4vzVyOrPrB3mx4\": \"main-trackList-rowFeedbackButton\",\n\t\"tGKwoPuvNBNK3TzCS5OH\": \"main-trackList-rowHeartButton\",\n\t\"GcODM2Bp3srQqJzi8Tzs\": \"main-trackList-rowHeartButton\",\n\t\"rkw8BWQi3miXqtlJhKg0\": \"main-trackList-rowImage\",\n\t\"IqDKYprOtD_EJR1WClPv\": \"main-trackList-rowImage\",\n\t\"GTdNqPsL1mHfybwJSeVz\": \"main-trackList-rowImage\",\n\t\"EfStVlHpnUDOJF3pM93I\": \"main-trackList-rowImageFallback\",\n\t\"tgCyNnKttOMQXfuqVuhI\": \"main-trackList-rowImageFallback\",\n\t\"RfidWIoz8FON2WhFoItU\": \"main-trackList-rowImagePlayButton\",\n\t\"j2s64Lz8y6VzBLB_V9Gm\": \"main-trackList-rowImagePlayButton\",\n\t\"y3wrMu2sPRR2DCdEpWlg\": \"main-trackList-rowImagePlayButton\",\n\t\"Qs11Fsr_XqTVFDFWWRkQ\": \"main-trackList-rowImagePlayPauseButton\",\n\t\"OXMPsUBpIQoIbOPIv7Bh\": \"main-trackList-rowImagePlayPauseButton\",\n\t\"DoIH4Mjt4sJFHkmAGs03\": \"main-trackList-rowImagePlayPauseButtonPlaying\",\n\t\"cxYjUh_DjUkRijOXGrOT\": \"main-trackList-rowImageVideo\",\n\t\"byLkljnIRd_DJeSMD3LM\": \"main-trackList-rowImageWithPlay\",\n\t\"gmuBAqsC6pcufUyP1VQW\": \"main-trackList-rowImageWithPlay\",\n\t\"iCQtmPqY0QvkumAOuCjr\": \"main-trackList-rowMainContent\",\n\t\"_iQpvk1c9OgRAc8KRTlH\": \"main-trackList-rowMainContent\",\n\t\"hb8C1VAjyUg0VMxrwpix\": \"main-trackList-rowMainContent\",\n\t\"gQnx5tArze5q1wfG6AWJ\": \"main-trackList-rowMainContentCompact\",\n\t\"ft6dUifK4i03829TBAqC\": \"main-trackList-rowMainContentCompact\",\n\t\"VpYFchIiPg3tPhBGyynT\": \"main-trackList-rowMarker\",\n\t\"ucB9avGYvzsmzXUOw0S7\": \"main-trackList-rowMarker\",\n\t\"UUe_W_6spimNc8vpdEEO\": \"main-trackList-rowMarker\",\n\t\"JxZLQbpnH3fFGJHB4XQG\": \"main-trackList-rowMarkerChartStatus\",\n\t\"t4yFt9Ch_ZCPxEEEoImE\": \"main-trackList-rowMarkerChartStatus\",\n\t\"mYN_ST1TsDdC6q1k1_xs\": \"main-trackList-rowMoreButton\",\n\t\"ObVor_8sQq5whKbtWs8a\": \"main-trackList-rowMoreButton\",\n\t\"nYg_xsOVmrVE_8qk1GCW\": \"main-trackList-rowPlayCount\",\n\t\"HxDMwNr5oCxTOyqt85gi\": \"main-trackList-rowPlayCount\",\n\t\"UIBT7E6ZYMcSDl1KL62g\": \"main-trackList-rowPlayPauseIcon\",\n\t\"zOsKPnD_9x3KJqQCSmAq\": \"main-trackList-rowPlayPauseIcon\",\n\t\"HcMOFLaukKJdK5LfdHh0\": \"main-trackList-rowSectionEnd\",\n\t\"PAqIqZXvse_3h6sDVxU0\": \"main-trackList-rowSectionEnd\",\n\t\"qszimzwbM7AdyZGTvofd\": \"main-trackList-rowSectionEnd\",\n\t\"NZAU7CsuZsMeMQB8zYUu\": \"main-trackList-rowSectionIndex\",\n\t\"fS0C4IgbHviZxIVGC736\": \"main-trackList-rowSectionIndex\",\n\t\"ZC9Da494kc24m9FG64Pf\": \"main-trackList-rowSectionIndex\",\n\t\"gvLrgQXBFVW6m9MscfFA\": \"main-trackList-rowSectionStart\",\n\t\"w46g_LQVSLE9xK399VYf\": \"main-trackList-rowSectionStart\",\n\t\"J_3tQnLWkbEcYffbaUL4\": \"main-trackList-rowSectionStart\",\n\t\"bfQ2S9bMXr_kJjqEfcwA\": \"main-trackList-rowSectionVariable\",\n\t\"_TH6YAXEzJtzSxhkGSqu\": \"main-trackList-rowSectionVariable\",\n\t\"qev2KFBSKCHkeXT4fDTl\": \"main-trackList-rowSectionVariable\",\n\t\"rq2VQ5mb9SDAFWbBIUIn\": \"main-trackList-rowSubTitle\",\n\t\"t_yrXoUO3qGsJS4Y6iXX\": \"main-trackList-rowTitle\",\n\t\"btE2c3IKaOXZ4VNAb8WQ\": \"main-trackList-rowTitle\",\n\t\"eRuZMo_HNLjb1IalIeRb\": \"main-trackList-selected\",\n\t\"JgERXNoqNav5zOHiZGfG\": \"main-trackList-selected\",\n\t\"FCqh1RprhBCx2nZeC2Xi\": \"main-trackList-showDisabledAsEnabled\",\n\t\"ZdBognHQ3X610bLWE3e3\": \"main-trackList-sortable\",\n\t\"Bh8ehD8at2hrINB7YMOg\": \"main-trackList-sortable\",\n\t\"blGAYwvDWndVkFDAQZ8A\": \"main-trackList-sortable\",\n\t\"vY_4na7XFQWMFH8phXCQ\": \"main-trackList-statusChangeDown\",\n\t\"NJMsWXHYQgISlxnPODAD\": \"main-trackList-statusChangeDown\",\n\t\"YAINlTb90ZejTPv7k1dH\": \"main-trackList-statusChangeNew\",\n\t\"OqarR9DPk9OgwzSAwR05\": \"main-trackList-statusChangeNew\",\n\t\"zbBdn49lgTKVccLVZBqE\": \"main-trackList-statusChangeUp\",\n\t\"VrcWCORvyjWWMoz4Mbpd\": \"main-trackList-statusChangeUp\",\n\t\"_3IwXr7oR_KZlfPfwMh7\": \"main-trackList-talkIcon\",\n\t\"y8YIxGr73OXUGacKKsWb\": \"main-trackList-talkSegmentDuration\",\n\t\"jsV182e49Puwz9SIYt2J\": \"main-trackList-text\",\n\t\"ShMHCGsT93epRGdxJp2w\": \"main-trackList-trackList\",\n\t\"oIeuP60w1eYpFaXESRSg\": \"main-trackList-trackList\",\n\t\"V3hMbl6JubddBUkF2XKw\": \"main-trackList-trackList\",\n\t\"ixZyJJ3SHxsSb3NHkhWn\": \"main-trackList-trackListCompactMode\",\n\t\"iHiqmESWdGRBwk5cS7ZZ\": \"main-trackList-trackListCompactMode\",\n\t\"koyeY6AgGRPmyPITi7yO\": \"main-trackList-trackListHeader\",\n\t\"IpXjqI9ouS_N5zi0WM88\": \"main-trackList-trackListHeader\",\n\t\"Y4EDvZXtKfdzwuoUAPwO\": \"main-trackList-trackListHeader\",\n\t\"dZPmmYYhskhqHJCAruvI\": \"main-trackList-trackListHeaderRow\",\n\t\"ePPpO_NuGDUxVRTw7y6W\": \"main-trackList-trackListHeaderRow\",\n\t\"U9A7_SGWn6IJ0vAM2oU7\": \"main-trackList-trackListHeaderRow\",\n\t\"qJOhHoRcFhHJpEQ2CwFT\": \"main-trackList-trackListHeaderStuck\",\n\t\"_2ajKWDiy6YvJu5wo8I1g\": \"main-trackList-trackListHeaderStuck\",\n\t\"h4HgbO_Uu1JYg5UGANeQ\": \"main-trackList-trackListRow\",\n\t\"IjYxRc5luMiDPhKhZVUH\": \"main-trackList-trackListRow\",\n\t\"UhOLa3blz2xoAxM2vRwz\": \"main-trackList-trackListRow\",\n\t\"wTUruPetkKdWAR1dd6w4\": \"main-trackList-trackListRowGrid\",\n\t\"UpiE7J6vPrJIa59qxts4\": \"main-trackList-trackListRowGrid\",\n\t\"vzvX6wzymW8rwI4hkYo0\": \"main-trackList-trackListRowGrid\",\n\t\"vOp2HlcPkxOHebo3If32\": \"main-useDropTarget-album\",\n\t\"O0AN8Ty_Cxd4iLwyKATB\": \"main-useDropTarget-album\",\n\t\"G_xEAccmp3ulqXjuviWK\": \"main-useDropTarget-album\",\n\t\"VNdHKKznHkpJ0VHoDmai\": \"main-useDropTarget-artist\",\n\t\"wQnUXn1m6Gy4PH8jhslb\": \"main-useDropTarget-artist\",\n\t\"zWWLnqWslTLHwq3wBgGB\": \"main-useDropTarget-audiobook\",\n\t\"ufICQKJq0XJE5iiIsZfj\": \"main-useDropTarget-base\",\n\t\"NxEINIJHGytq4gF1r2N1\": \"main-useDropTarget-base\",\n\t\"LLlfyKiKbOd8gfCmHcZX\": \"main-useDropTarget-base\",\n\t\"ETclQEbcAcQdGdSioHaJ\": \"main-useDropTarget-episode\",\n\t\"XNjgtSbyhshr7YQcVvry\": \"main-useDropTarget-episode\",\n\t\"LNzflW6HN3b7upl8Pt7w\": \"main-useDropTarget-episode\",\n\t\"cuH8l_VHkTiz_NYVslQe\": \"main-useDropTarget-folder\",\n\t\"mhuhir0ikRqXAPHU8ZZ1\": \"main-useDropTarget-folder\",\n\t\"aRyoyQFJkzhoSOnf2ERM\": \"main-useDropTarget-local\",\n\t\"odS2IW9wfNVHhkhc0l_X\": \"main-useDropTarget-local\",\n\t\"FQFIqbs9Ic3VDNohmxRp\": \"main-useDropTarget-local\",\n\t\"kXEKypZEUzxx9rNJy09C\": \"main-useDropTarget-playlist\",\n\t\"D8wJ9TPfJzLeLJYxnad2\": \"main-useDropTarget-playlist\",\n\t\"eZnAGhYcXE4Bt0a7958z\": \"main-useDropTarget-playlistV2\",\n\t\"pTvxY5yAQklZgb7VZFGS\": \"main-useDropTarget-pseudoPlaylist\",\n\t\"ratGUXdpLCkyXZNaJryg\": \"main-useDropTarget-show\",\n\t\"oE8LAmRhbeQqsZrQo4lb\": \"main-useDropTarget-show\",\n\t\"caTDfb6Oj7a5_8jBLUSo\": \"main-useDropTarget-track\",\n\t\"or84FBarW2zQhXfB9VFb\": \"main-useDropTarget-track\",\n\t\"HgSl1rNhQllYYZneaYji\": \"main-useDropTarget-track\",\n\t\"atsRVFhRDxRbOyXyFZjS\": \"main-userWidget-active\",\n\t\"odcjv30UQnjaTv4sylc0\": \"main-userWidget-box\",\n\t\"KAq2kDjXj2VS4eXrFL4i\": \"main-userWidget-box\",\n\t\"BsYNRaiIo2R6htfnZiuG\": \"main-userWidget-box\",\n\t\"Fxnb0xe6bL7I7W8V0p6C\": \"main-userWidget-boxCondensed\",\n\t\"SFgYidQmrqrFEVh65Zrg\": \"main-userWidget-boxCondensed\",\n\t\"eAXFT6yvz37fvS1lmt6k\": \"main-userWidget-chevron\",\n\t\"EeWTFG_vxLI5QJc1TH4F\": \"main-userWidget-displayName\",\n\t\"ERyo7m5f00o7ToFdGMCD\": \"main-userWidget-dropDownMenu\",\n\t\"pEG0W4wkbkrOYURhz82H\": \"main-userWidget-hasAvatar\",\n\t\"fw30p54zgXxgBdOMzayR\": \"main-userWidget-hasAvatar\",\n\t\"PrOCaGCRoGw7XaycfkTl\": \"main-userWidget-notificationDot\",\n\t\"EIrPk5CxH5DyLgcOY_yx\": \"main-userWidget-notificationIndicator\",\n\t\"y5mR1se0HqD3uewF5Eb6\": \"main-userWidget-screenReaderOnly\",\n\t\"YqPjzOfhtzbCf_QD3P1f\": \"main-userWidget-setupPlan\",\n\t\"RfdRTSGwulyQdDepLUTT\": \"main-userWidget-showDisplayName\",\n\t\"VdLuku6YQT4tNLT6ojlD\": \"main-userWidget-unableToUpdate\",\n\t\"NCgGw6P72qbffJyHR2Kj\": \"main-watchFeed-actionBodyWrapper\",\n\t\"SliTY9e8oKOiypDQdhlw\": \"main-watchFeed-actionBodyWrapper\",\n\t\"URPxbnUYki78KWvmWBQg\": \"main-watchFeed-actionFooterWrapper\",\n\t\"NazSI1w8xaruwFqTP3sS\": \"main-watchFeed-actionFooterWrapper\",\n\t\"_XbYNhRerNrxKQEmGVwG\": \"main-watchFeed-actionHeaderWrapper\",\n\t\"YmJS4xMTXdNQLvbP2VAt\": \"main-watchFeed-actionHeaderWrapper\",\n\t\"Ptj9KE7YszR48vu9smYv\": \"main-watchFeed-actionSection\",\n\t\"ji0qx5PBc5_4aMgNLGrp\": \"main-watchFeed-actionSection\",\n\t\"BLIxyum1wPNrFibaQ9wr\": \"main-watchFeed-addToPlaylistButton\",\n\t\"Q0Cupq3WCjD1pDiL4J7k\": \"main-watchFeed-addToPlaylistButton\",\n\t\"hrqpTk8XsoleocU2gWHD\": \"main-watchFeed-artistContainer\",\n\t\"dkbQiFBHMdijQeINl_ij\": \"main-watchFeed-artistContainer\",\n\t\"fufiJplKkge3AdhYG29S\": \"main-watchFeed-artistMetadataContainer\",\n\t\"bl3iefJaRs18m1sL93Eh\": \"main-watchFeed-artistMetadataContainer\",\n\t\"ZRW6APLCVeBbH7aeLrA6\": \"main-watchFeed-artistWrapper\",\n\t\"aiSwqZguOk1P52s9o9xP\": \"main-watchFeed-artistWrapper\",\n\t\"TfHga1ciYp079YpX_6OU\": \"main-watchFeed-background\",\n\t\"JXh51ebDftiwM49U49IA\": \"main-watchFeed-background\",\n\t\"MvqsNri2d99MH6SQlvcK\": \"main-watchFeed-backgroundPill\",\n\t\"V795GKOEZNiChr8dJcD4\": \"main-watchFeed-backgroundPill\",\n\t\"Ca5lXvgFoN6C5_gJDmEA\": \"main-watchFeed-backgroundSpacer\",\n\t\"EMcF5GoJ2mLzcp0t365T\": \"main-watchFeed-backgroundSpacer\",\n\t\"BPmQqb9D_rN4hvkCC0jg\": \"main-watchFeed-canvasContainer\",\n\t\"Ul_Er3t8veggCi1APkwp\": \"main-watchFeed-canvasContainer\",\n\t\"fhmiZPVDhaOYIlNd7Myq\": \"main-watchFeed-canvasWrapper\",\n\t\"rDA7A4UaktbiAD9VVIlB\": \"main-watchFeed-canvasWrapper\",\n\t\"F7Qc4Utwpa36JORtcEYl\": \"main-watchFeed-columnContainer\",\n\t\"JYvxp3Y58xF59FA7Mnaf\": \"main-watchFeed-columnContainer\",\n\t\"FP_XXx0FMQPJEu3WzfpM\": \"main-watchFeed-container\",\n\t\"xGrVCnYsA9aDj2f8VAj_\": \"main-watchFeed-container\",\n\t\"qgczwXDFdF_gy4gRs3LN\": \"main-watchFeed-content\",\n\t\"xvG1g4UaMyY0DW6j8XzX\": \"main-watchFeed-content\",\n\t\"cjZ23zZucJkKFMIrGZa4\": \"main-watchFeed-contentAnimation\",\n\t\"h6KcnSRTHGBMCEv288Ha\": \"main-watchFeed-contentAnimation\",\n\t\"sZNnIteDjcGaXSrb7t3W\": \"main-watchFeed-contentRow\",\n\t\"sJnarE2UKqyhLo0LFNqy\": \"main-watchFeed-contentRow\",\n\t\"MlmOtfMWMgfezN_a1qZJ\": \"main-watchFeed-contentSection\",\n\t\"ZRarvpz5og3ifOVHzGOv\": \"main-watchFeed-contentSection\",\n\t\"rCxYvbE1hq2_VEMOM6M_\": \"main-watchFeed-contentSelected\",\n\t\"yV_lauXr1J6WkogIH9dV\": \"main-watchFeed-contentSelected\",\n\t\"SySgdgYnGJ2mZyFM36JP\": \"main-watchFeed-contentWrapper\",\n\t\"YHeDUF3aquXRxYmKOs1A\": \"main-watchFeed-contentWrapper\",\n\t\"A2NgztD8CPHGPFRoT69o\": \"main-watchFeed-equalizerIcon\",\n\t\"IQsUjbPtrVLyyA5iu3zQ\": \"main-watchFeed-equalizerIcon\",\n\t\"CkAFt5XZgLBU8S0jXZgE\": \"main-watchFeed-flipContainer\",\n\t\"dErRUxXewpEkZd52uLTh\": \"main-watchFeed-flipContainer\",\n\t\"NsRfiZjjSCqTim4kM13p\": \"main-watchFeed-followButton\",\n\t\"g47sxYAVvWCq_6T4Ba5L\": \"main-watchFeed-followButton\",\n\t\"n_VwJ2YeUb7y3CHbJ3As\": \"main-watchFeed-genreContainer\",\n\t\"zrFS4My93WidFfSAb0u6\": \"main-watchFeed-genreContainer\",\n\t\"mENWYVW_PAl8OWp2Ut6e\": \"main-watchFeed-genreContent\",\n\t\"SaK6aqYnHAkgc3cG86ZW\": \"main-watchFeed-genreContent\",\n\t\"NtpYi46fEElnZKhNUPK4\": \"main-watchFeed-image\",\n\t\"iQnF1gu0Mg2rGsKVHIFn\": \"main-watchFeed-image\",\n\t\"oI9M0dyjtCBPoup_4Sxv\": \"main-watchFeed-imageWrapper\",\n\t\"bPMdcQr8gSb4AgWfkolx\": \"main-watchFeed-imageWrapper\",\n\t\"F5kvnqpgBwMGbM1mWjNS\": \"main-watchFeed-menuButton\",\n\t\"OGPXh73aLJDzhfFAdPvM\": \"main-watchFeed-menuButton\",\n\t\"Ri_fkzPuxQdOzKQf1tnw\": \"main-watchFeed-metadataSection\",\n\t\"hK5DaTfJaFpo2BJuoy5k\": \"main-watchFeed-metadataSection\",\n\t\"jlBmyjsruclK7z6ILay5\": \"main-watchFeed-metadataWrapper\",\n\t\"E0LdRRiYvrkUdZCmAXqQ\": \"main-watchFeed-metadataWrapper\",\n\t\"d9fVEV27N0lWaeQajcC2\": \"main-watchFeed-modal\",\n\t\"DJxOEatMU2PHk0cCYWoX\": \"main-watchFeed-modal\",\n\t\"RLx9IUmPpPeLH0cWnocs\": \"main-watchFeed-nextButtonInner\",\n\t\"kT3dFHdVmU0vyeE71IDE\": \"main-watchFeed-nextButtonInner\",\n\t\"iZJIqsY5Yy3szidvO6ig\": \"main-watchFeed-nextButtonWrapper\",\n\t\"XIKBfnA7TQOV1T8LLiAS\": \"main-watchFeed-nextButtonWrapper\",\n\t\"gWworaJ45IDWOdwjrZKh\": \"main-watchFeed-pillAnimationFour\",\n\t\"zs2yWiCDKxo__fskASpC\": \"main-watchFeed-pillAnimationFour\",\n\t\"wJA26c7NDa4pJmcd_khZ\": \"main-watchFeed-pillAnimationOne\",\n\t\"pGPb4TiSkhPdYEt8h2Av\": \"main-watchFeed-pillAnimationOne\",\n\t\"PAkFzXltdtGuaW_wVPZ4\": \"main-watchFeed-pillAnimationThree\",\n\t\"BWyVJ8XXZ0jRGpFaA9iW\": \"main-watchFeed-pillAnimationThree\",\n\t\"EinJaDeKltP49Pc_nCPh\": \"main-watchFeed-pillAnimationTwo\",\n\t\"FWBI05WNSb4HR8goMhai\": \"main-watchFeed-pillAnimationTwo\",\n\t\"GfTEPivZmrudh2ZLmUJE\": \"main-watchFeed-playerContainer\",\n\t\"omghgDzOcUFl6oOXYNHX\": \"main-watchFeed-playerContainer\",\n\t\"madaLABKCn3HDMHk7bBQ\": \"main-watchFeed-playerImage\",\n\t\"_bMlIQGXEMMBRZOVM71Z\": \"main-watchFeed-playerImage\",\n\t\"_C07RbGcQZqPjX3UZ9D9\": \"main-watchFeed-playlistTitle\",\n\t\"AkStH5UDLsXzudcPz_J_\": \"main-watchFeed-playlistTitle\",\n\t\"ksxwRgG3qxwghi5X7wvE\": \"main-watchFeed-progressBar\",\n\t\"edeYmiJw2Fd9EFB2SQZo\": \"main-watchFeed-progressBar\",\n\t\"w091W9_8qRMxTEGofEej\": \"main-watchFeed-queueButton\",\n\t\"d9bbNw4YiAXpQpjrSgBJ\": \"main-watchFeed-queueButton\",\n\t\"oyVGfynGkVn_6VcTnd6h\": \"main-watchFeed-scrollToBottomButton\",\n\t\"bsYqS4l_sJOvBGGeudDj\": \"main-watchFeed-scrollToBottomButton\",\n\t\"fCykqEza3WtgKmxjL0C1\": \"main-watchFeed-scrollToTopButton\",\n\t\"OcwUYvKcnj9kvPBwybGI\": \"main-watchFeed-scrollToTopButton\",\n\t\"lu2vixiUgcBKQZ3WxslE\": \"main-watchFeed-shareButton\",\n\t\"pvGZ831aNzHTQMZ8CA_u\": \"main-watchFeed-shareButton\",\n\t\"VAO32xudyOddSHyKq31b\": \"main-watchFeed-showBackground\",\n\t\"IId345zLSrSuzIWtN4Sd\": \"main-watchFeed-showBackground\",\n\t\"X6BLCPn79TtvdLQLuSEn\": \"main-watchFeed-songArtist\",\n\t\"Cypiobiw1yGi5E9N48gJ\": \"main-watchFeed-songArtist\",\n\t\"HLY280CnQhfcMxgRsmhx\": \"main-watchFeed-songArtistListeners\",\n\t\"q2y15bQqkJDcBixyP5oa\": \"main-watchFeed-songArtistListeners\",\n\t\"IJgMYRGDiAtQjDaMNfIf\": \"main-watchFeed-songTitle\",\n\t\"kJqlixofqKau1v1r3YSE\": \"main-watchFeed-songTitle\",\n\t\"LHdDNVCKKrK088dZzmuy\": \"main-watchFeed-songTitleWrapper\",\n\t\"yJJKqce9DPVQnrFseKKU\": \"main-watchFeed-songTitleWrapper\",\n\t\"x2geOlp2y1nFZSOcBBlg\": \"main-watchFeed-soundButtonWrapper\",\n\t\"Qtx4b7Wc1c7NEX49CMeH\": \"main-watchFeed-soundButtonWrapper\",\n\t\"QY_q7DYy8V1MLOWFwSdn\": \"main-watchFeed-willChange\",\n\t\"TaaunKIj990MWZKrBWHW\": \"main-watchFeed-willChange\",\n\t\"kzlksKUC9aLBM62Bckxo\": \"main-whatsNewFeed-actions\",\n\t\"bcU463yG4LHEtlBCOa8Q\": \"main-whatsNewFeed-buttonAlwaysVisible\",\n\t\"n5y3jDsz8siC0JsxtS83\": \"main-whatsNewFeed-buttonVisibleOnHover\",\n\t\"h8Dik_naJfcSoNM_4FKX\": \"main-whatsNewFeed-content\",\n\t\"lgo_zhUnwxG2Qan4WLBY\": \"main-whatsNewFeed-dateAndTime\",\n\t\"CONFZNJkrMwpneVuYWXC\": \"main-whatsNewFeed-description\",\n\t\"eyWbmr17oov600GluVsy\": \"main-whatsNewFeed-divider\",\n\t\"Z1qkHjt67N3DaWqnEM0w\": \"main-whatsNewFeed-imageContainer\",\n\t\"j45TgktnaHqgEqeO3eXI\": \"main-whatsNewFeed-largeImage\",\n\t\"pNM_LHG1Yp9WV_mBN6du\": \"main-whatsNewFeed-list\",\n\t\"nQTI7556vneVslJT_fJv\": \"main-whatsNewFeed-listContent\",\n\t\"BQD_pE0Nva_z6z7CvZww\": \"main-whatsNewFeed-listEpisodeBody\",\n\t\"ymzFgL9iToSvtRvCWY3b\": \"main-whatsNewFeed-listExplicitIcon\",\n\t\"TTFwcgaxN1VxbkjC80_Z\": \"main-whatsNewFeed-listFallbackIcon\",\n\t\"nHEYUQKiBurmQ7SD4uzF\": \"main-whatsNewFeed-listFooter\",\n\t\"LLxSqFbzfI5SPTK_22ZU\": \"main-whatsNewFeed-listHeader\",\n\t\"GBycF4NUOkA5ZUHneLYa\": \"main-whatsNewFeed-listImageWrapper\",\n\t\"LXfpmhx6aZ9YVV8x4PEI\": \"main-whatsNewFeed-listPlayButton\",\n\t\"MCctbQkmLVbJyz3NSbq2\": \"main-whatsNewFeed-listRow\",\n\t\"_MBf1tVqzfo2AefcuHwv\": \"main-whatsNewFeed-listRow\",\n\t\"ThG4UqWk7ASXCMm69Opn\": \"main-whatsNewFeed-listSubtitleLink\",\n\t\"zKLP_l7gmiomI7kW3BMw\": \"main-whatsNewFeed-listTimeAgo\",\n\t\"uuqowKOQLu0zbk4zguPM\": \"main-whatsNewFeed-medium\",\n\t\"Z3LszrbA1M2fpLsEhlQT\": \"main-whatsNewFeed-progressBar\",\n\t\"uSj66OUSURBfFwFhD7Ed\": \"main-whatsNewFeed-separator\",\n\t\"sQnWbTOGnPZBNn3lZTtI\": \"main-whatsNewFeed-separatorAlbum\",\n\t\"nzZgeVfx1Y6nZe7Z9DsA\": \"main-whatsNewFeed-showImage\",\n\t\"oezNMICqWdJHdR3QV9La\": \"main-yourEpisodes-coverContainer\",\n\t\"ioDnN5QkrvTdd8oOrl2h\": \"main-yourEpisodes-coverIcon\",\n\t\"U_iGRN8gxm_rKG_w2EzR\": \"main-yourEpisodes-yourEpisodesCard\",\n\t\"vCaNVEuqazhZFQNcVHZj\": \"main-yourEpisodesButton-yourEpisodesIcon\",\n\t\"OMCDc2F7g_AufJAtaKfL\": \"main-yourLibraryX-button\",\n\t\"cljOO1tpzixzXctKJucK\": \"main-yourLibraryX-button\",\n\t\"prGqQr33U0mG14TJ5V8a\": \"main-yourLibraryX-collapseButton\",\n\t\"FTiXRW7kAldHmLaxVQ2N\": \"main-yourLibraryX-collapseButton\",\n\t\"e_r3VdhCt6ZHTRmscHgh\": \"main-yourLibraryX-collapseButton\",\n\t\"BhKGkKPprp2wm9bvfRKG\": \"main-yourLibraryX-collapseButtonIsCollapsed\",\n\t\"RAWO6AczuDMOTI0qAc0a\": \"main-yourLibraryX-collapseButtonWrapper\",\n\t\"i_FRcsaqNCEJmoyObIP0\": \"main-yourLibraryX-collapseButtonWrapper\",\n\t\"ksmcxhImUuj3_s1lcIm0\": \"main-yourLibraryX-createButton\",\n\t\"EZFyDnuQnx5hw78phLqP\": \"main-yourLibraryX-entryPoints\",\n\t\"lHJd4oSttKLxkxuoZ0Lr\": \"main-yourLibraryX-entryPoints\",\n\t\"OR9izNpUGviUYBP_yQR2\": \"main-yourLibraryX-entryPoints\",\n\t\"wBsWS202aGdsul2kEGUf\": \"main-yourLibraryX-filterArea\",\n\t\"paiZmlAHHhmZonuGJRAr\": \"main-yourLibraryX-filterArea\",\n\t\"rjsuxO8gqIyaiYTHNpOQ\": \"main-yourLibraryX-filterArea\",\n\t\"MLbFLVC33caOj3FgSQMC\": \"main-yourLibraryX-filters\",\n\t\"msaOP0MYt3paJGpbdeJs\": \"main-yourLibraryX-filters\",\n\t\"UvXqRORKQr_N3jlgGTcS\": \"main-yourLibraryX-header\",\n\t\"HjPqU_UW2egr14mRSom9\": \"main-yourLibraryX-header\",\n\t\"tyMY5Nyfm8lQMRQ3NF8c\": \"main-yourLibraryX-header\",\n\t\"j8iKBDzqTDtnDv4XbmrK\": \"main-yourLibraryX-headerContent\",\n\t\"tAfozCQs48q1JYdphYXi\": \"main-yourLibraryX-headerContent\",\n\t\"IKza_DZb6poXFThFwlFt\": \"main-yourLibraryX-headerContent\",\n\t\"O2Vp_sNHMLHUcgMPVnOA\": \"main-yourLibraryX-headerIsCollapsed\",\n\t\"iYP0xuQiJCgi7gx1jUPJ\": \"main-yourLibraryX-headerIsCollapsed\",\n\t\"qR4uulT7QiScqfhJKJ2K\": \"main-yourLibraryX-headerIsCollapsed\",\n\t\"TxO7Ee8iwqBpkgznKHsd\": \"main-yourLibraryX-iconOnly\",\n\t\"TiJahFhH6KZaibhRtEOA\": \"main-yourLibraryX-isFlattened\",\n\t\"y2UicQnlTq148rL8Y0jp\": \"main-yourLibraryX-isScrolled\",\n\t\"hjb8tUL3rpUa0ez4ZtAj\": \"main-yourLibraryX-library\",\n\t\"wM72343CksOCaL3bZvKK\": \"main-yourLibraryX-library\",\n\t\"doInDAjF8E_MHAPBv9fb\": \"main-yourLibraryX-library\",\n\t\"g581mszC8syz99uMMWsr\": \"main-yourLibraryX-libraryAfterDrop\",\n\t\"tXkgloQ88DHF_inQE69J\": \"main-yourLibraryX-libraryBeforeDrop\",\n\t\"PpUTJL2NIYDUnmfzVIbE\": \"main-yourLibraryX-libraryContainer\",\n\t\"hgJel0bLlS_1Uf0EIfSA\": \"main-yourLibraryX-libraryContainer\",\n\t\"vlH99nxvCKwDGQrD0B0M\": \"main-yourLibraryX-libraryContainer\",\n\t\"_0FuodatXU4_fToYAuYtY\": \"main-yourLibraryX-libraryFilter\",\n\t\"uBqliBvyhxGsiql8_OJv\": \"main-yourLibraryX-libraryFilter\",\n\t\"KluIYRb68APBNGHHItUz\": \"main-yourLibraryX-libraryFilter\",\n\t\"dNphEfQzPRaQufS04jUm\": \"main-yourLibraryX-libraryIsCollapsed\",\n\t\"kJ_Q4aphh_uCJCZdzPpD\": \"main-yourLibraryX-libraryIsExpanded\",\n\t\"_XBlEstA77PgWTJzWbe1\": \"main-yourLibraryX-libraryItem\",\n\t\"_K79lE9KrIAkl_bUSSUM\": \"main-yourLibraryX-libraryItemContainer\",\n\t\"WxM1eb7qnneSkMiT4dvw\": \"main-yourLibraryX-libraryItemContainer\",\n\t\"IfMCntz4HO4NOjoFdO2v\": \"main-yourLibraryX-libraryItemContainer\",\n\t\"oLOECYtBhVmBtyisKwew\": \"main-yourLibraryX-libraryOnDrop\",\n\t\"ifVI2CEdOZGgMWIUN2Cw\": \"main-yourLibraryX-libraryRootlist\",\n\t\"_W_0W9Uld1vxrRfsgdQR\": \"main-yourLibraryX-libraryRootlist\",\n\t\"lJ0crV7IZJ8PxJZPA8x6\": \"main-yourLibraryX-libraryRootlist\",\n\t\"RGofdOZulhL2p9MRA5hg\": \"main-yourLibraryX-librarySort\",\n\t\"XZRX_ea9eNn4rOLpNGLp\": \"main-yourLibraryX-librarySortWrapper\",\n\t\"GG5skerNjHXAO6tXyyY0\": \"main-yourLibraryX-librarySortWrapper\",\n\t\"qEiVyQ28VnOKb0LeijqL\": \"main-yourLibraryX-listItem\",\n\t\"vSC5QuwmzUhqUNWdMTJ5\": \"main-yourLibraryX-listItem\",\n\t\"lCgfN9VxRRQtVKTLxWn4\": \"main-yourLibraryX-listItem\",\n\t\"ojrThQm1wxR2gZ6GntJB\": \"main-yourLibraryX-listItemGroup\",\n\t\"Dtr130mQSR0j8k7bu5KS\": \"main-yourLibraryX-listItemGroupCompact\",\n\t\"nZSNG58XEPTX69mkNi9n\": \"main-yourLibraryX-listRowEntityImage\",\n\t\"G7aCptcOZswI1fN6dGkO\": \"main-yourLibraryX-listRowIcon\",\n\t\"o_wMyH9_LbAmIwlVqsF0\": \"main-yourLibraryX-listRowIconWrapper\",\n\t\"d33vqKRxohS9RxzCic1D\": \"main-yourLibraryX-listRowLink\",\n\t\"LSrBzBljgLeDhcm3Soye\": \"main-yourLibraryX-listRowSubtitle\",\n\t\"HdTF8gsRm5MgWvEYlokG\": \"main-yourLibraryX-listRowSubtitleLeadingWrapper\",\n\t\"gj1L_SVM_H8GteWMdEF_\": \"main-yourLibraryX-listRowTitleLink\",\n\t\"LU0q0itTx2613uiATSig\": \"main-yourLibraryX-navItem\",\n\t\"KAcp7QFuEYSouAsuC5i_\": \"main-yourLibraryX-navItem\",\n\t\"AlqlOMBoMUPbFmLmkhhg\": \"main-yourLibraryX-navItemOffline\",\n\t\"fFvRIGtMIgsOLVSq_JNS\": \"main-yourLibraryX-navItems\",\n\t\"QuHe04rU4bj0Z5U9E2Tk\": \"main-yourLibraryX-navItems\",\n\t\"UYeKN11KAw61rZoyjcgZ\": \"main-yourLibraryX-navLink\",\n\t\"hNvCMxbfz7HwgzLjt3IZ\": \"main-yourLibraryX-navLink\",\n\t\"DzWw3g4E_66wu9ktqn36\": \"main-yourLibraryX-navLinkActive\",\n\t\"Bh3b80dIrbc0keQ9kdso\": \"main-yourLibraryX-navLinkActive\",\n\t\"ep0_ry7CLwf91E1rN6Cv\": \"main-yourLibraryX-pulse\",\n\t\"B_HdWVSEWPHaOf9LQAtC\": \"main-yourLibraryX-rowCover\",\n\t\"Gw7E7MkWci1ttQhb4EK0\": \"npv-exitFullScreenButton-button\",\n\t\"gIobRDHAxkAvUaF4_OOL\": \"npv-nowPlayingBar-center\",\n\t\"tr1hDrJgoPSbMXlXU_sl\": \"npv-nowPlayingBar-container\",\n\t\"mbUrqWP55sK6zhspiR72\": \"npv-nowPlayingBar-controls\",\n\t\"N5cWYDvyLrfnyMZuqQHo\": \"npv-nowPlayingBar-left\",\n\t\"FTi9QEhetf4Q4__5sb4S\": \"npv-nowPlayingBar-right\",\n\t\"SVGHXIQcH9HYU7uGITw5\": \"npv-nowPlayingBar-section\",\n\t\"pn5V0OzovI9p6b8nWq8p\": \"playback-bar\",\n\t\"KfxBdL0Zay0br7dCfxbV\": \"playback-bar\",\n\t\"IPbBrI6yF4zhaizFmrg6\": \"playback-bar__progress-time-elapsed\",\n\t\"SRm6aI_til4K8p38XAxv\": \"playback-bar__progress-time-elapsed\",\n\t\"p1ULRzPc4bD8eQ4T_wyp\": \"playback-progressbar\",\n\t\"fDGGPFmD54w05ut8Ns4S\": \"playback-progressbar\",\n\t\"MFyTjrF4GU5ab2BrrOds\": \"playback-progressbar\",\n\t\"B1vgcMXBqOxgMxXh5j1f\": \"playback-progressbar-container\",\n\t\"tNhfHQhWhj9RoA1rfwY5\": \"playback-progressbar-container\",\n\t\"DFtdzavKSbEhwKYkPTa6\": \"playback-progressbar-isInteractive\",\n\t\"WFFDxnFUICVFNvvNU1IE\": \"playback-progressbar-isInteractive\",\n\t\"OEbZDzXdU1OUQactz3ZA\": \"playback-progressbar-isInteractive\",\n\t\"gItY2hnfCB4TsDJCkPiO\": \"player-controls\",\n\t\"O56NBOTLyueNotL56zJt\": \"player-controls\",\n\t\"XrZ1iHVHAPMya3jkB2sa\": \"player-controls__buttons\",\n\t\"fYf41RQrFiGX4KJcSq_5\": \"player-controls__buttons\",\n\t\"NKUrT1GciYXAEEUtagN1\": \"player-controls__left\",\n\t\"GcbM2tnkJCvKOjRfp8RQ\": \"player-controls__left\",\n\t\"Qt226Z4rBQs53aedRQBQ\": \"player-controls__right\",\n\t\"bCCN4Fy0V1eENMKmu7pM\": \"player-controls__right\",\n\t\"JzyZE2R09wq7xtjECDeR\": \"playlist-inlineSearchBox-clearButton\",\n\t\"FeWwGSRANj36qpOBoxdx\": \"playlist-inlineSearchBox-filterInput\",\n\t\"YAYCVnYpPvmYV4JyTmn5\": \"playlist-inlineSearchBox-filterInputContainer\",\n\t\"_h5mio6VqcL_fmiXAb1S\": \"playlist-inlineSearchBox-overlay\",\n\t\"sgZ_MgcS1NlccH19fYsa\": \"playlist-inlineSearchBox-searchIcon\",\n\t\"Hfgj4Zbb2ijt8g54MlCA\": \"playlist-inlineSearchBox-searchIconContainer\",\n\t\"Bdcf5g__Rug3TGqSdbiy\": \"playlist-playlist-actionBarBackground-background\",\n\t\"CeSZLCa3sD6XCrx_ld6S\": \"playlist-playlist-actionBarBackground-background\",\n\t\"rARdlCShKVQsvuXamFOX\": \"playlist-playlist-artistResultListTitle\",\n\t\"a4FkPOXWBc0nK4yzsJCf\": \"playlist-playlist-concertsFooter\",\n\t\"IWWS0F3oiajJG7nlrjXj\": \"playlist-playlist-ctaLink\",\n\t\"mWbx87vgssexrOs2tx4I\": \"playlist-playlist-disclaimerContainer\",\n\t\"_Z2TnFjt8GB5ryOtvyti\": \"playlist-playlist-emptySearchTermContainer\",\n\t\"Bl_kg24BjWgcXPokgEKy\": \"playlist-playlist-emptyStateContainer\",\n\t\"yP3JLuwUNDIQHxRFilK3\": \"playlist-playlist-header\",\n\t\"QD13ZfPiO5otS0PU89wG\": \"playlist-playlist-heading\",\n\t\"tzeKawjOOKFw1KfQ34mG\": \"playlist-playlist-icon\",\n\t\"KzOZOlCPgREEBCJH1Ieg\": \"playlist-playlist-leadingSlot\",\n\t\"ZbLneLRe2x_OBOYZMX3M\": \"playlist-playlist-list\",\n\t\"rjdQaIDkSgcGmxkdI2vU\": \"playlist-playlist-listItem\",\n\t\"u9KYiVXeDRQDGlTDH6rM\": \"playlist-playlist-noBooklistSupportContainer\",\n\t\"umouqjSkMUbvF4I_Xz6r\": \"playlist-playlist-paragraph\",\n\t\"dZ3U5sTGUTdanNamXe1z\": \"playlist-playlist-playlist\",\n\t\"rezqw3Q4OEPB1m4rmwfw\": \"playlist-playlist-playlistContent\",\n\t\"YdoIqRxigDKvBm7a7GSq\": \"playlist-playlist-playlistContent\",\n\t\"xgmjVLxjqfcXK5BV_XyN\": \"playlist-playlist-playlistDescription\",\n\t\"fUYMR7LuRXv0KJWFvRZA\": \"playlist-playlist-playlistDescription\",\n\t\"lykOktr3YIn79xOvVQtS\": \"playlist-playlist-playlistDescription\",\n\t\"oq1ci28WPaRsWkvRiB_J\": \"playlist-playlist-playlistImageContainer\",\n\t\"hVcUafGrnsA6nD1dJzc5\": \"playlist-playlist-playlistInlineCurationBackButton\",\n\t\"FC40AOSbVM9LXjVi7bjO\": \"playlist-playlist-playlistInlineCurationCloseButton\",\n\t\"Ykd_JWqkR9gSLHISDBwP\": \"playlist-playlist-playlistInlineCurationSection\",\n\t\"SMJIXlalPk_TESlyt2pC\": \"playlist-playlist-playlistInlineCurationTitle\",\n\t\"g9xHCCSXDR8S5NvTbfwL\": \"playlist-playlist-playlistInlineCurationWrapper\",\n\t\"pbkk9BuHlY36lmWvEmbg\": \"playlist-playlist-promoImage\",\n\t\"DWkbhLMcDefEZwJ5jXCq\": \"playlist-playlist-promoRow\",\n\t\"byOUxNEoiJOtBN6xTY24\": \"playlist-playlist-promoTitle\",\n\t\"kwe0I8sSNMv3gYBjkRYP\": \"playlist-playlist-recommendedTrackList\",\n\t\"QmGi2oa43BTcEZ5MCr9T\": \"playlist-playlist-refreshButton\",\n\t\"KodyK77Gzjb8NqPGpcgw\": \"playlist-playlist-searchBoxContainer\",\n\t\"Wss0KoPtWlohVbZKp8n3\": \"playlist-playlist-searchBoxContainer\",\n\t\"sAPXlA_oxu_8x1Cn0NTC\": \"playlist-playlist-searchResultListContainer\",\n\t\"STDuzt77yRCueC4Ohenl\": \"playlist-playlist-seeMore\",\n\t\"NCKSUYdZaTMrobq8ilkc\": \"playlist-playlist-subtitle\",\n\t\"PZkwbwJD1afoCmJkGt8w\": \"playlist-playlist-top\",\n\t\"jpVuvMOCbpaRr_6FLf3W\": \"playlist-playlist-whiteOpacity\",\n\t\"kHu_FTRgoBLSLeAJtyKY\": \"profile-editImage-editImageButtonContainer\",\n\t\"vASn9mcl4gxUuplIX9Xy\": \"profile-editImage-editImageButtonContainer\",\n\t\"Ws8Ec3GREpT5PAUesr9b\": \"profile-editImage-imageContainer\",\n\t\"wCWKq_kJnqpBX20J2047\": \"profile-editImage-imageContainer\",\n\t\"zHeo4VUxytwm6Ptr0QyA\": \"profile-userEditDetails-closeButton\",\n\t\"bGC0b5iP1T6jelgvzJi6\": \"profile-userEditDetails-closeButton\",\n\t\"XwNfIrI6_hCa_9_T2cQB\": \"profile-userEditDetails-container\",\n\t\"D5Oc01xtlfhXQZqrlFzQ\": \"profile-userEditDetails-container\",\n\t\"so0bdX3oZH6YW5_nGxIR\": \"profile-userEditDetails-content\",\n\t\"pGEUqO9v_snvTufHtvLg\": \"profile-userEditDetails-content\",\n\t\"zGbjZMZ1DTx4futEbN9l\": \"profile-userEditDetails-disclaimer\",\n\t\"DD5nCHAWIgFQ7W7xUYkK\": \"profile-userEditDetails-disclaimer\",\n\t\"aM3plU4zzDqjWlvUHGYb\": \"profile-userEditDetails-header\",\n\t\"JjuNUgcpU5G8mP9fPFVx\": \"profile-userEditDetails-header\",\n\t\"F8_EX1AeKxXNSeh1qiHq\": \"profile-userEditDetails-image\",\n\t\"E_XIT3yw1mv2SGH9DwBO\": \"profile-userEditDetails-image\",\n\t\"wvLAEV5wF5C5ej6rvimT\": \"profile-userEditDetails-label\",\n\t\"P_Wd3HXMzT3JT4ftZ53c\": \"profile-userEditDetails-label\",\n\t\"gAQfzAUp1FuSXODeZJfP\": \"profile-userEditDetails-labelText\",\n\t\"zalgOdcLXMnwsiv2gej1\": \"profile-userEditDetails-labelText\",\n\t\"uj7hczcCH1dZpse8Kfmi\": \"profile-userEditDetails-name\",\n\t\"X6ndokDuE6CDp3667Fl1\": \"profile-userEditDetails-name\",\n\t\"oN9QVvJKEtdTH3HGfCu1\": \"profile-userEditDetails-nameInput\",\n\t\"DnJD_wCahoYkGOVQPqxf\": \"profile-userEditDetails-nameInput\",\n\t\"MDb7QhAtHeyM4gKj8j8t\": \"profile-userEditDetails-saveButton\",\n\t\"xNF0wfUgjZElohP3dgiS\": \"profile-userEditDetails-saveButton\",\n\t\"umiKMm5NVr5UeBJCHS6U\": \"profile-userOverview-container\",\n\t\"wDIZ2yYKjfGI68I4cZ98\": \"profile-userOverview-header\",\n\t\"rMpf7sfaPDcj387_52fA\": \"profile-userOverview-imageContainer\",\n\t\"jzhwZKbfx4vrC_MYd_7c\": \"profile-userOverview-section\",\n\t\"MWWPQQjbjRfoGdPD8D68\": \"profile-userOverview-subPage\",\n\t\"uJxNEI2k7x8UCDdMKELt\": \"profile-userOverview-title\",\n\t\"kWCnF32FrVtGHmTy8QeV\": \"profile-userOverview-topTrackSubPage\",\n\t\"TywOcKZEqNynWecCiATc\": \"progress-bar\",\n\t\"NP0jD9fPfkH_VmIJ4hEg\": \"progress-bar\",\n\t\"LS6q7yFFlYCqXoVxEETT\": \"progress-bar\",\n\t\"Vis45PPawTyED7Lt2_LI\": \"progress-bar__slider\",\n\t\"A9z4_R9gegKhgs_3D7Os\": \"progress-bar__slider\",\n\t\"sUkzLbFQJMcD0k4CVDRl\": \"progress-bar__slider\",\n\t\"DuvrswZugGajIFNXObAr\": \"progress-bar--isDragging\",\n\t\"gTvMl6pwfRD9PobMSB5x\": \"queue-queuePage-emptyContainer\",\n\t\"hNAQG0TAe2WFYyf_iZEB\": \"queue-queuePage-emptyContainerTitle\",\n\t\"Zhzrb2k9nQRActS2lp4U\": \"queue-queuePage-findSomething\",\n\t\"DG9CsoFIptJhAneKoo_F\": \"queue-queuePage-header\",\n\t\"H3Puuvc2nV0GoZRrfpRS\": \"queue-queuePage-nextFrom\",\n\t\"HckHyQocDDePWQL2baOY\": \"queue-queuePage-nextInQueue\",\n\t\"rHpv7osDRvs3SUPMpQ_g\": \"queue-queuePage-queuePage\",\n\t\"jf2HafzDEI9jn7Yo05eM\": \"queue-queuePage-subHeader\",\n\t\"oaNVBli46GtVjaQKB15g\": \"queue-tabBar-active\",\n\t\"FvDsfgxSvLvL3q8d7nQv\": \"queue-tabBar-chevron\",\n\t\"Nts_ArOCGeROTDZND3M6\": \"queue-tabBar-header\",\n\t\"muYk5XIwKmqR9iNibk_f\": \"queue-tabBar-headerIsCentered\",\n\t\"OEFWODerafYHGp09iLlA\": \"queue-tabBar-headerItem\",\n\t\"JdlKTdpMquftpMwwegZo\": \"queue-tabBar-headerItemLink\",\n\t\"m20ShRDiGGDpJ5LSABTi\": \"queue-tabBar-moreButton\",\n\t\"Hvv0e7WKQ4kyftgSQJhg\": \"queue-tabBar-moreButtonActive\",\n\t\"vhW0kRN8JJD5UwW4TdXi\": \"queue-tabBar-nav\",\n\t\"QdB2YtfEq0ks5O4QbtwX\": \"Root__cinema-view\",\n\t\"IiNKULzwo9JgYzlYVmhH\": \"Root__cinema-view\",\n\t\"FWlHsHhD0hSw1ldIXnOF\": \"Root__cinema-view--controls-hidden\",\n\t\"D8hDVfDlaGGt34V4nDGA\": \"Root__cinema-view--controls-hidden\",\n\t\"nRSfonXHVr6utXYgk2Ui\": \"Root__globalNav\",\n\t\"wp7mZFPzV7Qmo51F0NA_\": \"Root__globalNav\",\n\t\"wgtzoAN2iFV0pENAqJko\": \"Root__globalNav\",\n\t\"jEMA2gVoLgPQqAFrPhFw\": \"Root__main-view\",\n\t\"HD6RiDXIzzF9i4Bx26AE\": \"Root__main-view\",\n\t\"mMjg1Gizg9kYk8ILoTdp\": \"Root__main-view-overlay\",\n\t\"BdcvqBAid96FaHAmPYw_\": \"Root__nav-bar\",\n\t\"WBFaUw_oOfN2m4aTxggt\": \"Root__nav-bar\",\n\t\"JG5J9NWJkaUO9fiKECMA\": \"Root__now-playing-bar\",\n\t\"f9pLH3HRZQxdDLzNqKjE\": \"Root__now-playing-bar\",\n\t\"OTfMDdomT5S7B5dbYTT8\": \"Root__right-sidebar\",\n\t\"lAtoMFm8vg4yAlGztUxI\": \"Root__right-sidebar\",\n\t\"PHgyArRLVFknlaOm31ID\": \"Root__top-bar\",\n\t\"ZQftYELq0aOsg6tPbVbV\": \"Root__top-container\",\n\t\"POZtIm1wHFiwlxZY5i0a\": \"Root__top-container\",\n\t\"H1bRFdpa3qfekTVTeDwC\": \"Root__top-container--has-notice-bar\",\n\t\"WIPpgUp9J37Dwd0ZJnv0\": \"Root__top-container--right-sidebar-hidden\",\n\t\"lPapCDz3v_LipgXwe8gi\": \"Root__top-container--right-sidebar-hidden\",\n\t\"EBaPq6VUr6kjeHD9Wf2s\": \"Root__top-container--transition-enter\",\n\t\"nxyZPOEjDd5ToiXtSgdA\": \"Root__top-container--transition-exit\",\n\t\"v8nEufWSPrv1ql9ZdMko\": \"search-modal-emptySearchTermContainer\",\n\t\"sFFh5DkVxeEcgBGFOvUE\": \"search-modal-emptyStateContainer\",\n\t\"AnelgzgI75Dckf_LHUiK\": \"search-modal-entityImage\",\n\t\"aIWRvSjvEN4rTMCIi4vG\": \"search-modal-hasResults\",\n\t\"kn5N6aKpq6ebWinhrPPK\": \"search-modal-hidden\",\n\t\"v2oO4ItuH_0zk3OFj5dh\": \"search-modal-input\",\n\t\"QWeCmvys_7VINTbfUmGB\": \"search-modal-input\",\n\t\"kQ22nY00NOOrZjfmRP5J\": \"search-modal-inputContainer\",\n\t\"sKrYQkHlFOyAc0bM142q\": \"search-modal-isSelected\",\n\t\"EieXgtfUJKc4XQugVglV\": \"search-modal-key\",\n\t\"NU8xwWC0RWBRh_PBJdJe\": \"search-modal-keyboard-accessibility-bar\",\n\t\"SUOB9gChfkiToLTPl9Bc\": \"search-modal-listbox\",\n\t\"DoibN62ZFcOCvZ0f6xK5\": \"search-modal-modalAfterOpen\",\n\t\"_ZcO2wuO5d8P3TbjEukA\": \"search-modal-modalBase\",\n\t\"s75RW4QQV6LZ_NCdFtag\": \"search-modal-modalBeforeClose\",\n\t\"_p8ywioveAdTZ8yZmPfr\": \"search-modal-modalWrapper\",\n\t\"kUkjSLUuPyag37OAbVPH\": \"search-modal-resultItem\",\n\t\"ssvI7dCe2ZiLKChmS8tG\": \"search-modal-resultItem\",\n\t\"zi377dMLSwXnFiejYnRa\": \"search-modal-searchBar\",\n\t\"TQb3mB9R6qGsxvPnCurA\": \"search-modal-searchBar\",\n\t\"HN_3fmk5t15DGlzDbx1_\": \"search-modal-searchIcon\",\n\t\"NG1F0VTHk73cEJ8TNfOn\": \"search-modal-searchIcon\",\n\t\"wIyyGaSPOHR78wksX3Us\": \"search-modal-searchModalInstructions\",\n\t\"I9yJ0MC3kmodVJJlA6iq\": \"search-modal-searchResultRow\",\n\t\"tz3aYeI1uG6kMzJOPWr6\": \"search-modal-searchResultTitle\",\n\t\"TAHnl8KATdqiQLuz2TLv\": \"search-modal-searchResultType\",\n\t\"xol36rXFgZ_biOcw6Czk\": \"search-recentSearches-narrowPage\",\n\t\"khkfPsJuVBQyL_5cLT7y\": \"search-recentSearches-searchPageGrid\",\n\t\"a7lvtXATo3HALtrsOHtO\": \"search-recentSearches-seeAll\",\n\t\"rvvoAdb7aaUPYRasW7sK\": \"search-searchBrowse-browseAllContainer\",\n\t\"UdXTcsz1eiiInKThkfYp\": \"search-searchBrowse-browseAllWrapper\",\n\t\"M7LKuAFiIKaigK0fVguF\": \"search-searchBrowse-browseAllWrapper\",\n\t\"CCi1L2OQvgdZvxkRHeKE\": \"search-searchBrowse-SearchBrowse\",\n\t\"ijZQH9pePkbB2MbJHCJV\": \"search-searchCategory-carousel\",\n\t\"efpPrkQXWhVHykZxOGCQ\": \"search-searchCategory-carousel\",\n\t\"XTk61Y8OkBdUT6Wj4F6i\": \"search-searchCategory-carouselButton\",\n\t\"bsdZjMeYT0eYpTXrGNaH\": \"search-searchCategory-carouselButton\",\n\t\"VfDGbMWaJe9rcefizTNk\": \"search-searchCategory-carouselButtonVisible\",\n\t\"Nd2dSpwo9xYae8YuQIkb\": \"search-searchCategory-carouselButtonVisible\",\n\t\"KjPUGV8uMbl_0bvk9ePv\": \"search-searchCategory-categoryGrid\",\n\t\"TGtxFIzyLG46VAEOCaiI\": \"search-searchCategory-categoryGrid\",\n\t\"RXEjGtcNKiPQFxo613jX\": \"search-searchCategory-categoryGrid\",\n\t\"ZWI7JsjzJaR_G8Hy4W6J\": \"search-searchCategory-categoryGridItem\",\n\t\"UnwG2v9ISmcUhnjKj22Y\": \"search-searchCategory-categoryGridItem\",\n\t\"e179_Eg8r7Ub6yjjxctr\": \"search-searchCategory-container\",\n\t\"XAwhJzXeTM5Iv2jLMCEj\": \"search-searchCategory-container\",\n\t\"bMurPtRDRv5LuN78MTVG\": \"search-searchCategory-contentArea\",\n\t\"JDUqvfRssLaT4MgywPx0\": \"search-searchCategory-contentArea\",\n\t\"fVB_YDdnaDlztX7CcWTA\": \"search-searchCategory-SearchCategory\",\n\t\"qG4q41T8PJl0SkVgUeJc\": \"search-searchCategory-SearchCategory\",\n\t\"VIeVCUUETJyYPCDpsBif\": \"search-searchCategory-wrapper\",\n\t\"IDNDdMa6ACThrEsGWsXX\": \"search-searchCategory-wrapper\",\n\t\"nGARy02O1AklvHT7OBLA\": \"search-searchResult-searchResultGrid\",\n\t\"iGyMsGo7FgYQQThBj2y9\": \"search-searchResult-topResult\",\n\t\"eITFAR9yPwhjL_2gxB09\": \"search-searchResult-topResultCard\",\n\t\"Lj3brgOJmjcq6MQ22XKq\": \"search-searchResult-tracklist\",\n\t\"QVIrLvegL13F9cEdMqfT\": \"search-searchResult-tracklistContainer\",\n\t\"rP1oFnzzvss0GV6VPgGG\": \"search-searchResult-tracklistHeader\",\n\t\"PW9eULMYYH14XQgoJ0ui\": \"search-searchResult-tracklistHeaderText\",\n\t\"XQakH0M0GDc6g6JKeyds\": \"search-searchResult-tracklistHeaderWrapper\",\n\t\"rjgEnbv42_EUDbaiZnA2\": \"search-searchResult-tracklistLong\",\n\t\"EbZrO5qZMclA_AaI3NV8\": \"search-searchResult-tracklistShort\",\n\t\"DbMYFmOEEz9PH1h1zK9n\": \"show-episodeBlock-actions\",\n\t\"LbePDApGej12_NyRphHu\": \"show-episodeBlock-description\",\n\t\"upo8sAflD1byxWObSkgn\": \"show-episodeBlock-descriptionContainer\",\n\t\"hTRqaN61SDG95erQGMmx\": \"show-episodeBlock-episodeBlock\",\n\t\"V0pEigrddg3VxP_sTdAJ\": \"show-episodeBlock-header\",\n\t\"ij5_Bi2LfqgWwHzQBXJS\": \"show-episodeBlock-imageContainer\",\n\t\"y9kEPjDek0J80YRf8JJw\": \"show-episodeBlock-metadata\",\n\t\"o_TP9z7A8LQvMXujJC7N\": \"show-episodeBlock-showImage\",\n\t\"bG5fSAAS6rRL8xxU5iyG\": \"show-episodeBlock-title\",\n\t\"HLixBI5DbVZNC6lrUbAB\": \"show-episodeBlock-titleContainer\",\n\t\"g5gZaZVzR0tGT4pK6iEU\": \"show-episodeBlock-titleLink\",\n\t\"jtfSxoRam9rzTtdXIjzc\": \"show-show-episodesFilter\",\n\t\"ghfuv80I8uW_ymG_jfx9\": \"show-show-episodesFilter\",\n\t\"kR0M2WSYVUj4cohADSFM\": \"show-show-episodesHeader\",\n\t\"OodUnm1iCEZTUeL6X1gj\": \"show-show-moreButton\",\n\t\"aQMtxnKeiJqZ9XCcDuZ7\": \"show-showPage-sectionWrapper\",\n\t\"g3f_cI5usQX7ZOQyDtA9\": \"view-homeShortcutsGrid-draggable\",\n\t\"BKloN2GNmpktpuW0mQAs\": \"view-homeShortcutsGrid-draggable\",\n\t\"vpQWUBWS_lXRLZMRJT7w\": \"view-homeShortcutsGrid-episodeExtraContent\",\n\t\"GSv7K805J9Jw7LB9tn2A\": \"view-homeShortcutsGrid-episodeProgressBar\",\n\t\"jxXIarsEHgz2HoaVCVzA\": \"view-homeShortcutsGrid-equaliser\",\n\t\"nlOU1unbFAd7ZHyeSMTH\": \"view-homeShortcutsGrid-grid\",\n\t\"w0blQbt2NXnWLPlDFpNm\": \"view-homeShortcutsGrid-grid\",\n\t\"jdqzCrz9SoQp3ZUsU08w\": \"view-homeShortcutsGrid-gridOf4Columns\",\n\t\"gsCK1co9enL8tv4h7Cqe\": \"view-homeShortcutsGrid-gridOf4Columns\",\n\t\"ima0EKmsnCNUG08T82EM\": \"view-homeShortcutsGrid-iconDownloaded\",\n\t\"WWDxafTPs4AgThdcX5jN\": \"view-homeShortcutsGrid-image\",\n\t\"iXePCai5atydcl7RL5Mn\": \"view-homeShortcutsGrid-image\",\n\t\"jvWzgRWM_y_9FFTYRCcB\": \"view-homeShortcutsGrid-imageContainer\",\n\t\"w8fgcKCl6FoFioBbV4Zk\": \"view-homeShortcutsGrid-imageContainer\",\n\t\"zXwER4Lsqq_e7fVVaPkZ\": \"view-homeShortcutsGrid-imageWrapper\",\n\t\"KE9MrtvlIuoKtYKVr2wy\": \"view-homeShortcutsGrid-imageWrapper\",\n\t\"Tzzq1pG_inwo_oSOdyjb\": \"view-homeShortcutsGrid-main\",\n\t\"ICqWxTlKOkm9mUl_fSgS\": \"view-homeShortcutsGrid-main\",\n\t\"JFDEiqT_8B5zeG_CDSdK\": \"view-homeShortcutsGrid-name\",\n\t\"RidYQJ8faceMVnFLBEMC\": \"view-homeShortcutsGrid-name\",\n\t\"EzRmGRncgnv1zFgF4dqE\": \"view-homeShortcutsGrid-name\",\n\t\"lr4c5HqnzSyhBz8Y68M0\": \"view-homeShortcutsGrid-nameContainer\",\n\t\"TbrIq3NG2VYFoAUMSmp9\": \"view-homeShortcutsGrid-nameWrapper\",\n\t\"uy0HA5m_ehOLwt1y44DS\": \"view-homeShortcutsGrid-nameWrapper\",\n\t\"vq0lsCoYrDUDvkuUIaRg\": \"view-homeShortcutsGrid-playButton\",\n\t\"wvTlGX9H5RlvLD9JmMFD\": \"view-homeShortcutsGrid-playButton\",\n\t\"Kcb74zm1aMqGfPxTwO5s\": \"view-homeShortcutsGrid-PlayButtonContainer\",\n\t\"iuJndmfe5nTDK5FQch9C\": \"view-homeShortcutsGrid-PlayButtonContainer\",\n\t\"s9c9x_mJq197U2hBzGtV\": \"view-homeShortcutsGrid-PlayButtonContainerVisible\",\n\t\"DqAHiYCkcPCcDorezUKI\": \"view-homeShortcutsGrid-recentlyPlayedShortcutIcon\",\n\t\"Z35BWOA10YGn5uc9YgAp\": \"view-homeShortcutsGrid-shortcut\",\n\t\"mFip2ELQbL3MKPkNpa2R\": \"view-homeShortcutsGrid-shortcut\",\n\t\"jb9xD5ECTqKFK02qe3HZ\": \"view-homeShortcutsGrid-shortcutLink\",\n\t\"pE62MKOt7O7y8dGnTySw\": \"view-homeShortcutsGrid-shortcutLink\",\n\t\"bPmmKmSPLKMhtJSaUJRX\": \"view-homeShortcutsGrid-shortcutNewEpisodeIndicator\",\n\t\"rPV8BmHZXaGIGT2HwvBB\": \"view-homeShortcutsGrid-shortcuts\",\n\t\"Y89c1_2SAoZFkICK7WVp\": \"view-homeShortcutsGrid-shortcuts\",\n\t\"sLw5dxB32cAxVxqiar7J\": \"view-homeShortcutsGrid-title\",\n\t\"lt7UxuNqHQBCO6IWyA_G\": \"view-homeShortcutsGrid-trailingIndicator\",\n\t\"WPGf6HbizJpLXHLbETa5\": \"view-homeShortcutsGrid-trailingIndicator\",\n\t\"Z9WJyI5OCGSXF82DxhOH\": \"view-homeShortcutsGrid-trailingIndicator\",\n\t\"eEZSnYlv7__34b2yulfm\": \"volume-bar\",\n\t\"lhIRi4qz54GmbEZahPwS\": \"volume-bar\",\n\t\"G4n5bTzWUvlftzDwrFVG\": \"volume-bar\",\n\t\"FZhaXNtbN3Crwrgd0TA7\": \"volume-bar__icon-button\",\n\t\"rT09bwCEwXECMbLbX_7A\": \"volume-bar__icon-button\",\n\t\"GI7bVF6DBlyfIpWQKQs1\": \"volume-bar__icon-button\",\n\t\"eyxRppgmsuUUNbazkw3q\": \"volume-bar__slider-container\",\n\t\"x1jWng8HDweDS840aiIA\": \"volume-bar__slider-container\",\n\t\"tIr7C6B0Pt6YKdOnqaqj\": \"volume-bar__slider-container\",\n\t\"M_nS5sMI0NvZNLeFgFVa\": \"watchFeed-pillFour-keyframe\",\n\t\"jVGLy9ny4f46DxxtuC9z\": \"watchFeed-pillOne-keyframe\",\n\t\"DNVx4QdehkJ8two0tNVO\": \"watchFeed-pillThree-keyframe\",\n\t\"WUOx_rJjDlJ0yebh23UI\": \"watchFeed-pillTwo-keyframe\",\n\t\"fLS8v3_EfBadEerbGVoR\": \"x-album-releasesDropdown\",\n\t\"iqDYHLUedgX8y1eM1zep\": \"x-carousel-button\",\n\t\"ge8K7iQqLE77g0FDGfDn\": \"x-carousel-carousel\",\n\t\"Em2LrSSfvrgXQoajs6cm\": \"x-categoryCard-CategoryCard\",\n\t\"jXeqeqkxEBVeFjA2YydA\": \"x-categoryCard-CategoryCard\",\n\t\"tV9cjMpTPaykKsn2OVsw\": \"x-categoryCard-image\",\n\t\"Op2n2H4o1iY0Xo2wAUH9\": \"x-categoryCard-image\",\n\t\"i2yp6pOoZpYZLd5QWguN\": \"x-categoryCard-title\",\n\t\"bQthUEx0_U98DJkT1saO\": \"x-categoryCard-title\",\n\t\"jMg2yhvAA3YfgM1Ix5GL\": \"x-contributorButton-button\",\n\t\"X1lXSiVj0pzhQCUo_72A\": \"x-contributorButton-ContributorButton\",\n\t\"HbKLiGoYM4dpuK8L4TMX\": \"x-downloadButton-button\",\n\t\"VmwiDoU6RpqyzK_n7XRO\": \"x-downloadButton-cancelDownload\",\n\t\"l_MW0G9qeeCKlVJwBykT\": \"x-downloadButton-cancelDownloadButton\",\n\t\"BKsbV2Xl786X9a09XROH\": \"x-downloadButton-DownloadButton\",\n\t\"GWCBhKJqeZal3n5tCQwl\": \"x-downloadButton-DownloadButton\",\n\t\"rEx3EYgBzS8SoY7dmC6x\": \"x-downloadButton-progress\",\n\t\"_APVWqivXc4YqgsnpFkP\": \"x-downloadButton-removeDownload\",\n\t\"OadpZJiOaGfX6Qp4j6n5\": \"x-entityImage-circle\",\n\t\"AeEoI6ueagbJtaHl2cRd\": \"x-entityImage-defaultSize\",\n\t\"iJp40IxKg6emF6KYJ414\": \"x-entityImage-image\",\n\t\"vreceNX3ABcxyddeS83B\": \"x-entityImage-imageContainer\",\n\t\"lKuMkIKSZanMIK6aQWfx\": \"x-entityImage-imageContainer\",\n\t\"Ozitxbqs1vcOukDz3GDw\": \"x-entityImage-imagePlaceholder\",\n\t\"Toq4yy4bj8GGMSF5oWbj\": \"x-entityImage-imagePlaceholder\",\n\t\"SBpny8HrUTBzSjk7Vtk1\": \"x-entityImage-large\",\n\t\"H71KtIrytVayf_dFofu7\": \"x-entityImage-medium\",\n\t\"O5_0cReFdHe81E0xFAD1\": \"x-entityImage-small\",\n\t\"CxurIfvXVb_TqGF4q8Yf\": \"x-entityImage-square\",\n\t\"g3kBhX1E4EYEC2NFhhxG\": \"x-entityImage-xsmall\",\n\t\"hn6wKYDgRJLk3ObMeArw\": \"x-entityImage-xsmall\",\n\t\"SgFtsvn3upY_tG6mnt4n\": \"x-explicit-icon\",\n\t\"zUF7IRW3lpivOZHjWRko\": \"x-explicit-icon\",\n\t\"Ps9zgW56WZaBVLo1n3cg\": \"x-explicit-label\",\n\t\"CBiDL2Ry7eHHcTjB4SME\": \"x-explicit-label\",\n\t\"EsqEJ_NPrHsPkTbX4FW8\": \"x-filterBox-clearButton\",\n\t\"wCl7pMTEE68v1xuZeZiB\": \"x-filterBox-expandButton\",\n\t\"kFl3NAPY_5yenNQk7VpZ\": \"x-filterBox-expandButton\",\n\t\"t6HIrX67Lp80Nj6tGauz\": \"x-filterBox-expandedOrHasFilter\",\n\t\"_quDRjKzF4E0L6Qv0l_F\": \"x-filterBox-expandedOrHasFilter\",\n\t\"KAydWoHSkQRqhQ1vkVwK\": \"x-filterBox-expandRight\",\n\t\"QZhV0hWVKlExlKr266jo\": \"x-filterBox-filterInput\",\n\t\"A1ZtjTtf0TzOYLOBjaQe\": \"x-filterBox-filterInput\",\n\t\"JzZyf6OGCGtdscOZGt8Y\": \"x-filterBox-filterInputContainer\",\n\t\"CNJRzwGSOPxBBfje5FEK\": \"x-filterBox-filterInputContainer\",\n\t\"iiNt4l0UwY2wL42vyBc2\": \"x-filterBox-fullWidth\",\n\t\"uAJxc_psYWeimY8N9bH9\": \"x-filterBox-overlay\",\n\t\"nt15x8uJhSbH9flWJ8r6\": \"x-filterBox-overlay\",\n\t\"CIVozJ8XNPJ60uMN23Yg\": \"x-filterBox-searchIcon\",\n\t\"AFy8ta8zNE0nUMR7Dwdk\": \"x-filterBox-searchIcon\",\n\t\"_bjbHn5TABOW2s5LsEGX\": \"x-filterBox-searchIconContainer\",\n\t\"_oM_IwFBK0iLCBHKYvEp\": \"x-filterBox-searchIconContainer\",\n\t\"epWhU7hHGktzlO_dop6z\": \"x-progressBar-fillColor\",\n\t\"pghDjOdspOgysFT72xOx\": \"x-progressBar-fillColor\",\n\t\"JZ3wV4Y5GgAD0UFsQpE7\": \"x-progressBar-fillColor\",\n\t\"NoOAOv6U6vtqj_ybS1Cd\": \"x-progressBar-progressBarBg\",\n\t\"BDW4CFlIaMu9sHJRFCCg\": \"x-progressBar-progressBarBg\",\n\t\"NuZQwzwm2yKxIGrFO_y4\": \"x-progressBar-progressBarBg\",\n\t\"w699O0LgQRghXyl3bs9u\": \"x-progressBar-sliderArea\",\n\t\"JRShuZ9K3QC6x8Nl0FMu\": \"x-progressBar-sliderArea\",\n\t\"nfLzZOKzyFjyut6toQIa\": \"x-progressBar-sliderArea\",\n\t\"SfuwkNwMOfcRX0KF6cPa\": \"x-progressBar-wrapper\",\n\t\"ENGp3mqoFqfGLHott9_k\": \"x-progressBar-enabled\",\n\t\"eVonGEFHbUE9S2xfVA8d\": \"x-progressBar-progressBar\",\n\t\"jLzAN9dQidF6eUHqVFk_\": \"x-progressBar-background\",\n\t\"uVRQz1zaxSUQVQ0s06iw\": \"x-progressBar-foregroundWrapper\",\n\t\"Xm6MvwqQLnIU1TANX6Rt\": \"x-progressBar-middleground\",\n\t\"vSotTDmUOWTotCYudlgI\": \"x-progressBar-foreground\",\n\t\"snYg67O_WNutGrCsqzIZ\": \"x-progressBar-tooltipAnchor\",\n\t\"REMgVhoMClSNRZve5xaJ\": \"x-progressBar-handle\",\n\t\"SjP3wobd2iE2kK8vEIsg\": \"x-progressBar-progressAnimatable\",\n\t\"_DrNS3e4ylFBzHyCWDF0\": \"x-progressBar-disableProgressAnimatable\",\n\t\"edMDVHlUyeCR51HK_LGe\": \"x-progressBar-progressBarActive\",\n\t\"UHDS6Tgm15et79EOkj_9\": \"x-progressBar-saberEnabled\",\n\t\"mKfEQ7SMFopnDLydx4HP\": \"x-progressBar-chapterHoverOverlay\",\n\t\"AavUTirpKSAHapguSAy0\": \"x-proxySettings-fullWidth\",\n\t\"qfjicQPaTTPrLWJWRxMQ\": \"x-proxySettings-horizontalPair\",\n\t\"YZUWpDgQNo3KQwdRQHh3\": \"x-proxySettings-horizontalPair\",\n\t\"acVhMt5pELcXQyLaaPuV\": \"x-proxySettings-ProxySettings\",\n\t\"k5yrn4bTRFh4dMUyEtX2\": \"x-proxySettings-ProxySettings\",\n\t\"fOEOTcOAgPryvbYRYfOo\": \"x-searchHistoryEntries-clearSingleSearchHistory\",\n\t\"bONdaZB1x9i_WIeOBxEC\": \"x-searchHistoryEntries-clearSingleSearchHistory\",\n\t\"xmJl0s8mcJ3bfhtnoaP1\": \"x-searchHistoryEntries-clearSingleSearchHistoryButton\",\n\t\"fTpRygNKlEhmZyrgy6hD\": \"x-searchHistoryEntries-clearSingleSearchHistoryButton\",\n\t\"ADri2r8kq8LVqSsNNvIr\": \"x-searchHistoryEntries-searchHistoryEntry\",\n\t\"rKHLepPL9WCKR7yfqS1s\": \"x-searchHistoryEntries-searchHistoryEntry\",\n\t\"ZtY42R4YSo_W7VMeAg9m\": \"x-searchInput-searchInputClearButton\",\n\t\"LWugd2SlihapRT0PX8K3\": \"x-searchInput-searchInputClearButton\",\n\t\"mOLTJ2mxkzHJj6Y9_na_\": \"x-searchInput-searchInputClearIcon\",\n\t\"t2K4_iLmAyDtH7mcT5Wy\": \"x-searchInput-searchInputIconContainer\",\n\t\"lJ81QkQ1Aw5AK50afb9c\": \"x-searchInput-searchInputIconContainer\",\n\t\"QO9loc33XC50mMRUCIvf\": \"x-searchInput-searchInputInput\",\n\t\"NtkAQg9R1r5CjuP0XHwl\": \"x-searchInput-searchInputInput\",\n\t\"Mx356zpxhMqMxje_7QXv\": \"x-searchInput-searchInputInput\",\n\t\"XD3TMuMHmKsfbqieC6q_\": \"x-searchInput-searchInputOnSearch\",\n\t\"H6jh9Xd7DNOq3NsLDmCB\": \"x-searchInput-searchInputSearchIcon\",\n\t\"aCQCOVyiFROs_qr_DvQ4\": \"x-searchInput-searchInputSearchIcon\",\n\t\"MpoH5sdgCUbPL5LCl3Cy\": \"x-searchInput-searchInputSearchIcon\",\n\t\"rFFJg1UIumqUUFDgo6n7\": \"x-settings-button\",\n\t\"l_pugTMA53sS_K65iEiW\": \"x-settings-button\",\n\t\"LOsH9AUZc2uFRlhqtpRT\": \"x-settings-container\",\n\t\"xQ33L7BC8Xbpdjo1gCAU\": \"x-settings-container\",\n\t\"xbm3VfL2kntDlxtyDKwj\": \"x-settings-crossFadeContainer\",\n\t\"gO8pl5RByZlVaPDbaAVu\": \"x-settings-crossFadeContainer\",\n\t\"gv7Rcc2ouDRSd0pto7Df\": \"x-settings-equalizerPanelCanvas\",\n\t\"i9YYQi3sdgQiftnT0G7l\": \"x-settings-equalizerPanelCanvas\",\n\t\"zrn877LGjVA_oYp2IKeu\": \"x-settings-equalizerPanelFilter\",\n\t\"KBoHP10jGFBIjRt0TilW\": \"x-settings-equalizerPanelFilter\",\n\t\"aOnWJeLmOj8plFB5QPSt\": \"x-settings-equalizerPanelFilters\",\n\t\"yApUWWS53HH6f8UXAOrD\": \"x-settings-equalizerPanelFilters\",\n\t\"L9sAZBDUVTnJLn7TqR1E\": \"x-settings-equalizerPanelGainLabel\",\n\t\"DgABXUkFFAu6h3dxMlER\": \"x-settings-equalizerPanelGainLabel\",\n\t\"aPIPHZU8F7TMi6LEw_Yq\": \"x-settings-equalizerPanelGainLabelDown\",\n\t\"W5vAyrwphGDgfEcVvWCG\": \"x-settings-equalizerPanelGainLabelDown\",\n\t\"YKGOZh1y8HVOqQgbJB9F\": \"x-settings-equalizerPanelGainLabelUp\",\n\t\"L6g4d49oWcb89EH_UvI1\": \"x-settings-equalizerPanelGainLabelUp\",\n\t\"KGnDzV9IPjGQ8Ude8Cgl\": \"x-settings-equalizerPanelInput\",\n\t\"N4bVba_JCN915eH2yz7O\": \"x-settings-equalizerPanelInput\",\n\t\"NWmefh0djDBBQmHr_mdf\": \"x-settings-equalizerPanelLabel\",\n\t\"LpIve1Boio_yH1npDnzz\": \"x-settings-equalizerPanelLabel\",\n\t\"PumO62RLWafcexCJx0oe\": \"x-settings-equalizerPanelPreset\",\n\t\"HGN6qJ4noxOV2YSJWj3g\": \"x-settings-equalizerPanelWrapper\",\n\t\"S20UWmwnvAky4mj2zHXQ\": \"x-settings-equalizerPanelWrapper\",\n\t\"j7cPtD65ArW8eWnGNrUo\": \"x-settings-equalizerPresetsContainer\",\n\t\"xPMt4zCAUHum2VzFuHhR\": \"x-settings-equalizerPresetsContainer\",\n\t\"ZzNb8P1Bz1VHtlhimIWM\": \"x-settings-equalizerPresetsLabel\",\n\t\"mJOG8oySLSgkKcRKQEcw\": \"x-settings-equalizerPresetsLabel\",\n\t\"PiFWoUIRceOm8SHTCakS\": \"x-settings-equalizerResetButtonWrapper\",\n\t\"p5IaREhBZJIUrqcK5ifE\": \"x-settings-equalizerResetButtonWrapper\",\n\t\"xK6HEWejcSHKyWfhNiJc\": \"x-settings-equalizerSection\",\n\t\"PZqn_zrcjFHRxNwlhrCY\": \"x-settings-equalizerSection\",\n\t\"FulR95cAh4QPdw6wUeRw\": \"x-settings-equalizerWrapper\",\n\t\"hdG4NraLUWvgQWKmA3PQ\": \"x-settings-equalizerWrapper\",\n\t\"GMGmbx5FRBd6DOVvzSgk\": \"x-settings-firstColumn\",\n\t\"g2SG95QPZfbn5RINccth\": \"x-settings-firstColumn\",\n\t\"lfXDZUXLhhKhFPjDO8by\": \"x-settings-firstColumn\",\n\t\"NnDXf1J9xlVM5AUuqVt1\": \"x-settings-header\",\n\t\"Euj6Lot2A7ir5T4xBiTf\": \"x-settings-header\",\n\t\"DQ9fp5DjBJxKHeHqtFwC\": \"x-settings-headerContainer\",\n\t\"lcSvuotO0TXX8S7X6D_A\": \"x-settings-headerContainer\",\n\t\"V4Bh2Ch7KvYUdn2s9ZdX\": \"x-settings-hidden\",\n\t\"SkbGMKYv49KtJNB5XxdX\": \"x-settings-input\",\n\t\"QDmFXu7LLdLf6M3BASsu\": \"x-settings-input\",\n\t\"gvcgOXnAiNKEe_z92_lw\": \"x-settings-restartAppButton\",\n\t\"gdRu9OyPL_MvmSMgE042\": \"x-settings-restartAppButton\",\n\t\"weV_qxFz4gF5sPotO10y\": \"x-settings-row\",\n\t\"BMtRRwqaJD_95vJFMFD0\": \"x-settings-row\",\n\t\"eguwzH_QWTBXry7hiNj3\": \"x-settings-row\",\n\t\"yNitN64xoLNhzJlkfzOh\": \"x-settings-secondColumn\",\n\t\"rtzkwMH3kqwgnS_BxP_t\": \"x-settings-secondColumn\",\n\t\"jKCZodyn7H2Trr7dhvGm\": \"x-settings-secondColumn\",\n\t\"c6TyNYOUJRIsjYZJZofy\": \"x-settings-section\",\n\t\"k6GFKyOKVR5Ruofj3aTQ\": \"x-settings-section\",\n\t\"YtAW7cQal8op8H9JkJ8T\": \"x-settings-section\",\n\t\"GuwMf98GUBSpCDgf8KRA\": \"x-settings-tooltip\",\n\t\"NLh3bMez5RC2mwsYOf2g\": \"x-settings-tooltip\",\n\t\"nW1RKQOkzcJcX6aDCZB4\": \"x-settings-tooltipIcon\",\n\t\"RDrhzf8eX7cW85q3CuCw\": \"x-settings-tooltipIcon\",\n\t\"qBZYab2T7Yc4O5Nh0mjA\": \"x-settings-tooltipIconWrapper\",\n\t\"m8DaD7yVdc8BD9wWBC2Z\": \"x-settings-tooltipIconWrapper\",\n\t\"l35q2le3C8eAxw1TKELD\": \"x-settings-wordBreakAll\",\n\t\"w6j_vX6SF5IxSXrrkYw5\": \"x-sortBox-sortDropdown\",\n\t\"fvlp70y1LlEYYjgC8yEs\": \"x-sortBox-sortDropdown\",\n\t\"es9mguuOfkp6pBe1Bjlw\": \"x-toggle-indicator\",\n\t\"sxTbfT6ioOgvOvHzaBE7\": \"x-toggle-indicator\",\n\t\"Js64TOfWtHksI6TQ6knT\": \"x-toggle-indicatorWrapper\",\n\t\"Qb0gCQFXpstteRqnAF9q\": \"x-toggle-indicatorWrapper\",\n\t\"n8tsDTs8wDH73kejYfXs\": \"x-toggle-input\",\n\t\"Smo4wLHtFoFOOsJP0evo\": \"x-toggle-input\",\n\t\"ztL0S6Lyoye5upzDS_yU\": \"x-toggle-wrapper\",\n\t\"JWYoNAyrIIdW30u4PSGE\": \"x-toggle-wrapper\"\n}\n"
  },
  {
    "path": "globals.d.ts",
    "content": "declare namespace Spicetify {\n\ttype Icon =\n\t\t| \"album\"\n\t\t| \"artist\"\n\t\t| \"block\"\n\t\t| \"brightness\"\n\t\t| \"car\"\n\t\t| \"chart-down\"\n\t\t| \"chart-up\"\n\t\t| \"check\"\n\t\t| \"check-alt-fill\"\n\t\t| \"chevron-left\"\n\t\t| \"chevron-right\"\n\t\t| \"chromecast-disconnected\"\n\t\t| \"clock\"\n\t\t| \"collaborative\"\n\t\t| \"computer\"\n\t\t| \"copy\"\n\t\t| \"download\"\n\t\t| \"downloaded\"\n\t\t| \"edit\"\n\t\t| \"enhance\"\n\t\t| \"exclamation-circle\"\n\t\t| \"external-link\"\n\t\t| \"facebook\"\n\t\t| \"follow\"\n\t\t| \"fullscreen\"\n\t\t| \"gamepad\"\n\t\t| \"grid-view\"\n\t\t| \"heart\"\n\t\t| \"heart-active\"\n\t\t| \"instagram\"\n\t\t| \"laptop\"\n\t\t| \"library\"\n\t\t| \"list-view\"\n\t\t| \"location\"\n\t\t| \"locked\"\n\t\t| \"locked-active\"\n\t\t| \"lyrics\"\n\t\t| \"menu\"\n\t\t| \"minimize\"\n\t\t| \"minus\"\n\t\t| \"more\"\n\t\t| \"new-spotify-connect\"\n\t\t| \"offline\"\n\t\t| \"pause\"\n\t\t| \"phone\"\n\t\t| \"play\"\n\t\t| \"playlist\"\n\t\t| \"playlist-folder\"\n\t\t| \"plus-alt\"\n\t\t| \"plus2px\"\n\t\t| \"podcasts\"\n\t\t| \"projector\"\n\t\t| \"queue\"\n\t\t| \"repeat\"\n\t\t| \"repeat-once\"\n\t\t| \"search\"\n\t\t| \"search-active\"\n\t\t| \"shuffle\"\n\t\t| \"skip-back\"\n\t\t| \"skip-back15\"\n\t\t| \"skip-forward\"\n\t\t| \"skip-forward15\"\n\t\t| \"soundbetter\"\n\t\t| \"speaker\"\n\t\t| \"spotify\"\n\t\t| \"subtitles\"\n\t\t| \"tablet\"\n\t\t| \"ticket\"\n\t\t| \"twitter\"\n\t\t| \"visualizer\"\n\t\t| \"voice\"\n\t\t| \"volume\"\n\t\t| \"volume-off\"\n\t\t| \"volume-one-wave\"\n\t\t| \"volume-two-wave\"\n\t\t| \"watch\"\n\t\t| \"x\";\n\ttype Variant =\n\t\t| \"bass\"\n\t\t| \"forte\"\n\t\t| \"brio\"\n\t\t| \"altoBrio\"\n\t\t| \"alto\"\n\t\t| \"canon\"\n\t\t| \"celloCanon\"\n\t\t| \"cello\"\n\t\t| \"ballad\"\n\t\t| \"balladBold\"\n\t\t| \"viola\"\n\t\t| \"violaBold\"\n\t\t| \"mesto\"\n\t\t| \"mestoBold\"\n\t\t| \"metronome\"\n\t\t| \"finale\"\n\t\t| \"finaleBold\"\n\t\t| \"minuet\"\n\t\t| \"minuetBold\";\n\ttype SemanticColor =\n\t\t| \"textBase\"\n\t\t| \"textSubdued\"\n\t\t| \"textBrightAccent\"\n\t\t| \"textNegative\"\n\t\t| \"textWarning\"\n\t\t| \"textPositive\"\n\t\t| \"textAnnouncement\"\n\t\t| \"essentialBase\"\n\t\t| \"essentialSubdued\"\n\t\t| \"essentialBrightAccent\"\n\t\t| \"essentialNegative\"\n\t\t| \"essentialWarning\"\n\t\t| \"essentialPositive\"\n\t\t| \"essentialAnnouncement\"\n\t\t| \"decorativeBase\"\n\t\t| \"decorativeSubdued\"\n\t\t| \"backgroundBase\"\n\t\t| \"backgroundHighlight\"\n\t\t| \"backgroundPress\"\n\t\t| \"backgroundElevatedBase\"\n\t\t| \"backgroundElevatedHighlight\"\n\t\t| \"backgroundElevatedPress\"\n\t\t| \"backgroundTintedBase\"\n\t\t| \"backgroundTintedHighlight\"\n\t\t| \"backgroundTintedPress\"\n\t\t| \"backgroundUnsafeForSmallTextBase\"\n\t\t| \"backgroundUnsafeForSmallTextHighlight\"\n\t\t| \"backgroundUnsafeForSmallTextPress\";\n\ttype ColorSet =\n\t\t| \"base\"\n\t\t| \"brightAccent\"\n\t\t| \"negative\"\n\t\t| \"warning\"\n\t\t| \"positive\"\n\t\t| \"announcement\"\n\t\t| \"invertedDark\"\n\t\t| \"invertedLight\"\n\t\t| \"mutedAccent\"\n\t\t| \"overMedia\";\n\ttype ColorSetBackgroundColors = {\n\t\tbase: string;\n\t\thighlight: string;\n\t\tpress: string;\n\t};\n\ttype ColorSetNamespaceColors = {\n\t\tannouncement: string;\n\t\tbase: string;\n\t\tbrightAccent: string;\n\t\tnegative: string;\n\t\tpositive: string;\n\t\tsubdued: string;\n\t\twarning: string;\n\t};\n\ttype ColorSetBody = {\n\t\tbackground: ColorSetBackgroundColors & {\n\t\t\televated: ColorSetBackgroundColors;\n\t\t\ttinted: ColorSetBackgroundColors;\n\t\t\tunsafeForSmallText: ColorSetBackgroundColors;\n\t\t};\n\t\tdecorative: {\n\t\t\tbase: string;\n\t\t\tsubdued: string;\n\t\t};\n\t\tessential: ColorSetNamespaceColors;\n\t\ttext: ColorSetNamespaceColors;\n\t};\n\ttype Metadata = Partial<Record<string, string>>;\n\ttype ContextTrack = {\n\t\turi: string;\n\t\tuid?: string;\n\t\tmetadata?: Metadata;\n\t};\n\ttype PlayerState = {\n\t\ttimestamp: number;\n\t\tcontext: PlayerContext;\n\t\tindex: PlayerIndex;\n\t\titem: PlayerTrack;\n\t\tshuffle: boolean;\n\t\tsmartShuffle: boolean;\n\t\trepeat: number;\n\t\tspeed: number;\n\t\tpositionAsOfTimestamp: number;\n\t\tduration: number;\n\t\thasContext: boolean;\n\t\tisPaused: boolean;\n\t\tisBuffering: boolean;\n\t\trestrictions: Restrictions;\n\t\tpreviousItems?: PlayerTrack[];\n\t\tnextItems?: PlayerTrack[];\n\t\tplaybackQuality: PlaybackQuality;\n\t\tplaybackId: string;\n\t\tsessionId: string;\n\t\tsignals?: any[];\n\t};\n\ttype PlayerContext = {\n\t\turi: string;\n\t\turl: string;\n\t\tmetadata: {\n\t\t\t\"player.arch\": string;\n\t\t};\n\t};\n\ttype PlayerIndex = {\n\t\tpageURI?: string | null;\n\t\tpageIndex: number;\n\t\titemIndex: number;\n\t};\n\ttype PlayerTrack = {\n\t\ttype: string;\n\t\turi: string;\n\t\tuid: string;\n\t\tname: string;\n\t\tmediaType: string;\n\t\tduration: {\n\t\t\tmilliseconds: number;\n\t\t};\n\t\talbum: Album;\n\t\tartists?: ArtistsEntity[];\n\t\tisLocal: boolean;\n\t\tisExplicit: boolean;\n\t\tis19PlusOnly: boolean;\n\t\tprovider: string;\n\t\tmetadata: TrackMetadata;\n\t\timages?: ImagesEntity[];\n\t};\n\ttype TrackMetadata = {\n\t\tartist_uri: string;\n\t\tentity_uri: string;\n\t\titeration: string;\n\t\ttitle: string;\n\t\t\"collection.is_banned\": string;\n\t\t\"artist_uri:1\": string;\n\t\t\"collection.in_collection\": string;\n\t\timage_small_url: string;\n\t\t\"collection.can_ban\": string;\n\t\tis_explicit: string;\n\t\talbum_disc_number: string;\n\t\talbum_disc_count: string;\n\t\ttrack_player: string;\n\t\talbum_title: string;\n\t\t\"canvas.artist.avatar\": string;\n\t\t\"canvas.artist.name\": string;\n\t\t\"canvas.artist.uri\": string;\n\t\t\"canvas.canvasUri\": string;\n\t\t\"canvas.entityUri\": string;\n\t\t\"canvas.explicit\": string;\n\t\t\"canvas.fileId\": string;\n\t\t\"canvas.id\": string;\n\t\t\"canvas.type\": string;\n\t\t\"canvas.uploadedBy\": string;\n\t\t\"canvas.url\": string;\n\t\t\"collection.can_add\": string;\n\t\timage_large_url: string;\n\t\t\"actions.skipping_prev_past_track\": string;\n\t\tpage_instance_id: string;\n\t\timage_xlarge_url: string;\n\t\tmarked_for_download: string;\n\t\t\"actions.skipping_next_past_track\": string;\n\t\tcontext_uri: string;\n\t\t\"artist_name:1\": string;\n\t\thas_lyrics: string;\n\t\tinteraction_id: string;\n\t\timage_url: string;\n\t\talbum_uri: string;\n\t\talbum_artist_name: string;\n\t\talbum_track_number: string;\n\t\tartist_name: string;\n\t\tduration: string;\n\t\talbum_track_count: string;\n\t\tpopularity: string;\n\t\tassociated_video_id: string;\n\t\tvideo_association: string;\n\t\tvideo_association_image: string;\n\t\tvideo_association_image_height: string;\n\t\tvideo_association_image_height_large: string;\n\t\tvideo_association_image_height_xxlarge: string;\n\t\tvideo_association_image_large: string;\n\t\tvideo_association_image_width: string;\n\t\tvideo_association_image_width_large: string;\n\t\tvideo_association_image_width_xxlarge: string;\n\t\tvideo_association_image_xxlarge: string;\n\t\t[key: string]: string;\n\t};\n\ttype Album = {\n\t\ttype: string;\n\t\turi: string;\n\t\tname: string;\n\t\timages?: ImagesEntity[];\n\t};\n\ttype ImagesEntity = {\n\t\turl: string;\n\t\tlabel: string;\n\t};\n\ttype ArtistsEntity = {\n\t\ttype: string;\n\t\turi: string;\n\t\tname: string;\n\t};\n\ttype Restrictions = {\n\t\tcanPause: boolean;\n\t\tcanResume: boolean;\n\t\tcanSeek: boolean;\n\t\tcanSkipPrevious: boolean;\n\t\tcanSkipNext: boolean;\n\t\tcanToggleRepeatContext: boolean;\n\t\tcanToggleRepeatTrack: boolean;\n\t\tcanToggleShuffle: boolean;\n\t\tdisallowPausingReasons?: string[];\n\t\tdisallowResumingReasons?: string[];\n\t\tdisallowSeekingReasons?: string[];\n\t\tdisallowSkippingPreviousReasons?: string[];\n\t\tdisallowSkippingNextReasons?: string[];\n\t\tdisallowTogglingRepeatContextReasons?: string[];\n\t\tdisallowTogglingRepeatTrackReasons?: string[];\n\t\tdisallowTogglingShuffleReasons?: string[];\n\t\tdisallowTransferringPlaybackReasons?: string[];\n\t};\n\ttype PlaybackQuality = {\n\t\tbitrateLevel: number;\n\t\tstrategy: number;\n\t\ttargetBitrateLevel: number;\n\t\ttargetBitrateAvailable: boolean;\n\t\thifiStatus: number;\n\t};\n\tnamespace Player {\n\t\t/**\n\t\t *\n\t\t * Contains vast array of internal APIs.\n\t\t * Please explore in Devtool Console.\n\t\t */\n\t\tconst origin: any;\n\t\t/**\n\t\t * Register a listener `type` on Spicetify.Player.\n\t\t *\n\t\t * On default, `Spicetify.Player` always dispatch:\n\t\t *  - `songchange` type when player changes track.\n\t\t *  - `onplaypause` type when player plays or pauses.\n\t\t *  - `onprogress` type when track progress changes.\n\t\t *  - `appchange` type when user changes page.\n\t\t */\n\t\tfunction addEventListener(type: string, callback: (event?: Event) => void): void;\n\t\tfunction addEventListener(type: \"songchange\", callback: (event?: Event & { data: PlayerState }) => void): void;\n\t\tfunction addEventListener(type: \"onplaypause\", callback: (event?: Event & { data: PlayerState }) => void): void;\n\t\tfunction addEventListener(type: \"onprogress\", callback: (event?: Event & { data: number }) => void): void;\n\t\tfunction addEventListener(\n\t\t\ttype: \"appchange\",\n\t\t\tcallback: (\n\t\t\t\tevent?: Event & {\n\t\t\t\t\tdata: {\n\t\t\t\t\t\t/**\n\t\t\t\t\t\t * App href path\n\t\t\t\t\t\t */\n\t\t\t\t\t\tpath: string;\n\t\t\t\t\t\t/**\n\t\t\t\t\t\t * App container\n\t\t\t\t\t\t */\n\t\t\t\t\t\tcontainer: HTMLElement;\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t) => void\n\t\t): void;\n\t\t/**\n\t\t * Skip to previous track.\n\t\t */\n\t\tfunction back(): void;\n\t\t/**\n\t\t * An object contains all information about current track and player.\n\t\t */\n\t\tconst data: PlayerState;\n\t\t/**\n\t\t * Decrease a small amount of volume.\n\t\t */\n\t\tfunction decreaseVolume(): void;\n\t\t/**\n\t\t * Dispatches an event at `Spicetify.Player`.\n\t\t *\n\t\t * On default, `Spicetify.Player` always dispatch\n\t\t *  - `songchange` type when player changes track.\n\t\t *  - `onplaypause` type when player plays or pauses.\n\t\t *  - `onprogress` type when track progress changes.\n\t\t *  - `appchange` type when user changes page.\n\t\t */\n\t\tfunction dispatchEvent(event: Event): void;\n\t\tconst eventListeners: {\n\t\t\t[key: string]: Array<(event?: Event) => void>;\n\t\t};\n\t\t/**\n\t\t * Convert milisecond to `mm:ss` format\n\t\t * @param milisecond\n\t\t */\n\t\tfunction formatTime(milisecond: number): string;\n\t\t/**\n\t\t * Return song total duration in milisecond.\n\t\t */\n\t\tfunction getDuration(): number;\n\t\t/**\n\t\t * Return mute state\n\t\t */\n\t\tfunction getMute(): boolean;\n\t\t/**\n\t\t * Return elapsed duration in milisecond.\n\t\t */\n\t\tfunction getProgress(): number;\n\t\t/**\n\t\t * Return elapsed duration in percentage (0 to 1).\n\t\t */\n\t\tfunction getProgressPercent(): number;\n\t\t/**\n\t\t * Return current Repeat state (No repeat = 0/Repeat all = 1/Repeat one = 2).\n\t\t */\n\t\tfunction getRepeat(): number;\n\t\t/**\n\t\t * Return current shuffle state.\n\t\t */\n\t\tfunction getShuffle(): boolean;\n\t\t/**\n\t\t * Return track heart state.\n\t\t */\n\t\tfunction getHeart(): boolean;\n\t\t/**\n\t\t * Return current volume level (0 to 1).\n\t\t */\n\t\tfunction getVolume(): number;\n\t\t/**\n\t\t * Increase a small amount of volume.\n\t\t */\n\t\tfunction increaseVolume(): void;\n\t\t/**\n\t\t * Return a boolean whether player is playing.\n\t\t */\n\t\tfunction isPlaying(): boolean;\n\t\t/**\n\t\t * Skip to next track.\n\t\t */\n\t\tfunction next(): void;\n\t\t/**\n\t\t * Pause track.\n\t\t */\n\t\tfunction pause(): void;\n\t\t/**\n\t\t * Resume track.\n\t\t */\n\t\tfunction play(): void;\n\t\t/**\n\t\t * Play a track, playlist, album, etc. immediately\n\t\t * @param uri Spotify URI\n\t\t * @param context\n\t\t * @param options\n\t\t */\n\t\tfunction playUri(uri: string, context?: any, options?: any): Promise<void>;\n\t\t/**\n\t\t * Unregister added event listener `type`.\n\t\t * @param type\n\t\t * @param callback\n\t\t */\n\t\tfunction removeEventListener(type: string, callback: (event?: Event) => void): void;\n\t\t/**\n\t\t * Seek track to position.\n\t\t * @param position can be in percentage (0 to 1) or in milisecond.\n\t\t */\n\t\tfunction seek(position: number): void;\n\t\t/**\n\t\t * Turn mute on/off\n\t\t * @param state\n\t\t */\n\t\tfunction setMute(state: boolean): void;\n\t\t/**\n\t\t * Change Repeat mode\n\t\t * @param mode `0` No repeat. `1` Repeat all. `2` Repeat one track.\n\t\t */\n\t\tfunction setRepeat(mode: number): void;\n\t\t/**\n\t\t * Turn shuffle on/off.\n\t\t * @param state\n\t\t */\n\t\tfunction setShuffle(state: boolean): void;\n\t\t/**\n\t\t * Set volume level\n\t\t * @param level 0 to 1\n\t\t */\n\t\tfunction setVolume(level: number): void;\n\t\t/**\n\t\t * Seek to previous `amount` of milisecond\n\t\t * @param amount in milisecond. Default: 15000.\n\t\t */\n\t\tfunction skipBack(amount?: number): void;\n\t\t/**\n\t\t * Seek to next  `amount` of milisecond\n\t\t * @param amount in milisecond. Default: 15000.\n\t\t */\n\t\tfunction skipForward(amount?: number): void;\n\t\t/**\n\t\t * Toggle Heart (Favourite) track state.\n\t\t */\n\t\tfunction toggleHeart(): void;\n\t\t/**\n\t\t * Toggle Mute/No mute.\n\t\t */\n\t\tfunction toggleMute(): void;\n\t\t/**\n\t\t * Toggle Play/Pause.\n\t\t */\n\t\tfunction togglePlay(): void;\n\t\t/**\n\t\t * Toggle No repeat/Repeat all/Repeat one.\n\t\t */\n\t\tfunction toggleRepeat(): void;\n\t\t/**\n\t\t * Toggle Shuffle/No shuffle.\n\t\t */\n\t\tfunction toggleShuffle(): void;\n\t}\n\t/**\n\t * Adds a track or array of tracks to prioritized queue.\n\t */\n\tfunction addToQueue(uri: ContextTrack[]): Promise<void>;\n\t/**\n\t * @deprecated\n\t */\n\tconst BridgeAPI: any;\n\t/**\n\t * @deprecated\n\t */\n\tconst CosmosAPI: any;\n\t/**\n\t * Async wrappers of CosmosAPI\n\t */\n\tnamespace CosmosAsync {\n\t\ttype Method = \"DELETE\" | \"GET\" | \"HEAD\" | \"PATCH\" | \"POST\" | \"PUT\" | \"SUB\";\n\t\tinterface Error {\n\t\t\tcode: number;\n\t\t\terror: string;\n\t\t\tmessage: string;\n\t\t\tstack?: string;\n\t\t}\n\n\t\ttype Headers = Record<string, string>;\n\t\ttype Body = Record<string, any>;\n\n\t\tinterface Response {\n\t\t\tbody: any;\n\t\t\theaders: Headers;\n\t\t\tstatus: number;\n\t\t\turi?: string;\n\t\t}\n\n\t\tfunction head(url: string, headers?: Headers): Promise<Headers>;\n\t\tfunction get(url: string, body?: Body, headers?: Headers): Promise<Response[\"body\"]>;\n\t\tfunction post(url: string, body?: Body, headers?: Headers): Promise<Response[\"body\"]>;\n\t\tfunction put(url: string, body?: Body, headers?: Headers): Promise<Response[\"body\"]>;\n\t\tfunction del(url: string, body?: Body, headers?: Headers): Promise<Response[\"body\"]>;\n\t\tfunction patch(url: string, body?: Body, headers?: Headers): Promise<Response[\"body\"]>;\n\t\tfunction sub(\n\t\t\turl: string,\n\t\t\tcallback: (b: Response[\"body\"]) => void,\n\t\t\tonError?: (e: Error) => void,\n\t\t\tbody?: Body,\n\t\t\theaders?: Headers\n\t\t): Promise<Response[\"body\"]>;\n\t\tfunction postSub(\n\t\t\turl: string,\n\t\t\tbody: Body | null,\n\t\t\tcallback: (b: Response[\"body\"]) => void,\n\t\t\tonError?: (e: Error) => void\n\t\t): Promise<Response[\"body\"]>;\n\t\tfunction request(method: Method, url: string, body?: Body, headers?: Headers): Promise<Response>;\n\t\tfunction resolve(method: Method, url: string, body?: Body, headers?: Headers): Promise<Response>;\n\t}\n\t/**\n\t * Fetch interesting colors from URI.\n\t * @param uri Any type of URI that has artwork (playlist, track, album, artist, show, ...)\n\t */\n\tfunction colorExtractor(uri: string): Promise<{\n\t\tDARK_VIBRANT: string;\n\t\tDESATURATED: string;\n\t\tLIGHT_VIBRANT: string;\n\t\tPROMINENT: string;\n\t\tVIBRANT: string;\n\t\tVIBRANT_NON_ALARMING: string;\n\t}>;\n\t/**\n\t * @deprecated\n\t */\n\tfunction getAblumArtColors(): any;\n\t/**\n\t * Fetch track analyzed audio data.\n\t * Beware, not all tracks have audio data.\n\t * @param uri is optional. Leave it blank to get current track\n\t * or specify another track uri.\n\t */\n\tfunction getAudioData(uri?: string): Promise<any>;\n\t/**\n\t * Set of APIs method to register, deregister hotkeys/shortcuts\n\t */\n\tnamespace Keyboard {\n\t\ttype ValidKey =\n\t\t\t| \"BACKSPACE\"\n\t\t\t| \"TAB\"\n\t\t\t| \"ENTER\"\n\t\t\t| \"SHIFT\"\n\t\t\t| \"CTRL\"\n\t\t\t| \"ALT\"\n\t\t\t| \"CAPS\"\n\t\t\t| \"ESCAPE\"\n\t\t\t| \"SPACE\"\n\t\t\t| \"PAGE_UP\"\n\t\t\t| \"PAGE_DOWN\"\n\t\t\t| \"END\"\n\t\t\t| \"HOME\"\n\t\t\t| \"ARROW_LEFT\"\n\t\t\t| \"ARROW_UP\"\n\t\t\t| \"ARROW_RIGHT\"\n\t\t\t| \"ARROW_DOWN\"\n\t\t\t| \"INSERT\"\n\t\t\t| \"DELETE\"\n\t\t\t| \"A\"\n\t\t\t| \"B\"\n\t\t\t| \"C\"\n\t\t\t| \"D\"\n\t\t\t| \"E\"\n\t\t\t| \"F\"\n\t\t\t| \"G\"\n\t\t\t| \"H\"\n\t\t\t| \"I\"\n\t\t\t| \"J\"\n\t\t\t| \"K\"\n\t\t\t| \"L\"\n\t\t\t| \"M\"\n\t\t\t| \"N\"\n\t\t\t| \"O\"\n\t\t\t| \"P\"\n\t\t\t| \"Q\"\n\t\t\t| \"R\"\n\t\t\t| \"S\"\n\t\t\t| \"T\"\n\t\t\t| \"U\"\n\t\t\t| \"V\"\n\t\t\t| \"W\"\n\t\t\t| \"X\"\n\t\t\t| \"Y\"\n\t\t\t| \"Z\"\n\t\t\t| \"WINDOW_LEFT\"\n\t\t\t| \"WINDOW_RIGHT\"\n\t\t\t| \"SELECT\"\n\t\t\t| \"NUMPAD_0\"\n\t\t\t| \"NUMPAD_1\"\n\t\t\t| \"NUMPAD_2\"\n\t\t\t| \"NUMPAD_3\"\n\t\t\t| \"NUMPAD_4\"\n\t\t\t| \"NUMPAD_5\"\n\t\t\t| \"NUMPAD_6\"\n\t\t\t| \"NUMPAD_7\"\n\t\t\t| \"NUMPAD_8\"\n\t\t\t| \"NUMPAD_9\"\n\t\t\t| \"MULTIPLY\"\n\t\t\t| \"ADD\"\n\t\t\t| \"SUBTRACT\"\n\t\t\t| \"DECIMAL_POINT\"\n\t\t\t| \"DIVIDE\"\n\t\t\t| \"F1\"\n\t\t\t| \"F2\"\n\t\t\t| \"F3\"\n\t\t\t| \"F4\"\n\t\t\t| \"F5\"\n\t\t\t| \"F6\"\n\t\t\t| \"F7\"\n\t\t\t| \"F8\"\n\t\t\t| \"F9\"\n\t\t\t| \"F10\"\n\t\t\t| \"F11\"\n\t\t\t| \"F12\"\n\t\t\t| \";\"\n\t\t\t| \"=\"\n\t\t\t| \" | \"\n\t\t\t| \"-\"\n\t\t\t| \".\"\n\t\t\t| \"/\"\n\t\t\t| \"`\"\n\t\t\t| \"[\"\n\t\t\t| \"\\\\\"\n\t\t\t| \"]\"\n\t\t\t| '\"'\n\t\t\t| \"~\"\n\t\t\t| \"!\"\n\t\t\t| \"@\"\n\t\t\t| \"#\"\n\t\t\t| \"$\"\n\t\t\t| \"%\"\n\t\t\t| \"^\"\n\t\t\t| \"&\"\n\t\t\t| \"*\"\n\t\t\t| \"(\"\n\t\t\t| \")\"\n\t\t\t| \"_\"\n\t\t\t| \"+\"\n\t\t\t| \":\"\n\t\t\t| \"<\"\n\t\t\t| \">\"\n\t\t\t| \"?\"\n\t\t\t| \"|\";\n\t\ttype KeysDefine =\n\t\t\t| string\n\t\t\t| {\n\t\t\t\t\tkey: string;\n\t\t\t\t\tctrl?: boolean;\n\t\t\t\t\tshift?: boolean;\n\t\t\t\t\talt?: boolean;\n\t\t\t\t\tmeta?: boolean;\n\t\t\t  };\n\t\tconst KEYS: Record<ValidKey, string>;\n\t\tfunction registerShortcut(keys: KeysDefine, callback: (event: KeyboardEvent) => void): void;\n\t\tfunction registerIsolatedShortcut(keys: KeysDefine, callback: (event: KeyboardEvent) => void): void;\n\t\tfunction registerImportantShortcut(keys: KeysDefine, callback: (event: KeyboardEvent) => void): void;\n\t\tfunction _deregisterShortcut(keys: KeysDefine): void;\n\t\tfunction deregisterImportantShortcut(keys: KeysDefine): void;\n\t\tfunction changeShortcut(keys: KeysDefine, newKeys: KeysDefine): void;\n\t}\n\n\t/**\n\t * @deprecated\n\t */\n\tconst LiveAPI: any;\n\n\tnamespace LocalStorage {\n\t\t/**\n\t\t * Empties the list associated with the object of all key/value pairs, if there are any.\n\t\t */\n\t\tfunction clear(): void;\n\t\t/**\n\t\t * Get key value\n\t\t */\n\t\tfunction get(key: string): string | null;\n\t\t/**\n\t\t * Delete key\n\t\t */\n\t\tfunction remove(key: string): void;\n\t\t/**\n\t\t * Set new value for key\n\t\t */\n\t\tfunction set(key: string, value: string): void;\n\t}\n\t/**\n\t * To create and prepend custom menu item in profile menu.\n\t */\n\tnamespace Menu {\n\t\t/**\n\t\t * Create a single toggle.\n\t\t */\n\t\tclass Item {\n\t\t\tconstructor(name: string, isEnabled: boolean, onClick: (self: Item) => void, icon?: Icon | string);\n\t\t\tname: string;\n\t\t\tisEnabled: boolean;\n\t\t\t/**\n\t\t\t * Change item name\n\t\t\t */\n\t\t\tsetName(name: string): void;\n\t\t\t/**\n\t\t\t * Change item enabled state.\n\t\t\t * Visually, item would has a tick next to it if its state is enabled.\n\t\t\t */\n\t\t\tsetState(isEnabled: boolean): void;\n\t\t\t/**\n\t\t\t * Change icon\n\t\t\t */\n\t\t\tsetIcon(icon: Icon | string): void;\n\t\t\t/**\n\t\t\t * Item is only available in Profile menu when method \"register\" is called.\n\t\t\t */\n\t\t\tregister(): void;\n\t\t\t/**\n\t\t\t * Stop item to be prepended into Profile menu.\n\t\t\t */\n\t\t\tderegister(): void;\n\t\t}\n\n\t\t/**\n\t\t * Create a sub menu to contain Item toggles.\n\t\t * `Item`s in `subItems` array shouldn't be registered.\n\t\t */\n\t\tclass SubMenu {\n\t\t\tconstructor(name: string, subItems: Item[]);\n\t\t\tname: string;\n\t\t\t/**\n\t\t\t * Change SubMenu name\n\t\t\t */\n\t\t\tsetName(name: string): void;\n\t\t\t/**\n\t\t\t * Add an item to sub items list\n\t\t\t */\n\t\t\taddItem(item: Item): void;\n\t\t\t/**\n\t\t\t * Remove an item from sub items list\n\t\t\t */\n\t\t\tremoveItem(item: Item): void;\n\t\t\t/**\n\t\t\t * SubMenu is only available in Profile menu when method \"register\" is called.\n\t\t\t */\n\t\t\tregister(): void;\n\t\t\t/**\n\t\t\t * Stop SubMenu to be prepended into Profile menu.\n\t\t\t */\n\t\t\tderegister(): void;\n\t\t}\n\t}\n\n\t/**\n\t * Keyboard shortcut library\n\t *\n\t * Documentation: https://craig.is/killing/mice v1.6.5\n\t *\n\t * Spicetify.Keyboard is wrapper of this library to be compatible with legacy Spotify,\n\t * so new extension should use this library instead.\n\t */\n\tfunction Mousetrap(element?: any): void;\n\n\t/**\n\t * Contains vast array of internal APIs.\n\t * Please explore in Devtool Console.\n\t */\n\tconst Platform: any;\n\t/**\n\t * Queue object contains list of queuing tracks,\n\t * history of played tracks and current track metadata.\n\t */\n\tconst Queue: {\n\t\tnextTracks: any[];\n\t\tprevTracks: any[];\n\t\tqueueRevision: string;\n\t\ttrack: any;\n\t};\n\t/**\n\t * Remove a track or array of tracks from current queue.\n\t */\n\tfunction removeFromQueue(uri: ContextTrack[]): Promise<void>;\n\t/**\n\t * Display a bubble of notification. Useful for a visual feedback.\n\t * @param message Message to display. Can use inline HTML for styling.\n\t * @param isError If true, bubble will be red. Defaults to false.\n\t * @param msTimeout Time in milliseconds to display the bubble. Defaults to Spotify's value.\n\t */\n\tfunction showNotification(message: React.ReactNode, isError?: boolean, msTimeout?: number): void;\n\t/**\n\t * Set of APIs method to parse and validate URIs.\n\t */\n\tclass URI {\n\t\tconstructor(type: string, props: any);\n\t\tpublic type: string;\n\t\tpublic hasBase62Id: boolean;\n\n\t\tpublic id?: string;\n\t\tpublic disc?: any;\n\t\tpublic args?: any;\n\t\tpublic category?: string;\n\t\tpublic username?: string;\n\t\tpublic track?: string;\n\t\tpublic artist?: string;\n\t\tpublic album?: string;\n\t\tpublic duration?: number;\n\t\tpublic query?: string;\n\t\tpublic country?: string;\n\t\tpublic global?: boolean;\n\t\tpublic context?: string | typeof URI | null;\n\t\tpublic anchor?: string;\n\t\tpublic play?: any;\n\t\tpublic toplist?: any;\n\n\t\t/**\n\t\t *\n\t\t * @return The URI representation of this uri.\n\t\t */\n\t\ttoURI(): string;\n\n\t\t/**\n\t\t *\n\t\t * @return The URI representation of this uri.\n\t\t */\n\t\ttoString(): string;\n\n\t\t/**\n\t\t * Get the URL path of this uri.\n\t\t *\n\t\t * @param opt_leadingSlash True if a leading slash should be prepended.\n\t\t * @return The path of this uri.\n\t\t */\n\t\ttoURLPath(opt_leadingSlash: boolean): string;\n\n\t\t/**\n\t\t *\n\t\t * @param origin The origin to use for the URL.\n\t\t * @return The URL string for the uri.\n\t\t */\n\t\ttoURL(origin?: string): string;\n\n\t\t/**\n\t\t * Clones a given SpotifyURI instance.\n\t\t *\n\t\t * @return An instance of URI.\n\t\t */\n\t\tclone(): URI | null;\n\n\t\t/**\n\t\t * Gets the path of the URI object by removing all hash and query parameters.\n\t\t *\n\t\t * @return The path of the URI object.\n\t\t */\n\t\tgetPath(): string;\n\n\t\t/**\n\t\t * The various URI Types.\n\t\t *\n\t\t * Note that some of the types in this enum are not real URI types, but are\n\t\t * actually URI particles. They are marked so.\n\t\t *\n\t\t */\n\t\tstatic Type: {\n\t\t\tAD: string;\n\t\t\tALBUM: string;\n\t\t\tGENRE: string;\n\t\t\tQUEUE: string;\n\t\t\tAPPLICATION: string;\n\t\t\tARTIST: string;\n\t\t\tARTIST_TOPLIST: string;\n\t\t\tARTIST_CONCERTS: string;\n\t\t\tAUDIO_FILE: string;\n\t\t\tCOLLECTION: string;\n\t\t\tCOLLECTION_ALBUM: string;\n\t\t\tCOLLECTION_ARTIST: string;\n\t\t\tCOLLECTION_MISSING_ALBUM: string;\n\t\t\tCOLLECTION_TRACK_LIST: string;\n\t\t\tCONCERT: string;\n\t\t\tCONTEXT_GROUP: string;\n\t\t\tDAILY_MIX: string;\n\t\t\tEMPTY: string;\n\t\t\tEPISODE: string;\n\t\t\t/** URI particle; not an actual URI. */\n\t\t\tFACEBOOK: string;\n\t\t\tFOLDER: string;\n\t\t\tFOLLOWERS: string;\n\t\t\tFOLLOWING: string;\n\t\t\tIMAGE: string;\n\t\t\tINBOX: string;\n\t\t\tINTERRUPTION: string;\n\t\t\tLIBRARY: string;\n\t\t\tLIVE: string;\n\t\t\tROOM: string;\n\t\t\tEXPRESSION: string;\n\t\t\tLOCAL: string;\n\t\t\tLOCAL_TRACK: string;\n\t\t\tLOCAL_ALBUM: string;\n\t\t\tLOCAL_ARTIST: string;\n\t\t\tMERCH: string;\n\t\t\tMOSAIC: string;\n\t\t\tPLAYLIST: string;\n\t\t\tPLAYLIST_V2: string;\n\t\t\tPRERELEASE: string;\n\t\t\tPROFILE: string;\n\t\t\tPUBLISHED_ROOTLIST: string;\n\t\t\tRADIO: string;\n\t\t\tROOTLIST: string;\n\t\t\tSEARCH: string;\n\t\t\tSHOW: string;\n\t\t\tSOCIAL_SESSION: string;\n\t\t\tSPECIAL: string;\n\t\t\tSTARRED: string;\n\t\t\tSTATION: string;\n\t\t\tTEMP_PLAYLIST: string;\n\t\t\tTOPLIST: string;\n\t\t\tTRACK: string;\n\t\t\tTRACKSET: string;\n\t\t\tUSER_TOPLIST: string;\n\t\t\tUSER_TOP_TRACKS: string;\n\t\t\tUNKNOWN: string;\n\t\t\tMEDIA: string;\n\t\t\tQUESTION: string;\n\t\t\tPOLL: string;\n\t\t};\n\n\t\t/**\n\t\t * Creates a new URI object from a parsed string argument.\n\t\t *\n\t\t * @param str The string that will be parsed into a URI object.\n\t\t * @throws TypeError If the string argument is not a valid URI, a TypeError will\n\t\t *     be thrown.\n\t\t * @return The parsed URI object.\n\t\t */\n\t\tstatic fromString(str: string): URI;\n\n\t\t/**\n\t\t * Parses a given object into a URI instance.\n\t\t *\n\t\t * Unlike URI.fromString, this function could receive any kind of value. If\n\t\t * the value is already a URI instance, it is simply returned.\n\t\t * Otherwise the value will be stringified before parsing.\n\t\t *\n\t\t * This function also does not throw an error like URI.fromString, but\n\t\t * instead simply returns null if it can't parse the value.\n\t\t *\n\t\t * @param value The value to parse.\n\t\t * @return The corresponding URI instance, or null if the\n\t\t *     passed value is not a valid value.\n\t\t */\n\t\tstatic from(value: any): URI | null;\n\n\t\t/**\n\t\t * Checks whether two URI:s refer to the same thing even though they might\n\t\t * not necessarily be equal.\n\t\t *\n\t\t * These two Playlist URIs, for example, refer to the same playlist:\n\t\t *\n\t\t *   spotify:user:napstersean:playlist:3vxotOnOGDlZXyzJPLFnm2\n\t\t *   spotify:playlist:3vxotOnOGDlZXyzJPLFnm2\n\t\t *\n\t\t * @param baseUri The first URI to compare.\n\t\t * @param refUri The second URI to compare.\n\t\t * @return Whether they shared idenitity\n\t\t */\n\t\tstatic isSameIdentity(baseUri: URI | string, refUri: URI | string): boolean;\n\n\t\t/**\n\t\t * Returns the hex representation of a Base62 encoded id.\n\t\t *\n\t\t * @param id The base62 encoded id.\n\t\t * @return The hex representation of the base62 id.\n\t\t */\n\t\tstatic idToHex(id: string): string;\n\n\t\t/**\n\t\t * Returns the base62 representation of a hex encoded id.\n\t\t *\n\t\t * @param hex The hex encoded id.\n\t\t * @return The base62 representation of the id.\n\t\t */\n\t\tstatic hexToId(hex: string): string;\n\n\t\t/**\n\t\t * Creates a new 'album' type URI.\n\t\t *\n\t\t * @param id The id of the album.\n\t\t * @param disc The disc number of the album.\n\t\t * @return The album URI.\n\t\t */\n\t\tstatic albumURI(id: string, disc: number): URI;\n\n\t\t/**\n\t\t * Creates a new 'application' type URI.\n\t\t *\n\t\t * @param id The id of the application.\n\t\t * @param args An array containing the arguments to the app.\n\t\t * @return The application URI.\n\t\t */\n\t\tstatic applicationURI(id: string, args: string[]): URI;\n\n\t\t/**\n\t\t * Creates a new 'artist' type URI.\n\t\t *\n\t\t * @param id The id of the artist.\n\t\t * @return The artist URI.\n\t\t */\n\t\tstatic artistURI(id: string): URI;\n\n\t\t/**\n\t\t * Creates a new 'collection' type URI.\n\t\t *\n\t\t * @param username The non-canonical username of the rootlist owner.\n\t\t * @param category The category of the collection.\n\t\t * @return The collection URI.\n\t\t */\n\t\tstatic collectionURI(username: string, category: string): URI;\n\n\t\t/**\n\t\t * Creates a new 'collection-album' type URI.\n\t\t *\n\t\t * @param username The non-canonical username of the rootlist owner.\n\t\t * @param id The id of the album.\n\t\t * @return The collection album URI.\n\t\t */\n\t\tstatic collectionAlbumURI(username: string, id: string): URI;\n\n\t\t/**\n\t\t * Creates a new 'collection-artist' type URI.\n\t\t *\n\t\t * @param username The non-canonical username of the rootlist owner.\n\t\t * @param id The id of the artist.\n\t\t * @return The collection artist URI.\n\t\t */\n\t\tstatic collectionAlbumURI(username: string, id: string): URI;\n\n\t\t/**\n\t\t * Creates a new 'concert' type URI.\n\t\t *\n\t\t * @param id The id of the concert.\n\t\t * @return The concert URI.\n\t\t */\n\t\tstatic concertURI(id: string): URI;\n\n\t\t/**\n\t\t * Creates a new 'episode' type URI.\n\t\t *\n\t\t * @param id The id of the episode.\n\t\t * @return The episode URI.\n\t\t */\n\t\tstatic episodeURI(id: string): URI;\n\n\t\t/**\n\t\t * Creates a new 'folder' type URI.\n\t\t *\n\t\t * @param id The id of the folder.\n\t\t * @return The folder URI.\n\t\t */\n\t\tstatic folderURI(id: string): URI;\n\n\t\t/**\n\t\t * Creates a new 'local-album' type URI.\n\t\t *\n\t\t * @param artist The artist of the album.\n\t\t * @param album The name of the album.\n\t\t * @return The local album URI.\n\t\t */\n\t\tstatic localAlbumURI(artist: string, album: string): URI;\n\n\t\t/**\n\t\t * Creates a new 'local-artist' type URI.\n\t\t *\n\t\t * @param artist The name of the artist.\n\t\t * @return The local artist URI.\n\t\t */\n\t\tstatic localArtistURI(artist: string): URI;\n\n\t\t/**\n\t\t * Creates a new 'playlist-v2' type URI.\n\t\t *\n\t\t * @param id The id of the playlist.\n\t\t * @return The playlist URI.\n\t\t */\n\t\tstatic playlistV2URI(id: string): URI;\n\n\t\t/**\n\t\t * Creates a new 'prerelease' type URI.\n\t\t *\n\t\t * @param id The id of the prerelease.\n\t\t * @return The prerelease URI.\n\t\t */\n\t\tstatic prereleaseURI(id: string): URI;\n\n\t\t/**\n\t\t * Creates a new 'profile' type URI.\n\t\t *\n\t\t * @param username The non-canonical username of the rootlist owner.\n\t\t * @param args A list of arguments.\n\t\t * @return The profile URI.\n\t\t */\n\t\tstatic profileURI(username: string, args: string[]): URI;\n\n\t\t/**\n\t\t * Creates a new 'search' type URI.\n\t\t *\n\t\t * @param query The unencoded search query.\n\t\t * @return The search URI\n\t\t */\n\t\tstatic searchURI(query: string): URI;\n\n\t\t/**\n\t\t * Creates a new 'show' type URI.\n\t\t *\n\t\t * @param id The id of the show.\n\t\t * @return The show URI.\n\t\t */\n\t\tstatic showURI(id: string): URI;\n\n\t\t/**\n\t\t * Creates a new 'station' type URI.\n\t\t *\n\t\t * @param args An array of arguments for the station.\n\t\t * @return The station URI.\n\t\t */\n\t\tstatic stationURI(args: string[]): URI;\n\n\t\t/**\n\t\t * Creates a new 'track' type URI.\n\t\t *\n\t\t * @param id The id of the track.\n\t\t * @param anchor The point in the track formatted as mm:ss\n\t\t * @param context An optional context URI\n\t\t * @param play Toggles autoplay\n\t\t * @return The track URI.\n\t\t */\n\t\tstatic trackURI(id: string, anchor: string, context?: string, play?: boolean): URI;\n\n\t\t/**\n\t\t * Creates a new 'user-toplist' type URI.\n\t\t *\n\t\t * @param username The non-canonical username of the toplist owner.\n\t\t * @param toplist The toplist type.\n\t\t * @return The user-toplist URI.\n\t\t */\n\t\tstatic userToplistURI(username: string, toplist: string): URI;\n\n\t\tstatic isAd(uri: URI | string): boolean;\n\t\tstatic isAlbum(uri: URI | string): boolean;\n\t\tstatic isGenre(uri: URI | string): boolean;\n\t\tstatic isQueue(uri: URI | string): boolean;\n\t\tstatic isApplication(uri: URI | string): boolean;\n\t\tstatic isArtist(uri: URI | string): boolean;\n\t\tstatic isArtistToplist(uri: URI | string): boolean;\n\t\tstatic isArtistConcerts(uri: URI | string): boolean;\n\t\tstatic isAudioFile(uri: URI | string): boolean;\n\t\tstatic isCollection(uri: URI | string): boolean;\n\t\tstatic isCollectionAlbum(uri: URI | string): boolean;\n\t\tstatic isCollectionArtist(uri: URI | string): boolean;\n\t\tstatic isCollectionMissingAlbum(uri: URI | string): boolean;\n\t\tstatic isCollectionTrackList(uri: URI | string): boolean;\n\t\tstatic isConcert(uri: URI | string): boolean;\n\t\tstatic isContextGroup(uri: URI | string): boolean;\n\t\tstatic isDailyMix(uri: URI | string): boolean;\n\t\tstatic isEmpty(uri: URI | string): boolean;\n\t\tstatic isEpisode(uri: URI | string): boolean;\n\t\tstatic isFacebook(uri: URI | string): boolean;\n\t\tstatic isFolder(uri: URI | string): boolean;\n\t\tstatic isFollowers(uri: URI | string): boolean;\n\t\tstatic isFollowing(uri: URI | string): boolean;\n\t\tstatic isImage(uri: URI | string): boolean;\n\t\tstatic isInbox(uri: URI | string): boolean;\n\t\tstatic isInterruption(uri: URI | string): boolean;\n\t\tstatic isLibrary(uri: URI | string): boolean;\n\t\tstatic isLive(uri: URI | string): boolean;\n\t\tstatic isRoom(uri: URI | string): boolean;\n\t\tstatic isExpression(uri: URI | string): boolean;\n\t\tstatic isLocal(uri: URI | string): boolean;\n\t\tstatic isLocalTrack(uri: URI | string): boolean;\n\t\tstatic isLocalAlbum(uri: URI | string): boolean;\n\t\tstatic isLocalArtist(uri: URI | string): boolean;\n\t\tstatic isMerch(uri: URI | string): boolean;\n\t\tstatic isMosaic(uri: URI | string): boolean;\n\t\tstatic isPlaylist(uri: URI | string): boolean;\n\t\tstatic isPlaylistV2(uri: URI | string): boolean;\n\t\tstatic isPrerelease(uri: URI | string): boolean;\n\t\tstatic isProfile(uri: URI | string): boolean;\n\t\tstatic isPublishedRootlist(uri: URI | string): boolean;\n\t\tstatic isRadio(uri: URI | string): boolean;\n\t\tstatic isRootlist(uri: URI | string): boolean;\n\t\tstatic isSearch(uri: URI | string): boolean;\n\t\tstatic isShow(uri: URI | string): boolean;\n\t\tstatic isSocialSession(uri: URI | string): boolean;\n\t\tstatic isSpecial(uri: URI | string): boolean;\n\t\tstatic isStarred(uri: URI | string): boolean;\n\t\tstatic isStation(uri: URI | string): boolean;\n\t\tstatic isTempPlaylist(uri: URI | string): boolean;\n\t\tstatic isToplist(uri: URI | string): boolean;\n\t\tstatic isTrack(uri: URI | string): boolean;\n\t\tstatic isTrackset(uri: URI | string): boolean;\n\t\tstatic isUserToplist(uri: URI | string): boolean;\n\t\tstatic isUserTopTracks(uri: URI | string): boolean;\n\t\tstatic isUnknown(uri: URI | string): boolean;\n\t\tstatic isMedia(uri: URI | string): boolean;\n\t\tstatic isQuestion(uri: URI | string): boolean;\n\t\tstatic isPoll(uri: URI | string): boolean;\n\t\tstatic isPlaylistV1OrV2(uri: URI | string): boolean;\n\t}\n\n\t/**\n\t * Create custom menu item and prepend to right click context menu\n\t */\n\tnamespace ContextMenu {\n\t\ttype OnClickCallback = (uris: string[], uids?: string[], contextUri?: string) => void;\n\t\ttype ShouldAddCallback = (uris: string[], uids?: string[], contextUri?: string) => boolean;\n\n\t\t// Single context menu item\n\t\tclass Item {\n\t\t\t/**\n\t\t\t * List of valid icons to use.\n\t\t\t */\n\t\t\tstatic readonly iconList: Icon[];\n\t\t\tconstructor(name: string, onClick: OnClickCallback, shouldAdd?: ShouldAddCallback, icon?: Icon, disabled?: boolean);\n\t\t\tname: string;\n\t\t\ticon: Icon | string;\n\t\t\tdisabled: boolean;\n\t\t\t/**\n\t\t\t * A function returning boolean determines whether item should be prepended.\n\t\t\t */\n\t\t\tshouldAdd: ShouldAddCallback;\n\t\t\t/**\n\t\t\t * A function to call when item is clicked\n\t\t\t */\n\t\t\tonClick: OnClickCallback;\n\t\t\t/**\n\t\t\t * Item is only available in Context Menu when method \"register\" is called.\n\t\t\t */\n\t\t\tregister: () => void;\n\t\t\t/**\n\t\t\t * Stop Item to be prepended into Context Menu.\n\t\t\t */\n\t\t\tderegister: () => void;\n\t\t}\n\n\t\t/**\n\t\t * Create a sub menu to contain `Item`s.\n\t\t * `Item`s in `subItems` array shouldn't be registered.\n\t\t */\n\t\tclass SubMenu {\n\t\t\tconstructor(name: string, subItems: Iterable<Item>, shouldAdd?: ShouldAddCallback, disabled?: boolean);\n\t\t\tname: string;\n\t\t\tdisabled: boolean;\n\t\t\t/**\n\t\t\t * A function returning boolean determines whether item should be prepended.\n\t\t\t */\n\t\t\tshouldAdd: ShouldAddCallback;\n\t\t\taddItem: (item: Item) => void;\n\t\t\tremoveItem: (item: Item) => void;\n\t\t\t/**\n\t\t\t * SubMenu is only available in Context Menu when method \"register\" is called.\n\t\t\t */\n\t\t\tregister: () => void;\n\t\t\t/**\n\t\t\t * Stop SubMenu to be prepended into Context Menu.\n\t\t\t */\n\t\t\tderegister: () => void;\n\t\t}\n\t}\n\n\t/**\n\t * Popup Modal\n\t */\n\tnamespace PopupModal {\n\t\tinterface Content {\n\t\t\ttitle: string;\n\t\t\t/**\n\t\t\t * You can specify a string for simple text display\n\t\t\t * or a HTML element for interactive config/setting menu,\n\t\t\t * or a React JSX element for React-based components\n\t\t\t */\n\t\t\tcontent: string | Element | React.JSX.Element;\n\t\t\t/**\n\t\t\t * Bigger window\n\t\t\t */\n\t\t\tisLarge?: boolean;\n\t\t}\n\n\t\tfunction display(e: Content): void;\n\t\tfunction hide(): void;\n\t}\n\n\t/** React instance to create components */\n\tconst React: any;\n\t/** React DOM instance to render and mount components */\n\tconst ReactDOM: any;\n\t/** React DOM Server instance to render components to string */\n\tconst ReactDOMServer: any;\n\t/** React JSX runtime instance to transform JSX elements */\n\tconst ReactJSX: any;\n\n\t/** Stock React components exposed from Spotify library */\n\tnamespace ReactComponent {\n\t\ttype ContextMenuProps = {\n\t\t\t/**\n\t\t\t * Decide whether to use the global singleton context menu (rendered in <body>)\n\t\t\t * or a new inline context menu (rendered in a sibling\n\t\t\t * element to `children`)\n\t\t\t */\n\t\t\trenderInline?: boolean;\n\t\t\t/**\n\t\t\t * Determins what will trigger the context menu. For example, a click, or a right-click\n\t\t\t */\n\t\t\ttrigger?: \"click\" | \"right-click\";\n\t\t\t/**\n\t\t\t * Determins is the context menu should open or toggle when triggered\n\t\t\t */\n\t\t\taction?: \"toggle\" | \"open\";\n\t\t\t/**\n\t\t\t * The preferred placement of the context menu when it opens.\n\t\t\t * Relative to trigger element.\n\t\t\t */\n\t\t\tplacement?:\n\t\t\t\t| \"top\"\n\t\t\t\t| \"top-start\"\n\t\t\t\t| \"top-end\"\n\t\t\t\t| \"right\"\n\t\t\t\t| \"right-start\"\n\t\t\t\t| \"right-end\"\n\t\t\t\t| \"bottom\"\n\t\t\t\t| \"bottom-start\"\n\t\t\t\t| \"bottom-end\"\n\t\t\t\t| \"left\"\n\t\t\t\t| \"left-start\"\n\t\t\t\t| \"left-end\";\n\t\t\t/**\n\t\t\t * The x and y offset distances at which the context menu should open.\n\t\t\t * Relative to trigger element and `position`.\n\t\t\t */\n\t\t\toffset?: [number, number];\n\t\t\t/**\n\t\t\t * Will stop the client from scrolling while the context menu is open\n\t\t\t */\n\t\t\tpreventScrollingWhileOpen?: boolean;\n\t\t\t/**\n\t\t\t * The menu UI to render inside of the context menu.\n\t\t\t */\n\t\t\tmenu:\n\t\t\t\t| typeof Spicetify.ReactComponent.Menu\n\t\t\t\t| typeof Spicetify.ReactComponent.AlbumMenu\n\t\t\t\t| typeof Spicetify.ReactComponent.PodcastShowMenu\n\t\t\t\t| typeof Spicetify.ReactComponent.ArtistMenu\n\t\t\t\t| typeof Spicetify.ReactComponent.PlaylistMenu;\n\t\t\t/**\n\t\t\t * A child of the context menu. Should be `<button>`, `<a>`,\n\t\t\t * a custom react component that forwards a ref to a `<button>` or `<a>`,\n\t\t\t * or a function. If a function is passed it will be called with\n\t\t\t * (`isOpen`, `handleContextMenu`, `ref`) as arguments.\n\t\t\t */\n\t\t\tchildren: Element | ((isOpen?: boolean, handleContextMenu?: (e: MouseEvent) => void, ref?: (e: Element) => void) => Element);\n\t\t};\n\t\ttype MenuProps = {\n\t\t\t/**\n\t\t\t * Function that is called when the menu is closed\n\t\t\t */\n\t\t\tonClose?: () => void;\n\t\t\t/**\n\t\t\t * Function that provides the element that focus should jump to when the menu\n\t\t\t * is opened\n\t\t\t */\n\t\t\tgetInitialFocusElement?: (el: HTMLElement | null) => HTMLElement | undefined | null;\n\t\t};\n\t\ttype MenuItemProps = {\n\t\t\t/**\n\t\t\t * Function that runs when `MenuItem` is clicked\n\t\t\t */\n\t\t\tonClick?: React.MouseEventHandler<HTMLButtonElement>;\n\t\t\t/**\n\t\t\t * Indicates if `MenuItem` is disabled. Disabled items will not cause\n\t\t\t * the `Menu` to close when clicked.\n\t\t\t */\n\t\t\tdisabled?: boolean;\n\t\t\t/**\n\t\t\t * Indicate that a divider line should be added `before` or `after` this `MenuItem`\n\t\t\t */\n\t\t\tdivider?: \"before\" | \"after\" | \"both\";\n\t\t\t/**\n\t\t\t * React component icon that will be rendered at the end of the `MenuItem`\n\t\t\t * @deprecated Since Spotify `1.2.8`. Use `leadingIcon` or `trailingIcon` instead\n\t\t\t */\n\t\t\ticon?: React.ReactNode;\n\t\t\t/**\n\t\t\t * React component icon that will be rendered at the start of the `MenuItem`\n\t\t\t * @since Spotify `1.2.8`\n\t\t\t */\n\t\t\tleadingIcon?: React.ReactNode;\n\t\t\t/**\n\t\t\t * React component icon that will be rendered at the end of the `MenuItem`\n\t\t\t * @since Spotify `1.2.8`\n\t\t\t */\n\t\t\ttrailingIcon?: React.ReactNode;\n\t\t};\n\t\ttype TooltipProps = {\n\t\t\t/**\n\t\t\t * Label to display in the tooltip\n\t\t\t */\n\t\t\tlabel: string | React.ReactNode;\n\t\t\t/**\n\t\t\t * The child element that the tooltip will be attached to\n\t\t\t * and will display when hovered over\n\t\t\t */\n\t\t\tchildren: React.ReactNode;\n\t\t\t/**\n\t\t\t * Decide whether to use the global singleton tooltip (rendered in `<body>`)\n\t\t\t * or a new inline tooltip (rendered in a sibling\n\t\t\t * element to `children`)\n\t\t\t */\n\t\t\trenderInline?: boolean;\n\t\t\t/**\n\t\t\t * Delay in milliseconds before the tooltip is displayed\n\t\t\t * after the user hovers over the child element\n\t\t\t */\n\t\t\tshowDelay?: number;\n\t\t\t/**\n\t\t\t * Determine whether the tooltip should be displayed\n\t\t\t */\n\t\t\tdisabled?: boolean;\n\t\t\t/**\n\t\t\t * The preferred placement of the context menu when it opens.\n\t\t\t * Relative to trigger element.\n\t\t\t * @default 'top'\n\t\t\t */\n\t\t\tplacement?:\n\t\t\t\t| \"top\"\n\t\t\t\t| \"top-start\"\n\t\t\t\t| \"top-end\"\n\t\t\t\t| \"right\"\n\t\t\t\t| \"right-start\"\n\t\t\t\t| \"right-end\"\n\t\t\t\t| \"bottom\"\n\t\t\t\t| \"bottom-start\"\n\t\t\t\t| \"bottom-end\"\n\t\t\t\t| \"left\"\n\t\t\t\t| \"left-start\"\n\t\t\t\t| \"left-end\";\n\t\t\t/**\n\t\t\t * Class name to apply to the tooltip\n\t\t\t */\n\t\t\tlabelClassName?: string;\n\t\t};\n\t\ttype IconComponentProps = {\n\t\t\t/**\n\t\t\t * Icon size\n\t\t\t * @default 24\n\t\t\t */\n\t\t\ticonSize?: number;\n\t\t\t/**\n\t\t\t * Icon color\n\t\t\t * Might not be used by component\n\t\t\t * @default 'currentColor'\n\t\t\t */\n\t\t\tcolor?: string;\n\t\t\t/**\n\t\t\t * Semantic color name\n\t\t\t * Matches color variables used in xpui\n\t\t\t * @default Inherit from parent\n\t\t\t */\n\t\t\tsemanticColor?: SemanticColor;\n\t\t\t/**\n\t\t\t * Icon title\n\t\t\t * @default ''\n\t\t\t */\n\t\t\ttitle?: string;\n\t\t\t/**\n\t\t\t * Title ID (internal)\n\t\t\t */\n\t\t\ttitleId?: string;\n\t\t\t/**\n\t\t\t * Icon description\n\t\t\t */\n\t\t\tdesc?: string;\n\t\t\t/**\n\t\t\t * Description ID (internal)\n\t\t\t */\n\t\t\tdescId?: string;\n\t\t\t/**\n\t\t\t * Auto mirror icon\n\t\t\t * @default false\n\t\t\t */\n\t\t\tautoMirror?: boolean;\n\t\t};\n\t\ttype TextComponentProps = {\n\t\t\t/**\n\t\t\t * Text color\n\t\t\t * Might not be used by component\n\t\t\t * @default 'currentColor'\n\t\t\t */\n\t\t\tcolor?: string;\n\t\t\t/**\n\t\t\t * Semantic color name\n\t\t\t * Matches color variables used in xpui\n\t\t\t * @default Inherit from parent\n\t\t\t */\n\t\t\tsemanticColor?: SemanticColor;\n\t\t\t/**\n\t\t\t * Text style variant\n\t\t\t * @default 'viola'\n\t\t\t */\n\t\t\tvariant?: Variant;\n\t\t\t/**\n\t\t\t * Bottom padding size\n\t\t\t */\n\t\t\tpaddingBottom?: string;\n\t\t\t/**\n\t\t\t * Font weight\n\t\t\t */\n\t\t\tweight?: \"book\" | \"bold\" | \"black\";\n\t\t};\n\t\ttype ConfirmDialogProps = {\n\t\t\t/**\n\t\t\t * Boolean to determine if the dialog should be opened\n\t\t\t * @default true\n\t\t\t */\n\t\t\tisOpen?: boolean;\n\t\t\t/**\n\t\t\t * Whether to allow inline HTML in component text\n\t\t\t * @default false\n\t\t\t */\n\t\t\tallowHTML?: boolean;\n\t\t\t/**\n\t\t\t * Dialog title. Can be inline HTML if `allowHTML` is true\n\t\t\t */\n\t\t\ttitleText: string;\n\t\t\t/**\n\t\t\t * Dialog description. Can be inline HTML if `allowHTML` is true\n\t\t\t */\n\t\t\tdescriptionText?: string;\n\t\t\t/**\n\t\t\t * Confirm button text\n\t\t\t */\n\t\t\tconfirmText?: string;\n\t\t\t/**\n\t\t\t * Cancel button text\n\t\t\t */\n\t\t\tcancelText?: string;\n\t\t\t/**\n\t\t\t * Confirm button aria-label\n\t\t\t */\n\t\t\tconfirmLabel?: string;\n\t\t\t/**\n\t\t\t * Function to run when confirm button is clicked\n\t\t\t * The dialog does not close automatically, a handler must be included.\n\t\t\t * @param {React.MouseEvent<HTMLButtonElement>} event\n\t\t\t */\n\t\t\tonConfirm?: (event: React.MouseEvent<HTMLButtonElement>) => void;\n\t\t\t/**\n\t\t\t * Function to run when cancel button is clicked.\n\t\t\t * The dialog does not close automatically, a handler must be included.\n\t\t\t * @param {React.MouseEvent<HTMLButtonElement>} event\n\t\t\t */\n\t\t\tonClose?: (event: React.MouseEvent<HTMLButtonElement>) => void;\n\t\t\t/**\n\t\t\t * Function to run when dialog is clicked outside of.\n\t\t\t * By default, this will run `onClose`.\n\t\t\t * A handler must be included to close the dialog.\n\t\t\t * @param {React.MouseEvent<HTMLButtonElement>} event\n\t\t\t */\n\t\t\tonOutside?: (event: React.MouseEvent<HTMLButtonElement>) => void;\n\t\t};\n\t\ttype SliderProps = {\n\t\t\t/**\n\t\t\t * Label for the slider.\n\t\t\t */\n\t\t\tlabelText?: string;\n\t\t\t/**\n\t\t\t * The current value of the slider.\n\t\t\t */\n\t\t\tvalue: number;\n\t\t\t/**\n\t\t\t * The minimum value of the slider.\n\t\t\t */\n\t\t\tmin: number;\n\t\t\t/**\n\t\t\t * The maximum value of the slider.\n\t\t\t */\n\t\t\tmax: number;\n\t\t\t/**\n\t\t\t * The step value of the slider.\n\t\t\t */\n\t\t\tstep: number;\n\t\t\t/**\n\t\t\t * Whether or not the slider is disabled/can be interacted with.\n\t\t\t * @default true\n\t\t\t */\n\t\t\tisInteractive?: boolean;\n\t\t\t/**\n\t\t\t * Whether or not the active style of the slider should be shown.\n\t\t\t * This is equivalent to the slider being focused/hovered.\n\t\t\t * @default false\n\t\t\t */\n\t\t\tforceActiveStyles?: boolean;\n\t\t\t/**\n\t\t\t * Callback function that is called when the slider starts being dragged.\n\t\t\t *\n\t\t\t * @param {number} value The current value of the slider in percent.\n\t\t\t */\n\t\t\tonDragStart: (value: number) => void;\n\t\t\t/**\n\t\t\t * Callback function that is called when the slider is being dragged.\n\t\t\t *\n\t\t\t * @param {number} value The current value of the slider in percent.\n\t\t\t */\n\t\t\tonDragMove: (value: number) => void;\n\t\t\t/**\n\t\t\t * Callback function that is called when the slider stops being dragged.\n\t\t\t *\n\t\t\t * @param {number} value The current value of the slider in percent.\n\t\t\t */\n\t\t\tonDragEnd: (value: number) => void;\n\t\t\t/**\n\t\t\t * Callback function that is called when the slider incremented a step.\n\t\t\t *\n\t\t\t * @deprecated Use `onDrag` props instead.\n\t\t\t */\n\t\t\tonStepForward?: () => void;\n\t\t\t/**\n\t\t\t * Callback function that is called when the slider decremented a step.\n\t\t\t *\n\t\t\t * @deprecated Use `onDrag` props instead.\n\t\t\t */\n\t\t\tonStepBackward?: () => void;\n\t\t};\n\t\ttype ButtonProps = {\n\t\t\tcomponent: any;\n\t\t\t/**\n\t\t\t * Color set for the button.\n\t\t\t * @default \"brightAccent\"\n\t\t\t */\n\t\t\tcolorSet?: ColorSet;\n\t\t\t/**\n\t\t\t * Size for the button.\n\t\t\t * @default \"md\"\n\t\t\t */\n\t\t\tbuttonSize?: \"sm\" | \"md\" | \"lg\";\n\t\t\t/**\n\t\t\t * Size for the button.\n\t\t\t * @deprecated Use `buttonSize` prop instead, as it will take precedence.\n\t\t\t * @default \"medium\"\n\t\t\t */\n\t\t\tsize?: \"small\" | \"medium\" | \"large\";\n\t\t\t/**\n\t\t\t * Unused by Spotify. Usage unknown.\n\t\t\t */\n\t\t\tfullWidth?: any;\n\t\t\t/**\n\t\t\t * React component to render for an icon placed before children. Component, not element!\n\t\t\t */\n\t\t\ticonLeading?: (props: any) => any | string;\n\t\t\t/**\n\t\t\t * React component to render for an icon placed after children. Component, not element!\n\t\t\t */\n\t\t\ticonTrailing?: (props: any) => any | string;\n\t\t\t/**\n\t\t\t * React component to render for an icon used as button body. Component, not element!\n\t\t\t */\n\t\t\ticonOnly?: (props: any) => any | string;\n\t\t\t/**\n\t\t\t * Additional class name to apply to the button.\n\t\t\t */\n\t\t\tclassName?: string;\n\t\t\t/**\n\t\t\t * Label of the element for screen readers.\n\t\t\t */\n\t\t\t\"aria-label\"?: string;\n\t\t\t/**\n\t\t\t * ID of an element that describes the button for screen readers.\n\t\t\t */\n\t\t\t\"aria-labelledby\"?: string;\n\t\t\t/**\n\t\t\t * Unsafely set the color set for the button.\n\t\t\t * Values from the colorSet will be pasted into the CSS.\n\t\t\t */\n\t\t\tUNSAFE_colorSet?: ColorSetBody;\n\t\t\tonClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;\n\t\t\tonMouseEnter?: (event: React.MouseEvent<HTMLButtonElement>) => void;\n\t\t\tonMouseLeave?: (event: React.MouseEvent<HTMLButtonElement>) => void;\n\t\t\tonMouseDown?: (event: React.MouseEvent<HTMLButtonElement>) => void;\n\t\t\tonMouseUp?: (event: React.MouseEvent<HTMLButtonElement>) => void;\n\t\t\tonFocus?: (event: React.FocusEvent<HTMLButtonElement>) => void;\n\t\t\tonBlur?: (event: React.FocusEvent<HTMLButtonElement>) => void;\n\t\t};\n\t\t/**\n\t\t * Generic context menu provider\n\t\t *\n\t\t * Props:\n\t\t * @see Spicetify.ReactComponent.ContextMenuProps\n\t\t */\n\t\tconst ContextMenu: any;\n\t\t/**\n\t\t * Wrapper of ReactComponent.ContextMenu with props: action = 'toggle' and trigger = 'right-click'\n\t\t *\n\t\t * Props:\n\t\t * @see Spicetify.ReactComponent.ContextMenuProps\n\t\t */\n\t\tconst RightClickMenu: any;\n\t\t/**\n\t\t * Outer layer contain ReactComponent.MenuItem(s)\n\t\t *\n\t\t * Props:\n\t\t * @see Spicetify.ReactComponent.MenuProps\n\t\t */\n\t\tconst Menu: any;\n\t\t/**\n\t\t * Component to construct menu item\n\t\t * Used as ReactComponent.Menu children\n\t\t *\n\t\t * Props:\n\t\t * @see Spicetify.ReactComponent.MenuItemProps\n\t\t */\n\t\tconst MenuItem: any;\n\t\t/**\n\t\t * Tailored ReactComponent.Menu for specific type of object\n\t\t *\n\t\t * Props: {\n\t\t *      uri: string;\n\t\t *      onRemoveCallback?: (uri: string) => void;\n\t\t * }\n\t\t */\n\t\tconst AlbumMenu: any;\n\t\tconst PodcastShowMenu: any;\n\t\tconst ArtistMenu: any;\n\t\tconst PlaylistMenu: any;\n\t\tconst TrackMenu: any;\n\t\t/**\n\t\t * Component to display tooltip when hovering over element\n\t\t * Useful for accessibility\n\t\t *\n\t\t * Props:\n\t\t * @see Spicetify.ReactComponent.TooltipProps\n\t\t */\n\t\tconst TooltipWrapper: any;\n\t\t/**\n\t\t * Component to render Spotify-style icon\n\t\t * @since Spotify `1.1.95`\n\t\t *\n\t\t * Props:\n\t\t * @see Spicetify.ReactComponent.IconComponentProps\n\t\t */\n\t\tconst IconComponent: any;\n\t\t/**\n\t\t * Component to render Spotify-style text\n\t\t * @since Spotify `1.1.95`\n\t\t *\n\t\t * Props:\n\t\t * @see Spicetify.ReactComponent.TextComponentProps\n\t\t */\n\t\tconst TextComponent: any;\n\t\t/**\n\t\t * Component to render Spotify-style confirm dialog\n\t\t *\n\t\t * Props:\n\t\t * @see Spicetify.ReactComponent.ConfirmDialogProps\n\t\t */\n\t\tconst ConfirmDialog: any;\n\t\t/**\n\t\t * Component to render Spotify slider\n\t\t *\n\t\t * Used in progress bar, volume slider, crossfade settings, etc.\n\t\t *\n\t\t * Props:\n\t\t * @see Spicetify.ReactComponent.SliderProps\n\t\t */\n\t\tconst Slider: any;\n\t\t/**\n\t\t * Component to render Spotify primary button\n\t\t *\n\t\t * Props:\n\t\t * @see Spicetify.ReactComponent.ButtonProps\n\t\t */\n\t\tconst ButtonPrimary: any;\n\t\t/**\n\t\t * Component to render Spotify secondary button\n\t\t *\n\t\t * Props:\n\t\t * @see Spicetify.ReactComponent.ButtonProps\n\t\t */\n\t\tconst ButtonSecondary: any;\n\t\t/**\n\t\t * Component to render Spotify tertiary button\n\t\t *\n\t\t * Props:\n\t\t * @see Spicetify.ReactComponent.ButtonProps\n\t\t */\n\t\tconst ButtonTertiary: any;\n\t}\n\n\t/**\n\t * Add button in top bar next to navigation buttons\n\t */\n\tnamespace Topbar {\n\t\tclass Button {\n\t\t\tconstructor(label: string, icon: Icon | string, onClick: (self: Button) => void, disabled?: boolean, isRight?: boolean);\n\t\t\tlabel: string;\n\t\t\ticon: string;\n\t\t\tonClick: (self: Button) => void;\n\t\t\tdisabled: boolean;\n\t\t\tisRight: boolean;\n\t\t\telement: HTMLButtonElement;\n\t\t\ttippy: any;\n\t\t}\n\t}\n\n\t/**\n\t * Add button in player controls\n\t */\n\tnamespace Playbar {\n\t\t/**\n\t\t * Create a button on the right side of the playbar\n\t\t */\n\t\tclass Button {\n\t\t\tconstructor(\n\t\t\t\tlabel: string,\n\t\t\t\ticon: Icon | string,\n\t\t\t\tonClick?: (self: Button) => void,\n\t\t\t\tdisabled?: boolean,\n\t\t\t\tactive?: boolean,\n\t\t\t\tregisterOnCreate?: boolean\n\t\t\t);\n\t\t\tlabel: string;\n\t\t\ticon: string;\n\t\t\tonClick: (self: Button) => void;\n\t\t\tdisabled: boolean;\n\t\t\tactive: boolean;\n\t\t\telement: HTMLButtonElement;\n\t\t\ttippy: any;\n\t\t\tregister: () => void;\n\t\t\tderegister: () => void;\n\t\t}\n\n\t\t/**\n\t\t * Create a widget next to track info\n\t\t */\n\t\tclass Widget {\n\t\t\tconstructor(\n\t\t\t\tlabel: string,\n\t\t\t\ticon: Icon | string,\n\t\t\t\tonClick?: (self: Widget) => void,\n\t\t\t\tdisabled?: boolean,\n\t\t\t\tactive?: boolean,\n\t\t\t\tregisterOnCreate?: boolean\n\t\t\t);\n\t\t\tlabel: string;\n\t\t\ticon: string;\n\t\t\tonClick: (self: Widget) => void;\n\t\t\tdisabled: boolean;\n\t\t\tactive: boolean;\n\t\t\telement: HTMLButtonElement;\n\t\t\ttippy: any;\n\t\t\tregister: () => void;\n\t\t\tderegister: () => void;\n\t\t}\n\t}\n\n\t/**\n\t * SVG icons\n\t */\n\tconst SVGIcons: Record<Icon, string>;\n\n\t/**\n\t * A filtered copy of user's `config-xpui` file.\n\t */\n\tnamespace Config {\n\t\tconst version: string;\n\t\tconst current_theme: string;\n\t\tconst color_scheme: string;\n\t\tconst extensions: string[];\n\t\tconst custom_apps: string[];\n\t}\n\n\t/**\n\t * Tippy.js instance used by Spotify\n\t */\n\tconst Tippy: any;\n\t/**\n\t * Spicetify's predefined props for Tippy.js\n\t * Used to mimic Spotify's tooltip behavior\n\t */\n\tconst TippyProps: any;\n\n\t/**\n\t * Interface for interacting with Spotify client's app title\n\t */\n\tnamespace AppTitle {\n\t\t/**\n\t\t * Set default app title. This has no effect if the player is running.\n\t\t * Will override any previous forced title.\n\t\t * @param title Title to set\n\t\t * @return Promise that resolves to a function to cancel forced title. This doesn't reset the title.\n\t\t */\n\t\tfunction set(title: string): Promise<{ clear: () => void }>;\n\t\t/**\n\t\t * Reset app title to default\n\t\t */\n\t\tfunction reset(): Promise<void>;\n\t\t/**\n\t\t * Get current default app title\n\t\t * @return Current default app title\n\t\t */\n\t\tfunction get(): Promise<string>;\n\t\t/**\n\t\t * Subscribe to title changes.\n\t\t * This event is not fired when the player changes app title.\n\t\t * @param callback Callback to call when title changes\n\t\t * @return Object with method to unsubscribe\n\t\t */\n\t\tfunction sub(callback: (title: string) => void): { clear: () => void };\n\t}\n\n\t/**\n\t * Spicetify's QraphQL wrapper for Spotify's GraphQL API endpoints\n\t */\n\tnamespace GraphQL {\n\t\t/**\n\t\t * Possible types of entities.\n\t\t *\n\t\t * This list is dynamic and may change in the future.\n\t\t */\n\t\ttype Query =\n\t\t\t| \"decorateItemsForEnhance\"\n\t\t\t| \"imageURLAndSize\"\n\t\t\t| \"imageSources\"\n\t\t\t| \"audioItems\"\n\t\t\t| \"creator\"\n\t\t\t| \"extractedColors\"\n\t\t\t| \"extractedColorsAndImageSources\"\n\t\t\t| \"fetchExtractedColorAndImageForAlbumEntity\"\n\t\t\t| \"fetchExtractedColorAndImageForArtistEntity\"\n\t\t\t| \"fetchExtractedColorAndImageForEpisodeEntity\"\n\t\t\t| \"fetchExtractedColorAndImageForPlaylistEntity\"\n\t\t\t| \"fetchExtractedColorAndImageForPodcastEntity\"\n\t\t\t| \"fetchExtractedColorAndImageForTrackEntity\"\n\t\t\t| \"fetchExtractedColorForAlbumEntity\"\n\t\t\t| \"fetchExtractedColorForArtistEntity\"\n\t\t\t| \"fetchExtractedColorForEpisodeEntity\"\n\t\t\t| \"fetchExtractedColorForPlaylistEntity\"\n\t\t\t| \"fetchExtractedColorForPodcastEntity\"\n\t\t\t| \"fetchExtractedColorForTrackEntity\"\n\t\t\t| \"getAlbumNameAndTracks\"\n\t\t\t| \"getEpisodeName\"\n\t\t\t| \"getTrackName\"\n\t\t\t| \"queryAlbumTrackUris\"\n\t\t\t| \"queryTrackArtists\"\n\t\t\t| \"decorateContextEpisodesOrChapters\"\n\t\t\t| \"decorateContextTracks\"\n\t\t\t| \"fetchTracksForRadioStation\"\n\t\t\t| \"decoratePlaylists\"\n\t\t\t| \"playlistUser\"\n\t\t\t| \"FetchPlaylistMetadata\"\n\t\t\t| \"playlistContentsItemTrackArtist\"\n\t\t\t| \"playlistContentsItemTrackAlbum\"\n\t\t\t| \"playlistContentsItemTrack\"\n\t\t\t| \"playlistContentsItemLocalTrack\"\n\t\t\t| \"playlistContentsItemEpisodeShow\"\n\t\t\t| \"playlistContentsItemEpisode\"\n\t\t\t| \"playlistContentsItemResponse\"\n\t\t\t| \"playlistContentsItem\"\n\t\t\t| \"FetchPlaylistContents\"\n\t\t\t| \"episodeTrailerUri\"\n\t\t\t| \"podcastEpisode\"\n\t\t\t| \"podcastMetadataV2\"\n\t\t\t| \"minimalAudiobook\"\n\t\t\t| \"audiobookChapter\"\n\t\t\t| \"audiobookMetadataV2\"\n\t\t\t| \"fetchExtractedColors\"\n\t\t\t| \"queryFullscreenMode\"\n\t\t\t| \"queryNpvEpisode\"\n\t\t\t| \"queryNpvArtist\"\n\t\t\t| \"albumTrack\"\n\t\t\t| \"getAlbum\"\n\t\t\t| \"queryAlbumTracks\"\n\t\t\t| \"queryArtistOverview\"\n\t\t\t| \"queryArtistAppearsOn\"\n\t\t\t| \"discographyAlbum\"\n\t\t\t| \"albumMetadataReleases\"\n\t\t\t| \"albumMetadata\"\n\t\t\t| \"queryArtistDiscographyAlbums\"\n\t\t\t| \"queryArtistDiscographySingles\"\n\t\t\t| \"queryArtistDiscographyCompilations\"\n\t\t\t| \"queryArtistDiscographyAll\"\n\t\t\t| \"queryArtistDiscographyOverview\"\n\t\t\t| \"artistPlaylist\"\n\t\t\t| \"queryArtistPlaylists\"\n\t\t\t| \"queryArtistDiscoveredOn\"\n\t\t\t| \"queryArtistFeaturing\"\n\t\t\t| \"queryArtistRelated\"\n\t\t\t| \"queryArtistMinimal\"\n\t\t\t| \"searchModalResults\"\n\t\t\t| \"queryWhatsNewFeed\"\n\t\t\t| \"whatsNewFeedNewItems\"\n\t\t\t| \"SetItemsStateInWhatsNewFeed\"\n\t\t\t| \"browseImageURLAndSize\"\n\t\t\t| \"browseImageSources\"\n\t\t\t| \"browseAlbum\"\n\t\t\t| \"browseArtist\"\n\t\t\t| \"browseEpisode\"\n\t\t\t| \"browseChapter\"\n\t\t\t| \"browsePlaylist\"\n\t\t\t| \"browsePodcast\"\n\t\t\t| \"browseAudiobook\"\n\t\t\t| \"browseTrack\"\n\t\t\t| \"browseUser\"\n\t\t\t| \"browseMerch\"\n\t\t\t| \"browseArtistConcerts\"\n\t\t\t| \"browseContent\"\n\t\t\t| \"browseSectionContainer\"\n\t\t\t| \"browseClientFeature\"\n\t\t\t| \"browseItem\"\n\t\t\t| \"browseAll\"\n\t\t\t| \"browsePage\";\n\t\t/**\n\t\t * Collection of GraphQL definitions.\n\t\t */\n\t\tconst Definitions: Record<Query | string, any>;\n\t\t/**\n\t\t * Sends a GraphQL query to Spotify.\n\t\t * @description A preinitialized version of `Spicetify.GraphQL.Handler` using current context.\n\t\t * @param query Query to send\n\t\t * @param variables Variables to use\n\t\t * @param context Context to use\n\t\t * @return Promise that resolves to the response\n\t\t */\n\t\tfunction Request(query: (typeof Definitions)[Query | string], variables?: Record<string, any>, context?: Record<string, any>): Promise<any>;\n\t\t/**\n\t\t * Context for GraphQL queries.\n\t\t * @description Used to set context for the handler and initialze it.\n\t\t */\n\t\tconst Context: Record<string, any>;\n\t\t/**\n\t\t * Handler for GraphQL queries.\n\t\t * @param context Context to use\n\t\t * @return Function to handle GraphQL queries\n\t\t */\n\t\tfunction Handler(\n\t\t\tcontext: Record<string, any>\n\t\t): (query: (typeof Definitions)[Query | string], variables?: Record<string, any>, context?: Record<string, any>) => Promise<any>;\n\t}\n\n\tnamespace ReactHook {\n\t\t/**\n\t\t * React Hook to create interactive drag-and-drop element\n\t\t * @description Used to create a draggable element that can be dropped into Spotify's components (e.g. Playlist, Folder, Sidebar, Queue)\n\t\t * @param uris List of URIs to be dragged\n\t\t * @param label Label to be displayed when dragging\n\t\t * @param contextUri Context URI of the element from which the drag originated (e.g. Playlist URI)\n\t\t * @param sectionIndex Index of the section in which the drag originated\n\t\t * @param dropOriginUri URI of the desired drop target. Leave empty to allow drop anywhere\n\t\t * @return Function to handle drag event. Should be passed to `onDragStart` prop of the element. All parameters passed onto the hook will be passed onto the handler unless declared otherwise.\n\t\t *\n\t\t */\n\t\tfunction DragHandler(\n\t\t\turis?: string[],\n\t\t\tlabel?: string,\n\t\t\tcontextUri?: string,\n\t\t\tsectionIndex?: number,\n\t\t\tdropOriginUri?: string\n\t\t): (event: React.DragEvent, uris?: string[], label?: string, contextUri?: string, sectionIndex?: number) => void;\n\n\t\t/**\n\t\t * React Hook to use extracted color from GraphQL\n\t\t *\n\t\t * @note This is a wrapper of ReactQuery's `useQuery` hook.\n\t\t * The component using this hook must be wrapped in a `QueryClientProvider` component.\n\t\t *\n\t\t * @see https://tanstack.com/query/v3/docs/react/reference/QueryClientProvider\n\t\t *\n\t\t * @param uri URI of the Spotify image to extract color from.\n\t\t * @param fallbackColor Fallback color to use if the image is not available. Defaults to `#535353`.\n\t\t * @param variant Variant of the color to use. Defaults to `colorRaw`.\n\t\t *\n\t\t * @return Extracted color hex code.\n\t\t */\n\t\tfunction useExtractedColor(uri: string, fallbackColor?: string, variant?: \"colorRaw\" | \"colorLight\" | \"colorDark\"): string;\n\t}\n\n\t/**\n\t * react-flip-toolkit\n\t * @description A lightweight magic-move library for configurable layout transitions.\n\t * @link https://github.com/aholachek/react-flip-toolkit\n\t */\n\tnamespace ReactFlipToolkit {\n\t\tconst Flipper: any;\n\t\tconst Flipped: any;\n\t\tconst spring: any;\n\t}\n\n\t/**\n\t * classnames\n\t * @description A simple JavaScript utility for conditionally joining classNames together.\n\t * @link https://github.com/JedWatson/classnames\n\t */\n\tfunction classnames(...args: any[]): string;\n\n\t/**\n\t * React Query v3\n\t * @description A hook for fetching, caching and updating asynchronous data in React.\n\t * @link https://github.com/TanStack/query/tree/v3\n\t */\n\tconst ReactQuery: any;\n\n\t/**\n\t * Analyse and extract color presets from an image. Works for any valid image URL/URI.\n\t * @param image Spotify URI to an image, or an image URL.\n\t */\n\tfunction extractColorPreset(image: string | string[]): Promise<\n\t\t{\n\t\t\tcolorRaw: Color;\n\t\t\tcolorLight: Color;\n\t\t\tcolorDark: Color;\n\t\t\tisFallback: boolean;\n\t\t}[]\n\t>;\n\n\tinterface hsl {\n\t\th: number;\n\t\ts: number;\n\t\tl: number;\n\t}\n\tinterface hsv {\n\t\th: number;\n\t\ts: number;\n\t\tv: number;\n\t}\n\tinterface rgb {\n\t\tr: number;\n\t\tg: number;\n\t\tb: number;\n\t}\n\ttype CSSColors = \"HEX\" | \"HEXA\" | \"HSL\" | \"HSLA\" | \"RGB\" | \"RGBA\";\n\t/**\n\t * Spotify's internal color class\n\t */\n\tclass Color {\n\t\tconstructor(rgb: rgb, hsl: hsl, hsv: hsv, alpha?: number);\n\n\t\tstatic BLACK: Color;\n\t\tstatic WHITE: Color;\n\t\tstatic CSSFormat: Record<CSSColors, number> & Record<number, CSSColors>;\n\n\t\ta: number;\n\t\thsl: hsl;\n\t\thsv: hsv;\n\t\trgb: rgb;\n\n\t\t/**\n\t\t * Convert CSS representation to Color\n\t\t * @param cssColor CSS representation of the color. Must not contain spaces.\n\t\t * @param alpha Alpha value of the color. Defaults to 1.\n\t\t * @return Color object\n\t\t * @throws {Error} If the CSS color is invalid or unsupported\n\t\t */\n\t\tstatic fromCSS(cssColor: string, alpha?: number): Color;\n\t\tstatic fromHSL(hsl: hsl, alpha?: number): Color;\n\t\tstatic fromHSV(hsv: hsv, alpha?: number): Color;\n\t\tstatic fromRGB(rgb: rgb, alpha?: number): Color;\n\t\tstatic fromHex(hex: string, alpha?: number): Color;\n\n\t\t/**\n\t\t * Change the contrast of the color against another so that\n\t\t * the contrast between them is at least `strength`.\n\t\t */\n\t\tcontrastAdjust(against: Color, strength?: number): Color;\n\n\t\t/**\n\t\t * Stringify JSON result\n\t\t */\n\t\tstringify(): string;\n\n\t\t/**\n\t\t * Convert to CSS representation\n\t\t * @param colorFormat CSS color format to convert to\n\t\t * @return CSS representation of the color\n\t\t */\n\t\ttoCSS(colorFormat: number): string;\n\n\t\t/**\n\t\t * Return RGBA representation of the color\n\t\t */\n\t\ttoString(): string;\n\t}\n\n\t/**\n\t * Spotify internal library for localization\n\t */\n\tnamespace Locale {\n\t\t/**\n\t\t * Relative time format\n\t\t */\n\t\tconst _relativeTimeFormat: Intl.RelativeTimeFormat | null;\n\t\t/**\n\t\t * Registered date time formats in the current session\n\t\t */\n\t\tconst _dateTimeFormats: Record<string, Intl.DateTimeFormat>;\n\t\t/**\n\t\t * Current locale of the client\n\t\t */\n\t\tconst _locale: string;\n\t\tconst _urlLocale: string;\n\t\t/**\n\t\t * Collection of supported locales\n\t\t */\n\t\tconst _supportedLocales: Record<string, string>;\n\t\t/**\n\t\t * Dictionary of localized strings\n\t\t */\n\t\tconst _dictionary: Record<string, string | { one: string; other: string }>;\n\n\t\t/**\n\t\t * Format date into locale string\n\t\t *\n\t\t * @param date Date to format\n\t\t * @param options Options to use\n\t\t * @return Localized string\n\t\t * @throws {RangeError} If the date is invalid\n\t\t */\n\t\tfunction formatDate(date: number | Date | undefined, options?: Intl.DateTimeFormatOptions): string;\n\t\t/**\n\t\t * Format time into relative locale string\n\t\t *\n\t\t * @param date Date to format\n\t\t * @param options Options to use\n\t\t * @return Localized string\n\t\t * @throws {RangeError} If the date is invalid\n\t\t */\n\t\tfunction formatRelativeTime(date: number | Date | undefined, options?: Intl.DateTimeFormatOptions): string;\n\t\t/**\n\t\t * Format number into locale string\n\t\t *\n\t\t * @param number Number to format\n\t\t * @param options Options to use\n\t\t * @return Localized string\n\t\t */\n\t\tfunction formatNumber(number: number, options?: Intl.NumberFormatOptions): string;\n\t\t/**\n\t\t * Format number into compact locale string\n\t\t *\n\t\t * @param number Number to format\n\t\t * @return Localized string\n\t\t */\n\t\tfunction formatNumberCompact(number: number): string;\n\t\t/**\n\t\t * Get localized string\n\t\t *\n\t\t * @param key Key of the string\n\t\t * @param children React children to pass the string into\n\t\t * @return Localized string or React Fragment of the children\n\t\t */\n\t\tfunction get(key: string, ...children: React.ReactNode[]): string | React.ReactNode;\n\t\t/**\n\t\t * Get date time format of the passed options.\n\t\t *\n\t\t * Function calls here will register to the `_dateTimeFormats` dictionary.\n\t\t *\n\t\t * @param options Options to use\n\t\t * @return Date time format\n\t\t */\n\t\tfunction getDateTimeFormat(options?: Intl.DateTimeFormatOptions): Intl.DateTimeFormat;\n\t\t/**\n\t\t * Get the current locale dictionary\n\t\t *\n\t\t * @return Current locale dictionary\n\t\t */\n\t\tfunction getDictionary(): Record<string, string | { one: string; other: string }>;\n\t\t/**\n\t\t * Get the current locale\n\t\t *\n\t\t * @return Current locale\n\t\t */\n\t\tfunction getLocale(): string;\n\t\t/**\n\t\t * Get the current locale code for Smartling\n\t\t *\n\t\t * @return Current locale code for Smartling\n\t\t */\n\t\tfunction getSmartlingLocale(): string;\n\t\t/**\n\t\t * Get the current locale code for URL\n\t\t *\n\t\t * @return Current locale code for URL\n\t\t */\n\t\tfunction getUrlLocale(): string;\n\t\t/**\n\t\t * Get the current relative time format\n\t\t *\n\t\t * @return Current relative time format\n\t\t */\n\t\tfunction getRelativeTimeFormat(): Intl.RelativeTimeFormat;\n\t\t/**\n\t\t * Get the separator for the current locale\n\t\t *\n\t\t * @return Separator for the current locale\n\t\t */\n\t\tfunction getSeparator(): string;\n\t\t/**\n\t\t * Set the current locale\n\t\t *\n\t\t * This will clear all previously set relative time formats and key-value pairs.\n\t\t *\n\t\t * @param locale Locale to set\n\t\t */\n\t\tfunction setLocale(locale: string): void;\n\t\t/**\n\t\t * Set the current locale code for URL\n\t\t *\n\t\t * @param locale Locale code for URL to set\n\t\t */\n\t\tfunction setUrlLocale(locale: string): void;\n\t\t/**\n\t\t * Set the dictionary for the current locale\n\t\t *\n\t\t * @param dictionary Dictionary to set\n\t\t */\n\t\tfunction setDictionary(dictionary: Record<string, string | { one: string; other: string }>): void;\n\t\t/**\n\t\t * Transform text into locale lowercase\n\t\t *\n\t\t * @param text Text to transform\n\t\t * @return Locale lowercase text\n\t\t */\n\t\tfunction toLocaleLowerCase(text: string): string;\n\t\t/**\n\t\t * Transform text into locale uppercase\n\t\t *\n\t\t * @param text Text to transform\n\t\t * @return Locale uppercase text\n\t\t */\n\t\tfunction toLocaleUpperCase(text: string): string;\n\t}\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/spicetify/cli\n\ngo 1.24.2\n\nrequire (\n\tgithub.com/go-ini/ini v1.67.0\n\tgithub.com/mattn/go-colorable v0.1.14\n\tgithub.com/pterm/pterm v0.12.82\n\tgolang.org/x/net v0.49.0\n\tgolang.org/x/sys v0.40.0\n)\n\nrequire (\n\tatomicgo.dev/cursor v0.2.0 // indirect\n\tatomicgo.dev/keyboard v0.2.9 // indirect\n\tatomicgo.dev/schedule v0.1.0 // indirect\n\tgithub.com/containerd/console v1.0.5 // indirect\n\tgithub.com/gookit/color v1.5.4 // indirect\n\tgithub.com/lithammer/fuzzysearch v1.1.8 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.16 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/stretchr/testify v1.9.0 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgolang.org/x/term v0.39.0 // indirect\n\tgolang.org/x/text v0.33.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg=\natomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ=\natomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw=\natomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU=\natomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8=\natomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=\natomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=\natomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=\ngithub.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=\ngithub.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=\ngithub.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=\ngithub.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k=\ngithub.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI=\ngithub.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c=\ngithub.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=\ngithub.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4=\ngithub.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY=\ngithub.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=\ngithub.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=\ngithub.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=\ngithub.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=\ngithub.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=\ngithub.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=\ngithub.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=\ngithub.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=\ngithub.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=\ngithub.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=\ngithub.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=\ngithub.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=\ngithub.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=\ngithub.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=\ngithub.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=\ngithub.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=\ngithub.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=\ngithub.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=\ngithub.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU=\ngithub.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=\ngithub.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=\ngithub.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=\ngithub.com/pterm/pterm v0.12.82 h1:+D9wYhCaeaK0FIQoZtqbNQuNpe2lB2tajKKsTd5paVQ=\ngithub.com/pterm/pterm v0.12.82/go.mod h1:TyuyrPjnxfwP+ccJdBTeWHtd/e0ybQHkOS/TakajZCw=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=\ngithub.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=\ngolang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=\ngolang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=\ngolang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=\ngolang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=\ngolang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "install.ps1",
    "content": "$ErrorActionPreference = 'Stop'\r\n[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\r\n\r\n#region Variables\r\n$spicetifyFolderPath = \"$env:LOCALAPPDATA\\spicetify\"\r\n$spicetifyOldFolderPath = \"$HOME\\spicetify-cli\"\r\n#endregion Variables\r\n\r\n#region Functions\r\nfunction Write-Success {\r\n  [CmdletBinding()]\r\n  param ()\r\n  process {\r\n    Write-Host -Object ' > OK' -ForegroundColor 'Green'\r\n  }\r\n}\r\n\r\nfunction Write-Unsuccess {\r\n  [CmdletBinding()]\r\n  param ()\r\n  process {\r\n    Write-Host -Object ' > ERROR' -ForegroundColor 'Red'\r\n  }\r\n}\r\n\r\nfunction Test-Admin {\r\n  [CmdletBinding()]\r\n  param ()\r\n  begin {\r\n    Write-Host -Object \"Checking if the script is not being run as administrator...\" -NoNewline\r\n  }\r\n  process {\r\n    $currentUser = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())\r\n    -not $currentUser.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)\r\n  }\r\n}\r\n\r\nfunction Test-PowerShellVersion {\r\n  [CmdletBinding()]\r\n  param ()\r\n  begin {\r\n    $PSMinVersion = [version]'5.1'\r\n  }\r\n  process {\r\n    Write-Host -Object 'Checking if your PowerShell version is compatible...' -NoNewline\r\n    $PSVersionTable.PSVersion -ge $PSMinVersion\r\n  }\r\n}\r\n\r\nfunction Move-OldSpicetifyFolder {\r\n  [CmdletBinding()]\r\n  param ()\r\n  process {\r\n    if (Test-Path -Path $spicetifyOldFolderPath) {\r\n      Write-Host -Object 'Moving the old spicetify folder...' -NoNewline\r\n      Copy-Item -Path \"$spicetifyOldFolderPath\\*\" -Destination $spicetifyFolderPath -Recurse -Force\r\n      Remove-Item -Path $spicetifyOldFolderPath -Recurse -Force\r\n      Write-Success\r\n    }\r\n  }\r\n}\r\n\r\nfunction Get-Spicetify {\r\n  [CmdletBinding()]\r\n  param ()\r\n  begin {\r\n    if ($env:PROCESSOR_ARCHITECTURE -eq 'AMD64') {\r\n      $architecture = 'x64'\r\n    }\r\n    elseif ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') {\r\n      $architecture = 'arm64'\r\n    }\r\n    else {\r\n      $architecture = 'x32'\r\n    }\r\n    if ($v) {\r\n      if ($v -match '^\\d+\\.\\d+\\.\\d+$') {\r\n        $targetVersion = $v\r\n      }\r\n      else {\r\n        Write-Warning -Message \"You have specified an invalid spicetify version: $v `nThe version must be in the following format: 1.2.3\"\r\n        Pause\r\n        exit\r\n      }\r\n    }\r\n    else {\r\n      Write-Host -Object 'Fetching the latest spicetify version...' -NoNewline\r\n      $latestRelease = Invoke-RestMethod -Uri 'https://api.github.com/repos/spicetify/cli/releases/latest'\r\n      $targetVersion = $latestRelease.tag_name -replace 'v', ''\r\n      Write-Success\r\n    }\r\n    $archivePath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), \"spicetify.zip\")\r\n  }\r\n  process {\r\n    Write-Host -Object \"Downloading spicetify v$targetVersion...\" -NoNewline\r\n    $Parameters = @{\r\n      Uri            = \"https://github.com/spicetify/cli/releases/download/v$targetVersion/spicetify-$targetVersion-windows-$architecture.zip\"\r\n      UseBasicParsin = $true\r\n      OutFile        = $archivePath\r\n    }\r\n    Invoke-WebRequest @Parameters\r\n    Write-Success\r\n  }\r\n  end {\r\n    $archivePath\r\n  }\r\n}\r\n\r\nfunction Add-SpicetifyToPath {\r\n  [CmdletBinding()]\r\n  param ()\r\n  begin {\r\n    Write-Host -Object 'Making spicetify available in the PATH...' -NoNewline\r\n    $user = [EnvironmentVariableTarget]::User\r\n    $path = [Environment]::GetEnvironmentVariable('PATH', $user)\r\n  }\r\n  process {\r\n    $path = $path -replace \"$([regex]::Escape($spicetifyOldFolderPath))\\\\*;*\", ''\r\n    if ($path -notlike \"*$spicetifyFolderPath*\") {\r\n      $path = \"$path;$spicetifyFolderPath\"\r\n    }\r\n  }\r\n  end {\r\n    [Environment]::SetEnvironmentVariable('PATH', $path, $user)\r\n    $env:PATH = $path\r\n    Write-Success\r\n  }\r\n}\r\n\r\nfunction Install-Spicetify {\r\n  [CmdletBinding()]\r\n  param ()\r\n  begin {\r\n    Write-Host -Object 'Installing spicetify...'\r\n  }\r\n  process {\r\n    $archivePath = Get-Spicetify\r\n    Write-Host -Object 'Extracting spicetify...' -NoNewline\r\n    Expand-Archive -Path $archivePath -DestinationPath $spicetifyFolderPath -Force\r\n    Write-Success\r\n    Add-SpicetifyToPath\r\n  }\r\n  end {\r\n    Remove-Item -Path $archivePath -Force -ErrorAction 'SilentlyContinue'\r\n    Write-Host -Object 'spicetify was successfully installed!' -ForegroundColor 'Green'\r\n  }\r\n}\r\n#endregion Functions\r\n\r\n#region Main\r\n#region Checks\r\nif (-not (Test-PowerShellVersion)) {\r\n  Write-Unsuccess\r\n  Write-Warning -Message 'PowerShell 5.1 or higher is required to run this script'\r\n  Write-Warning -Message \"You are running PowerShell $($PSVersionTable.PSVersion)\"\r\n  Write-Host -Object 'PowerShell 5.1 install guide:'\r\n  Write-Host -Object 'https://learn.microsoft.com/skypeforbusiness/set-up-your-computer-for-windows-powershell/download-and-install-windows-powershell-5-1'\r\n  Write-Host -Object 'PowerShell 7 install guide:'\r\n  Write-Host -Object 'https://learn.microsoft.com/powershell/scripting/install/installing-powershell-on-windows'\r\n  Pause\r\n  exit\r\n}\r\nelse {\r\n  Write-Success\r\n}\r\nif (-not (Test-Admin)) {\r\n  Write-Unsuccess\r\n  Write-Warning -Message \"The script was run as administrator. This can result in problems with the installation process or unexpected behavior. Do not continue if you do not know what you are doing.\"\r\n  $Host.UI.RawUI.Flushinputbuffer()\r\n  $choices = [System.Management.Automation.Host.ChoiceDescription[]] @(\r\n    (New-Object System.Management.Automation.Host.ChoiceDescription '&Yes', 'Abort installation.'),\r\n    (New-Object System.Management.Automation.Host.ChoiceDescription '&No', 'Resume installation.')\r\n  )\r\n  $choice = $Host.UI.PromptForChoice('', 'Do you want to abort the installation process?', $choices, 0)\r\n  if ($choice -eq 0) {\r\n    Write-Host -Object 'spicetify installation aborted' -ForegroundColor 'Yellow'\r\n    Pause\r\n    exit\r\n  }\r\n}\r\nelse {\r\n  Write-Success\r\n}\r\n#endregion Checks\r\n\r\n#region Spicetify\r\nMove-OldSpicetifyFolder\r\nInstall-Spicetify\r\nWrite-Host -Object \"`nRun\" -NoNewline\r\nWrite-Host -Object ' spicetify -h ' -NoNewline -ForegroundColor 'Cyan'\r\nWrite-Host -Object 'to get started'\r\n#endregion Spicetify\r\n\r\n#region Marketplace\r\n$Host.UI.RawUI.Flushinputbuffer()\r\n$choices = [System.Management.Automation.Host.ChoiceDescription[]] @(\r\n    (New-Object System.Management.Automation.Host.ChoiceDescription \"&Yes\", \"Install Spicetify Marketplace.\"),\r\n    (New-Object System.Management.Automation.Host.ChoiceDescription \"&No\", \"Do not install Spicetify Marketplace.\")\r\n)\r\n$choice = $Host.UI.PromptForChoice('', \"`nDo you also want to install Spicetify Marketplace? It will become available within the Spotify client, where you can easily install themes and extensions.\", $choices, 0)\r\nif ($choice -eq 1) {\r\n  Write-Host -Object 'spicetify Marketplace installation aborted' -ForegroundColor 'Yellow'\r\n}\r\nelse {\r\n  Write-Host -Object 'Starting the spicetify Marketplace installation script..'\r\n  $Parameters = @{\r\n    Uri             = 'https://raw.githubusercontent.com/spicetify/spicetify-marketplace/main/resources/install.ps1'\r\n    UseBasicParsing = $true\r\n  }\r\n  Invoke-WebRequest @Parameters | Invoke-Expression\r\n}\r\n#endregion Marketplace\r\n#endregion Main\r\n"
  },
  {
    "path": "install.sh",
    "content": "#!/usr/bin/env sh\n# Copyright 2022 khanhas.\n# Copyright 2023-present Spicetify contributors.\n# Edited from project Denoland install script (https://github.com/denoland/deno_install)\n\nset -e\n\nfor arg in \"$@\"; do\n    shift\n    case \"$arg\" in\n        \"--root\") override_root=1 ;;\n        *)\n        if echo \"$arg\" | grep -qv \"^-\"; then\n            tag=\"$arg\"\n        else\n            echo \"Invalid option $arg\" >&2\n            exit 1\n        fi\n    esac\ndone\n\nis_root() {\n    [ \"$(id -u)\" -ne 0 ]\n}\n\nif ! is_root && [ \"${override_root:-0}\" -eq 0 ]; then\n    echo \"The script was ran under sudo or as root. The script will now exit\"\n    echo \"If you hadn't intended to do this, please execute the script without root access to avoid problems with spicetify\"\n    echo \"To override this behavior, pass the '--root' parameter to this script\"\n    exit\nfi\n\n# wipe existing log\n> install.log :\n\nlog() {\n    echo \"$1\"\n    echo \"[$(date +'%H:%M:%S %Y-%m-%d')]\" \"$1\" >> install.log\n}\n\ncase $(uname -sm) in\n    \"Darwin x86_64\") target=\"darwin-amd64\" ;;\n    \"Darwin arm64\") target=\"darwin-arm64\" ;;\n    \"Linux x86_64\") target=\"linux-amd64\" ;;\n    \"Linux aarch64\") target=\"linux-arm64\" ;;\n    *) log \"Unsupported platform $(uname -sm). x86_64 and arm64 binaries for Linux and Darwin are available.\"; exit ;;\nesac\n\n# check for dependencies\ncommand -v curl >/dev/null || { log \"curl isn't installed!\" >&2; exit 1; }\ncommand -v tar >/dev/null || { log \"tar isn't installed!\" >&2; exit 1; }\ncommand -v grep >/dev/null || { log \"grep isn't installed!\" >&2; exit 1; }\n\n# download uri\nreleases_uri=https://github.com/spicetify/cli/releases\nif [ -z \"$tag\" ]; then\n    tag=$(curl -LsH 'Accept: application/json' $releases_uri/latest)\n    tag=${tag%\\,\\\"update_url*}\n    tag=${tag##*tag_name\\\":\\\"}\n    tag=${tag%\\\"}\nfi\n\ntag=${tag#v}\n\nlog \"FETCHING Version $tag\"\n\ndownload_uri=$releases_uri/download/v$tag/spicetify-$tag-$target.tar.gz\n\n# locations\nspicetify_install=\"$HOME/.spicetify\"\nexe=\"$spicetify_install/spicetify\"\ntar=\"$spicetify_install/spicetify.tar.gz\"\n\n# installing\n[ ! -d \"$spicetify_install\" ] && log \"CREATING $spicetify_install\" && mkdir -p \"$spicetify_install\"\n\nlog \"DOWNLOADING $download_uri\"\ncurl --fail --location --progress-bar --output \"$tar\" \"$download_uri\"\n\nlog \"EXTRACTING $tar\"\ntar xzf \"$tar\" -C \"$spicetify_install\"\n\nlog \"SETTING EXECUTABLE PERMISSIONS TO $exe\"\nchmod +x \"$exe\"\n\nlog \"REMOVING $tar\"\nrm \"$tar\"\n\nnotfound() {\n    cat << EOINFO\nManually add the directory to your \\$PATH through your shell profile\nexport SPICETIFY_INSTALL=\"$spicetify_install\"\nexport PATH=\"\\$PATH:$spicetify_install\"\nEOINFO\n}\n\nendswith_newline() {\n    [ \"$(od -An -c \"$1\" | tail -1 | grep -o '.$')\" = \"\\n\" ]\n}\n\ncheck() {\n    path=\"export PATH=\\$PATH:$spicetify_install\"\n    shellrc=$HOME/$1\n\n    if [ \"$1\" = \".zshrc\" ] && [ -n \"${ZDOTDIR}\" ]; then\n        shellrc=$ZDOTDIR/$1\n    fi\n\n    # Create shellrc if it doesn't exist\n    if ! [ -f \"$shellrc\" ]; then\n        log \"CREATING $shellrc\"\n        touch \"$shellrc\"\n    fi\n\n    # Still checking again, in case touch command failed\n    if [ -f \"$shellrc\" ]; then\n        if ! grep -q \"$spicetify_install\" \"$shellrc\"; then\n            log \"APPENDING $spicetify_install to PATH in $shellrc\"\n            if ! endswith_newline \"$shellrc\"; then\n                echo >> \"$shellrc\"\n            fi\n            echo \"${2:-$path}\" >> \"$shellrc\"\n            export PATH=\"$spicetify_install:$PATH\"\n        else\n            log \"spicetify path already set in $shellrc, continuing...\"\n        fi\n    else\n        notfound\n    fi\n}\n\ncase $SHELL in\n    *zsh) check \".zshrc\" ;;\n    *bash)\n        [ -f \"$HOME/.bashrc\" ] && check \".bashrc\"\n        [ -f \"$HOME/.bash_profile\" ] && check \".bash_profile\"\n    ;;\n    *fish) check \".config/fish/config.fish\" \"fish_add_path $spicetify_install\" ;;\n    *) notfound ;;\nesac\n\necho\nlog \"spicetify v$tag was installed successfully to $spicetify_install\"\nlog \"Run 'spicetify --help' to get started\"\n\necho \"Do you want to install spicetify Marketplace? (Y/n)\"\nread -r choice < /dev/tty\nif [ \"$choice\" = \"N\" ] || [ \"$choice\" = \"n\" ]; then\n    echo \"spicetify Marketplace installation aborted\"\n    exit 0\nfi\necho \"Starting the spicetify Marketplace installation script..\"\ncurl -fsSL \"https://raw.githubusercontent.com/spicetify/spicetify-marketplace/main/resources/install.sh\" | sh\n"
  },
  {
    "path": "jsHelper/expFeatures.js",
    "content": "(async () => {\n\tlet overrideList;\n\tlet prevSessionOverrideList = [];\n\tconst newFeatures = [];\n\tlet hooksPatched = false;\n\tconst featureMap = {};\n\tlet isFallback = false;\n\n\ttry {\n\t\toverrideList = JSON.parse(localStorage.getItem(\"spicetify-exp-features\"));\n\t\tif (!overrideList || overrideList !== Object(overrideList)) throw \"\";\n\t\tprevSessionOverrideList = Object.keys(overrideList);\n\t} catch {\n\t\toverrideList = {};\n\t\tprevSessionOverrideList = [];\n\t}\n\n\tSpicetify.expFeatureOverride = (feature) => {\n\t\thooksPatched = true;\n\t\tnewFeatures.push(feature.name);\n\n\t\tswitch (feature.type) {\n\t\t\tcase \"enum\":\n\t\t\t\tif (!overrideList[feature.name]) {\n\t\t\t\t\toverrideList[feature.name] = {\n\t\t\t\t\t\tdescription: feature.description,\n\t\t\t\t\t\tvalue: feature.default,\n\t\t\t\t\t\tvalues: feature.values,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tfeature.default = overrideList[feature.name].value;\n\t\t\t\tbreak;\n\t\t\tcase \"bool\":\n\t\t\t\tif (!overrideList[feature.name]) {\n\t\t\t\t\toverrideList[feature.name] = { description: feature.description, value: feature.default };\n\t\t\t\t}\n\t\t\t\tfeature.default = overrideList[feature.name].value;\n\t\t\t\tbreak;\n\t\t}\n\n\t\tlocalStorage.setItem(\"spicetify-exp-features\", JSON.stringify(overrideList));\n\t\treturn feature;\n\t};\n\n\tconst content = document.createElement(\"div\");\n\tcontent.classList.add(\"spicetify-exp-features\");\n\tconst style = document.createElement(\"style\");\n\tstyle.innerHTML = `\n.spicetify-exp-features .col {\n    padding: 0;\n}\n.spicetify-exp-features .setting-row::after {\n    content: \"\";\n    display: table;\n    clear: both;\n}\n.spicetify-exp-features .setting-row {\n    display: flex;\n    padding: 10px 0;\n    align-items: center;\n}\n.spicetify-exp-features .setting-row .col.description {\n    float: left;\n    padding-right: 15px;\n    width: 100%;\n}\n.spicetify-exp-features .setting-row .col.action {\n    float: right;\n    text-align: right;\n}\n.spicetify-exp-features .setting-row .col.action .dropdown {\n\twidth: max-content;\n}\n.spicetify-exp-features button.switch {\n    align-items: center;\n    border: 0px;\n    border-radius: 50%;\n    background-color: rgba(var(--spice-rgb-shadow), .7);\n    color: var(--spice-text);\n    cursor: pointer;\n    display: flex;\n    margin-inline-start: 12px;\n    padding: 8px;\n}\n.spicetify-exp-features button.switch.disabled,\n.spicetify-exp-features button.switch[disabled] {\n    color: rgba(var(--spice-rgb-text), .3);\n}\n.spicetify-exp-features button.reset {\n\tfont-weight: 700;\n\tfont-size: medium;\n\tbackground-color: transparent;\n\tborder-radius: 500px;\n\ttransition-duration: 33ms;\n\ttransition-property: background-color, border-color, color, box-shadow, filter, transform;\n\tpadding-inline: 15px;\n\tborder: 1px solid #727272;\n\tcolor: var(--spice-text);\n\tmin-block-size: 32px;\n\tcursor: pointer;\n}\n.spicetify-exp-features button.reset:hover {\n\ttransform: scale(1.04);\n\tborder-color: var(--spice-text);\n}\n.spicetify-exp-features .search-container {\n    width: 100%;\n}\n.spicetify-exp-features .setting-row#search .col.action {\n    position: relative;\n    width: 100%;\n}\n.spicetify-exp-features .setting-row#search svg {\n    position: absolute;\n    margin: 12px;\n}\n.spicetify-exp-features input.search {\n    border-style: solid;\n    border-color: var(--spice-sidebar);\n    background-color: var(--spice-sidebar);\n    border-radius: 8px;\n    padding: 10px 36px;\n    color: var(--spice-text);\n    width: 100%;\n}`;\n\tcontent.appendChild(style);\n\n\tconst notice = document.createElement(\"div\");\n\tnotice.classList.add(\"notice\");\n\tnotice.innerText = \"Waiting for Spotify to finish loading...\";\n\tcontent.appendChild(notice);\n\n\t(function waitForRemoteConfigResolver() {\n\t\t// Don't show options if hooks aren't patched/loaded\n\t\tif (!hooksPatched || (!Spicetify.RemoteConfigResolver && !Spicetify.Platform?.RemoteConfigDebugAPI && !Spicetify.Platform?.RemoteConfiguration)) {\n\t\t\tsetTimeout(waitForRemoteConfigResolver, 500);\n\t\t\treturn;\n\t\t}\n\n\t\tlet remoteConfiguration = Spicetify.RemoteConfigResolver?.value.remoteConfiguration || Spicetify.Platform?.RemoteConfiguration;\n\t\tconst setOverrides = async (overrides) => {\n\t\t\tif (Spicetify.Platform?.RemoteConfigDebugAPI) {\n\t\t\t\tfor (const [name, value] of Object.entries(overrides)) {\n\t\t\t\t\tconst feature = overrideList[name];\n\t\t\t\t\tconst type = feature.values ? \"enum\" : typeof value === \"number\" ? \"number\" : \"boolean\";\n\t\t\t\t\tawait Spicetify.Platform.RemoteConfigDebugAPI.setOverride(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tsource: \"web\",\n\t\t\t\t\t\t\ttype,\n\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tvalue\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} else if (Spicetify.RemoteConfigResolver?.value?.setOverrides) {\n\t\t\t\tSpicetify.RemoteConfigResolver.value.setOverrides(Spicetify.createInternalMap?.(overrides));\n\t\t\t}\n\t\t};\n\n\t\t(async function waitForResolver() {\n\t\t\tif (!Spicetify.RemoteConfigResolver && !Spicetify.Platform?.RemoteConfigDebugAPI) {\n\t\t\t\tisFallback = true;\n\t\t\t\tnotice.innerText = \"⚠️ Using fallback mode. Some features may not work.\";\n\t\t\t\tsetTimeout(waitForResolver, 500);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tisFallback = false;\n\t\t\tnotice.remove();\n\t\t\tremoteConfiguration =\n\t\t\t\tSpicetify?.RemoteConfigResolver?.value.remoteConfiguration ?? (await Spicetify.Platform?.RemoteConfigDebugAPI.getProperties());\n\t\t})();\n\n\t\tfor (const key of Object.keys(overrideList)) {\n\t\t\tif (newFeatures.includes(key)) continue;\n\t\t\tdelete overrideList[key];\n\t\t\tconsole.warn(`[spicetify-exp-features] Removed ${key} from override list`);\n\t\t\tlocalStorage.setItem(\"spicetify-exp-features\", JSON.stringify(overrideList));\n\t\t}\n\n\t\tfunction changeValue(name, value) {\n\t\t\toverrideList[name].value = value;\n\t\t\tlocalStorage.setItem(\"spicetify-exp-features\", JSON.stringify(overrideList));\n\n\t\t\tfeatureMap[name] = value;\n\t\t\tsetOverrides({ [name]: value });\n\t\t}\n\n\t\tfunction createSlider(name, desc, defaultVal) {\n\t\t\tconst container = document.createElement(\"div\");\n\t\t\tcontainer.classList.add(\"setting-row\");\n\t\t\tcontainer.id = name;\n\t\t\tcontainer.innerHTML = `\n<label class=\"col description\">${desc}</label>\n<div class=\"col action\"><button class=\"switch\">\n    <svg height=\"16\" width=\"16\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n        ${Spicetify.SVGIcons.check}\n    </svg>\n</button></div>`;\n\n\t\t\tconst slider = container.querySelector(\"button.switch\");\n\t\t\tslider.classList.toggle(\"disabled\", !defaultVal);\n\n\t\t\tslider.onclick = () => {\n\t\t\t\tconst state = slider.classList.contains(\"disabled\");\n\t\t\t\tslider.classList.toggle(\"disabled\");\n\t\t\t\tchangeValue(name, state);\n\t\t\t};\n\n\t\t\treturn container;\n\t\t}\n\n\t\tfunction createDropdown(name, desc, defaultVal, options) {\n\t\t\tconst container = document.createElement(\"div\");\n\t\t\tcontainer.classList.add(\"setting-row\");\n\t\t\tcontainer.id = name;\n\t\t\tcontainer.innerHTML = `\n<label class=\"col description\">${desc}</label>\n<div class=\"col action\">\n<select class=\"dropdown main-dropDown-dropDown\">\n    ${options.map((option) => `<option value=\"${option}\">${option}</option>`).join(\"\")}\n</select>\n</div>`;\n\t\t\tconst dropdown = container.querySelector(\"select\");\n\t\t\tdropdown.value = defaultVal;\n\n\t\t\tdropdown.onchange = () => {\n\t\t\t\tchangeValue(name, dropdown.value);\n\t\t\t};\n\n\t\t\treturn container;\n\t\t}\n\n\t\tconst searchBar = document.createElement(\"div\");\n\t\tsearchBar.classList.add(\"setting-row\");\n\t\tsearchBar.id = \"search\";\n\t\tsearchBar.innerHTML = `\n<div class=\"col action\">\n<div class=\"search-container\">\n<svg height=\"16\" width=\"16\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n${Spicetify.SVGIcons.search}\n</svg>\n<input type=\"text\" class=\"search\" placeholder=\"Search for a feature\">\n</div>\n</div>`;\n\t\tconst search = searchBar.querySelector(\"input.search\");\n\n\t\tsearch.oninput = () => {\n\t\t\tconst query = search.value.toLowerCase();\n\t\t\tconst rows = content.querySelectorAll(\".setting-row\");\n\t\t\tfor (const row of rows) {\n\t\t\t\tif (row.id === \"search\" || row.id === \"reset\") continue;\n\t\t\t\trow.style.display = row.textContent.trim().toLowerCase().includes(query) || row.id.toLowerCase().includes(query) ? \"flex\" : \"none\";\n\t\t\t}\n\t\t};\n\n\t\tconst resetButton = document.createElement(\"div\");\n\t\tresetButton.classList.add(\"setting-row\");\n\t\tresetButton.id = \"reset\";\n\t\tresetButton.innerHTML += `\n\t\t\t\t\t<label class=\"col description\">Clear all cached features and preferences</label>\n\t\t\t\t\t<div class=\"col action\">\n\t\t\t\t\t\t<button class=\"reset\">Reset</button>\n\t\t\t\t\t</div>`;\n\t\tconst resetBtn = resetButton.querySelector(\"button.reset\");\n\t\tresetBtn.onclick = () => {\n\t\t\tlocalStorage.removeItem(\"spicetify-exp-features\");\n\t\t\twindow.location.reload();\n\t\t};\n\n\t\tcontent.appendChild(searchBar);\n\n\t\tfor (const name of Object.keys(overrideList)) {\n\t\t\tif (!prevSessionOverrideList.includes(name) && remoteConfiguration.values.has(name)) {\n\t\t\t\tconst currentValue = remoteConfiguration.values.get(name);\n\t\t\t\toverrideList[name].value = currentValue;\n\t\t\t\tlocalStorage.setItem(\"spicetify-exp-features\", JSON.stringify(overrideList));\n\n\t\t\t\tfeatureMap[name] = currentValue;\n\t\t\t\tsetOverrides({ [name]: currentValue });\n\t\t\t}\n\n\t\t\tconst feature = overrideList[name];\n\t\t\tif (!overrideList[name]?.description) continue;\n\n\t\t\tif (overrideList[name].values) {\n\t\t\t\tcontent.appendChild(createDropdown(name, feature.description, feature.value, feature.values));\n\t\t\t} else content.appendChild(createSlider(name, feature.description, feature.value));\n\n\t\t\tfeatureMap[name] = feature.value;\n\t\t}\n\n\t\tcontent.appendChild(resetButton);\n\t})();\n\n\tawait new Promise((res) => Spicetify.Events.webpackLoaded.on(res));\n\n\tnew Spicetify.Menu.Item(\n\t\t\"Experimental features\",\n\t\tfalse,\n\t\t() => {\n\t\t\tSpicetify.PopupModal.display({\n\t\t\t\ttitle: \"Experimental features\",\n\t\t\t\tcontent,\n\t\t\t\tisLarge: true,\n\t\t\t});\n\t\t\tif (!isFallback) return;\n\n\t\t\tconst closeButton = document.querySelector(\"body > generic-modal button.main-trackCreditsModal-closeBtn\");\n\t\t\tconst modalOverlay = document.querySelector(\"body > generic-modal > div\");\n\n\t\t\tif (closeButton && modalOverlay) {\n\t\t\t\tcloseButton.onclick = () => location.reload();\n\t\t\t\tmodalOverlay.onclick = (e) => {\n\t\t\t\t\t// If clicked on overlay, also reload\n\t\t\t\t\tif (e.target === modalOverlay) {\n\t\t\t\t\t\tlocation.reload();\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t}\n\t\t},\n\t\t`<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.0\" viewBox=\"0 0 863 924\" width=\"16\" height=\"16\" fill=\"currentcolor\"><g transform=\"translate(0,924) scale(0.1,-0.1)\"><path d=\"M3725 9160 c-148 -4 -306 -10 -350 -14 -117 -11 -190 -49 -291 -150 -132 -133 -170 -234 -162 -431 7 -163 50 -255 185 -396 l64 -66 -10 -994 c-13 -1268 -15 -1302 -63 -1494 -87 -352 -263 -756 -511 -1172 -111 -186 -705 -1084 -1371 -2073 -537 -797 -585 -882 -607 -1090 -33 -317 39 -586 218 -810 114 -142 229 -235 386 -311 90 -43 116 -51 217 -65 209 -27 723 -33 2725 -33 2278 1 3098 9 3190 32 231 59 482 234 607 423 142 215 195 408 185 674 -9 241 -46 337 -240 634 -53 81 -97 156 -97 167 0 10 -6 19 -13 19 -19 0 -1264 1863 -1621 2424 -166 261 -361 668 -444 928 -42 129 -88 314 -107 428 -20 119 -34 783 -34 1683 l-1 629 80 91 c125 142 170 250 170 408 0 96 -16 162 -61 255 -74 152 -221 264 -371 284 -182 25 -1072 35 -1673 20z m1574 -388 c89 -20 141 -84 141 -172 0 -47 -5 -64 -30 -98 -16 -23 -38 -46 -50 -52 -45 -24 -311 -33 -985 -33 -764 0 -958 8 -1004 44 -42 33 -71 89 -71 138 0 56 34 127 69 145 30 16 151 35 256 40 159 7 1633 -3 1674 -12z m-116 -839 c11 -175 18 -570 27 -1378 9 -824 10 -825 70 -1066 81 -320 193 -597 398 -984 178 -337 326 -569 1065 -1663 186 -277 337 -505 335 -508 -3 -2 -1223 -3 -2712 -2 l-2707 3 82 120 c45 66 290 431 544 810 437 654 626 953 779 1233 229 416 404 893 445 1207 21 158 31 532 31 1175 0 360 3 766 7 902 l6 247 126 4 c69 1 435 4 812 5 l686 2 6 -107z\"/></g></svg>`\n\t).register();\n})();\n"
  },
  {
    "path": "jsHelper/homeConfig.js",
    "content": "SpicetifyHomeConfig = {};\n\n(async () => {\n\t// Status enum\n\tconst NORMAL = 0;\n\tconst STICKY = 1;\n\tconst LOWERED = 2;\n\t// List of sections' metadata\n\tlet list;\n\t// Store sections' statuses\n\tconst statusDic = {};\n\tlet mounted = false;\n\n\tSpicetifyHomeConfig.arrange = (sections) => {\n\t\tmounted = true;\n\t\tif (list) {\n\t\t\treturn list;\n\t\t}\n\t\tconst stickList = (localStorage.getItem(\"spicetify-home-config:stick\") || \"\").split(\",\");\n\t\tconst lowList = (localStorage.getItem(\"spicetify-home-config:low\") || \"\").split(\",\");\n\t\tconst stickSections = [];\n\t\tconst lowSections = [];\n\t\tfor (const uri of stickList) {\n\t\t\tconst index = sections.findIndex((a) => a?.uri === uri || a?.item?.uri === uri);\n\t\t\tif (index !== -1) {\n\t\t\t\tconst item = sections[index];\n\t\t\t\tconst uri = item.item.uri || item.uri;\n\t\t\t\tstatusDic[uri] = STICKY;\n\t\t\t\tstickSections.push(item);\n\t\t\t\tsections[index] = undefined;\n\t\t\t}\n\t\t}\n\t\tfor (const uri of lowList) {\n\t\t\tconst index = sections.findIndex((a) => a?.uri === uri || a?.item?.uri === uri);\n\t\t\tif (index !== -1) {\n\t\t\t\tconst item = sections[index];\n\t\t\t\tconst uri = item.item.uri || item.uri;\n\t\t\t\tstatusDic[uri] = LOWERED;\n\t\t\t\tlowSections.push(item);\n\t\t\t\tsections[index] = undefined;\n\t\t\t}\n\t\t}\n\n\t\tlist = [...stickSections, ...sections.filter(Boolean), ...lowSections];\n\t\treturn list;\n\t};\n\n\tconst up = document.createElement(\"button\");\n\tup.innerText = \"Up\";\n\tconst down = document.createElement(\"button\");\n\tdown.innerText = \"Down\";\n\tconst lower = document.createElement(\"button\");\n\tconst stick = document.createElement(\"button\");\n\tconst sectionStyle = document.createElement(\"style\");\n\tsectionStyle.innerHTML = `\n.main-home-content section {\n\torder: 0 !important;\n}\n`;\n\tconst containerStyle = document.createElement(\"style\");\n\tcontainerStyle.innerHTML = `\n#spicetify-home-config {\n    position: relative;\n    width: 100%;\n    height: 0;\n    display: flex;\n    justify-content: center;\n    align-items: flex-start;\n    gap: 5px;\n    z-index: 9999;\n}\n#spicetify-home-config button {\n    min-width: 60px;\n    height: 40px;\n    border-radius: 3px;\n    background-color: var(--spice-main);\n    color: var(--spice-text);\n    border: 1px solid var(--spice-text);\n}\n#spicetify-home-config button:disabled {\n    color: var(--spice-button-disabled);\n}\n`;\n\n\tconst container = document.createElement(\"div\");\n\tcontainer.id = \"spicetify-home-config\";\n\tcontainer.append(containerStyle, up, down, lower, stick);\n\tdocument.head.append(sectionStyle);\n\tlet elem = [];\n\n\tfunction injectInteraction() {\n\t\tconst main = document.querySelector(\".main-home-content\");\n\t\telem = [...main.querySelectorAll(\"section\")];\n\t\tfor (const [index, item] of elem.entries()) {\n\t\t\titem.dataset.uri = list[index]?.uri ?? list[index].item?.uri;\n\t\t}\n\n\t\tfunction appendItems() {\n\t\t\tconst stick = [];\n\t\t\tconst low = [];\n\t\t\tconst normal = [];\n\t\t\tfor (const el of elem) {\n\t\t\t\tif (statusDic[el.dataset.uri] === STICKY) stick.push(el);\n\t\t\t\telse if (statusDic[el.dataset.uri] === LOWERED) low.push(el);\n\t\t\t\telse normal.push(el);\n\t\t\t}\n\n\t\t\tlocalStorage.setItem(\n\t\t\t\t\"spicetify-home-config:stick\",\n\t\t\t\tstick.map((a) => a.dataset.uri)\n\t\t\t);\n\t\t\tlocalStorage.setItem(\n\t\t\t\t\"spicetify-home-config:low\",\n\t\t\t\tlow.map((a) => a.dataset.uri)\n\t\t\t);\n\n\t\t\telem = [...stick, ...normal, ...low];\n\t\t\tmain.append(...elem);\n\t\t}\n\n\t\tfunction onSwap(item, dir) {\n\t\t\tcontainer.remove();\n\t\t\tconst curPos = elem.findIndex((e) => e === item);\n\t\t\tconst newPos = curPos + dir;\n\t\t\tif (newPos < 0 || newPos > elem.length - 1) return;\n\n\t\t\t[elem[curPos], elem[newPos]] = [elem[newPos], elem[curPos]];\n\t\t\t[list[curPos], list[newPos]] = [list[newPos], list[curPos]];\n\t\t\tappendItems();\n\t\t}\n\n\t\tfunction onChangeStatus(item, status) {\n\t\t\tcontainer.remove();\n\t\t\tconst isToggle = statusDic[item.dataset.uri] === status;\n\t\t\tstatusDic[item.dataset.uri] = isToggle ? NORMAL : status;\n\t\t\tappendItems();\n\t\t}\n\n\t\tfor (const el of elem) {\n\t\t\tel.onmouseover = () => {\n\t\t\t\tconst status = statusDic[el.dataset.uri];\n\t\t\t\tconst index = elem.findIndex((a) => a === el);\n\n\t\t\t\tif (!status || index === 0 || status !== statusDic[elem[index - 1]?.dataset.uri]) {\n\t\t\t\t\tup.disabled = true;\n\t\t\t\t} else {\n\t\t\t\t\tup.disabled = false;\n\t\t\t\t\tup.onclick = () => onSwap(el, -1);\n\t\t\t\t}\n\n\t\t\t\tif (!status || index === elem.length - 1 || status !== statusDic[elem[index + 1]?.dataset.uri]) {\n\t\t\t\t\tdown.disabled = true;\n\t\t\t\t} else {\n\t\t\t\t\tdown.disabled = false;\n\t\t\t\t\tdown.onclick = () => onSwap(el, 1);\n\t\t\t\t}\n\n\t\t\t\tstick.innerText = status === STICKY ? \"Unstick\" : \"Stick\";\n\t\t\t\tlower.innerText = status === LOWERED ? \"Unlower\" : \"Lower\";\n\t\t\t\tlower.onclick = () => onChangeStatus(el, LOWERED);\n\t\t\t\tstick.onclick = () => onChangeStatus(el, STICKY);\n\n\t\t\t\tel.prepend(container);\n\t\t\t};\n\t\t}\n\t}\n\n\tfunction removeInteraction() {\n\t\tcontainer.remove();\n\t\tfor (const a of elem) {\n\t\t\ta.onmouseover = undefined;\n\t\t}\n\t}\n\n\tawait new Promise((res) => Spicetify.Events.webpackLoaded.on(res));\n\n\tSpicetifyHomeConfig.menu = new Spicetify.Menu.Item(\n\t\t\"Home config\",\n\t\tfalse,\n\t\t(self) => {\n\t\t\tself.setState(!self.isEnabled);\n\t\t\tif (self.isEnabled) {\n\t\t\t\tinjectInteraction();\n\t\t\t} else {\n\t\t\t\tremoveInteraction();\n\t\t\t}\n\t\t},\n\t\tSpicetify.SVGIcons[\"grid-view\"]\n\t);\n\n\tSpicetifyHomeConfig.addToMenu = () => {\n\t\tSpicetifyHomeConfig.menu.register();\n\t};\n\tSpicetifyHomeConfig.removeMenu = () => {\n\t\tSpicetifyHomeConfig.menu.setState(false);\n\t\tSpicetifyHomeConfig.menu.deregister();\n\t};\n\n\tawait new Promise((res) => Spicetify.Events.platformLoaded.on(res));\n\t// Init\n\tif (Spicetify.Platform.History.location.pathname === \"/\") {\n\t\tSpicetifyHomeConfig.addToMenu();\n\t}\n\n\tSpicetify.Platform.History.listen(({ pathname }) => {\n\t\tif (pathname === \"/\") {\n\t\t\tSpicetifyHomeConfig.addToMenu();\n\t\t} else {\n\t\t\tSpicetifyHomeConfig.removeMenu();\n\t\t}\n\t});\n})();\n"
  },
  {
    "path": "jsHelper/sidebarConfig.js",
    "content": "(function SidebarConfig() {\n\tconst sidebar = document.querySelector(\".Root__nav-bar\");\n\tif (!sidebar) return setTimeout(SidebarConfig, 100);\n\tlet isGlobalNavbar = false;\n\t// Status enum\n\tconst HIDDEN = 0;\n\tconst SHOW = 1;\n\tconst STICKY = 2;\n\t// Store sidebar buttons elements\n\tlet appItems;\n\tlet list;\n\tlet hiddenList;\n\n\tlet YLXSidebarState = 0;\n\n\t// Store sidebar buttons\n\tlet buttons = [];\n\tlet ordered = [];\n\n\tfunction arrangeItems(storage) {\n\t\tconst newButtons = [...buttons];\n\t\tconst orderedButtons = [];\n\t\tfor (const ele of storage) {\n\t\t\tconst index = newButtons.findIndex((a) => ele[0] === a?.dataset.id);\n\t\t\tif (index !== -1) {\n\t\t\t\torderedButtons.push([newButtons[index], ele[1]]);\n\t\t\t\tnewButtons[index] = undefined;\n\t\t\t}\n\t\t}\n\t\tfor (const button of newButtons) {\n\t\t\tif (button) orderedButtons.push([button, SHOW]);\n\t\t}\n\t\tordered = orderedButtons;\n\t}\n\n\tfunction appendItems() {\n\t\tconst toShow = [];\n\t\tconst toHide = [];\n\t\tconst toStick = [];\n\t\tfor (const el of ordered) {\n\t\t\tconst [item, status] = el;\n\t\t\tif (status === STICKY) {\n\t\t\t\tappItems.append(item);\n\t\t\t\ttoStick.push(el);\n\t\t\t} else if (status === SHOW) {\n\t\t\t\tlist.append(item);\n\t\t\t\ttoShow.push(el);\n\t\t\t} else {\n\t\t\t\thiddenList.append(item);\n\t\t\t\ttoHide.push(el);\n\t\t\t}\n\t\t}\n\t\tordered = [...toStick, ...toShow, ...toHide];\n\t}\n\n\tfunction writeStorage() {\n\t\tconst array = ordered.map((a) => [a[0].dataset.id, a[1]]);\n\n\t\treturn localStorage.setItem(\"spicetify-sidebar-config:ylx\", JSON.stringify(array));\n\t}\n\n\tconst container = document.createElement(\"div\");\n\tcontainer.id = \"spicetify-sidebar-config\";\n\tconst up = document.createElement(\"button\");\n\tup.innerText = \"Up\";\n\tconst down = document.createElement(\"button\");\n\tdown.innerText = \"Down\";\n\tconst hide = document.createElement(\"button\");\n\tconst stick = document.createElement(\"button\");\n\tconst style = document.createElement(\"style\");\n\tstyle.innerHTML = `\n#spicetify-hidden-list {\nbackground-color: rgba(var(--spice-rgb-main), .3);\n}\n#spicetify-sidebar-config {\nposition: relative;\nwidth: 100%;\nheight: 0;\ndisplay: flex;\njustify-content: space-evenly;\nalign-items: center;\ntop: -20px;\nleft: 0;\n}\n#spicetify-sidebar-config button {\nmin-width: 60px;\nborder-radius: 3px;\nbackground-color: var(--spice-main);\ncolor: var(--spice-text);\nborder: 1px solid var(--spice-text);\n}\n#spicetify-sidebar-config button:disabled {\ncolor: var(--spice-button-disabled);\n}\n`;\n\tcontainer.append(style, up, down, hide, stick);\n\n\tfunction injectInteraction() {\n\t\tfunction onSwap(item, dir) {\n\t\t\tcontainer.remove();\n\t\t\tconst curPos = ordered.findIndex((e) => e[0] === item);\n\t\t\tconst newPos = curPos + dir;\n\t\t\tif (newPos < 0 || newPos > ordered.length - 1) return;\n\n\t\t\t[ordered[curPos], ordered[newPos]] = [ordered[newPos], ordered[curPos]];\n\t\t\tappendItems();\n\t\t}\n\n\t\tfunction onChangeStatus(item, status) {\n\t\t\tcontainer.remove();\n\t\t\tconst curPos = ordered.findIndex((e) => e[0] === item);\n\t\t\tordered[curPos][1] = ordered[curPos][1] === status ? SHOW : status;\n\t\t\tappendItems();\n\t\t}\n\n\t\tYLXSidebarState = Spicetify.Platform.LocalStorageAPI.getItem(\"ylx-sidebar-state\");\n\t\tif (YLXSidebarState === 1) document.querySelector(\".main-yourLibraryX-collapseButton > button\")?.click();\n\n\t\tdocument.documentElement.style.setProperty(\"--nav-bar-width\", \"280px\");\n\n\t\thiddenList.classList.remove(\"hidden-visually\");\n\t\tfor (const el of ordered) {\n\t\t\tel[0].onmouseover = () => {\n\t\t\t\tconst [item, status] = el;\n\t\t\t\tconst index = ordered.findIndex((a) => a === el);\n\t\t\t\tif (index === 0 || ordered[index][1] !== ordered[index - 1][1]) {\n\t\t\t\t\tup.disabled = true;\n\t\t\t\t} else {\n\t\t\t\t\tup.disabled = false;\n\t\t\t\t\tup.onclick = () => onSwap(item, -1);\n\t\t\t\t}\n\t\t\t\tif (index === ordered.length - 1 || ordered[index][1] !== ordered[index + 1][1]) {\n\t\t\t\t\tdown.disabled = true;\n\t\t\t\t} else {\n\t\t\t\t\tdown.disabled = false;\n\t\t\t\t\tdown.onclick = () => onSwap(item, 1);\n\t\t\t\t}\n\n\t\t\t\tstick.innerText = status === STICKY ? \"Unstick\" : \"Stick\";\n\t\t\t\thide.innerText = status === HIDDEN ? \"Unhide\" : \"Hide\";\n\t\t\t\thide.onclick = () => onChangeStatus(item, HIDDEN);\n\t\t\t\tstick.onclick = () => onChangeStatus(item, STICKY);\n\n\t\t\t\titem.append(container);\n\t\t\t};\n\t\t}\n\t}\n\n\tfunction removeInteraction() {\n\t\thiddenList.classList.add(\"hidden-visually\");\n\t\tcontainer.remove();\n\t\tfor (const a of ordered) {\n\t\t\ta[0].onmouseover = undefined;\n\t\t}\n\t\tif (YLXSidebarState === 1) document.querySelector(\".main-yourLibraryX-collapseButton > button\")?.click();\n\t\telse\n\t\t\tdocument.documentElement.style.setProperty(\n\t\t\t\t\"--nav-bar-width\",\n\t\t\t\t`${Spicetify.Platform.LocalStorageAPI.getItem(\n\t\t\t\t\tYLXSidebarState === 2 ? \"ylx-expanded-state-nav-bar-width\" : \"ylx-default-state-nav-bar-width\"\n\t\t\t\t)}px`\n\t\t\t);\n\t\twriteStorage();\n\t}\n\n\t(async () => {\n\t\tawait new Promise((res) => Spicetify.Events.webpackLoaded.on(res));\n\n\t\twhile (!Spicetify.Snackbar?.enqueueCustomSnackbar) {\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, 10));\n\t\t}\n\n\t\tif (document.querySelector(\".Root__globalNav\")) {\n\t\t\tconst content = Spicetify.React.createElement(\"div\", {\n\t\t\t\tdangerouslySetInnerHTML: {\n\t\t\t\t\t__html:\n\t\t\t\t\t\t\"Sidebar config is not supported when Global Navbar is enabled.<br>In your terminal, please run <code>spicetify config sidebar_config 0</code> command and then re-apply spicetify with <code>spicetify apply</code>.\",\n\t\t\t\t},\n\t\t\t\tstyle: {\n\t\t\t\t\t\"text-size\": \"12px\",\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tSpicetify.Snackbar?.enqueueCustomSnackbar(\"sidebar-config\", {\n\t\t\t\tkeyPrefix: \"sidebar-config\",\n\t\t\t\tautoHideDuration: 7500,\n\t\t\t\tchildren: Spicetify.ReactComponent.Snackbar.wrapper({\n\t\t\t\t\tchildren: Spicetify.React.createElement(Spicetify.ReactComponent.Snackbar.simpleLayout, {\n\t\t\t\t\t\tcenter: content,\n\t\t\t\t\t\tchildren: content,\n\t\t\t\t\t\tdragMetadata: {},\n\t\t\t\t\t}),\n\t\t\t\t}),\n\t\t\t});\n\t\t\tisGlobalNavbar = true;\n\t\t}\n\n\t\tif (!isGlobalNavbar) {\n\t\t\tnew Spicetify.Menu.Item(\n\t\t\t\t\"Sidebar config\",\n\t\t\t\tfalse,\n\t\t\t\t(self) => {\n\t\t\t\t\tself.setState(!self.isEnabled);\n\t\t\t\t\tif (self.isEnabled) {\n\t\t\t\t\t\tinjectInteraction();\n\t\t\t\t\t} else {\n\t\t\t\t\t\tremoveInteraction();\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t`<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 48 48\" width=\"16px\" height=\"16px\" fill=\"currentcolor\"><path d=\"M44.7,11L36,19.6c0,0-2.6,0-5.2-2.6s-2.6-5.2-2.6-5.2l8.7-8.7c-4.9-1.2-10.8,0.4-14.4,4c-5.4,5.4-0.6,12.3-2,13.7C12.9,28.7,5.1,34.7,4.9,35c-2.3,2.3-2.4,6-0.2,8.2c2.2,2.2,5.9,2.1,8.2-0.2c0.3-0.3,6.7-8.4,14.2-15.9c1.4-1.4,8,3.7,13.6-1.8C44.2,21.7,45.9,15.9,44.7,11z M9.4,41.1c-1.4,0-2.5-1.1-2.5-2.5C6.9,37.1,8,36,9.4,36c1.4,0,2.5,1.1,2.5,2.5C11.9,39.9,10.8,41.1,9.4,41.1z\"/></svg>`\n\t\t\t).register();\n\t\t}\n\t})();\n\n\tfunction initConfig() {\n\t\tconst libraryX = document.querySelector(\".main-yourLibraryX-navItems\");\n\n\t\tif (!libraryX) {\n\t\t\tsetTimeout(initConfig, 300);\n\t\t\treturn;\n\t\t}\n\n\t\tInitSidebarXConfig();\n\t}\n\n\tfunction InitSidebarXConfig() {\n\t\t// STICKY container\n\t\tconst YLXAppItems = document.querySelector(\".main-yourLibraryX-navItems\");\n\t\tconst libraryItems = document.querySelector(\".main-yourLibraryX-library\");\n\n\t\tif (!YLXAppItems || !libraryItems?.querySelector(\"ul\")) {\n\t\t\tsetTimeout(InitSidebarXConfig, 300);\n\t\t\treturn;\n\t\t}\n\n\t\tappItems = YLXAppItems;\n\t\tbuttons = [];\n\t\tordered = [];\n\n\t\tappItems.id = \"spicetify-sticky-list\";\n\t\t// SHOW container\n\t\tlist = document.createElement(\"ul\");\n\t\tlist.id = \"spicetify-show-list\";\n\t\t// HIDDEN container\n\t\thiddenList = document.createElement(\"ul\");\n\t\thiddenList.id = \"spicetify-hidden-list\";\n\t\thiddenList.classList.add(\"hidden-visually\");\n\t\tconst playlistList = libraryItems.querySelector(\"ul\");\n\t\tplaylistList.id = \"spicetify-playlist-list\";\n\t\tlibraryItems.prepend(list, hiddenList);\n\n\t\tfor (const ele of appItems.children) {\n\t\t\tele.dataset.id = ele.querySelector(\"a\").pathname;\n\t\t\tbuttons.push(ele);\n\t\t}\n\n\t\tlet storage = [];\n\t\ttry {\n\t\t\tstorage = JSON.parse(localStorage.getItem(\"spicetify-sidebar-config:ylx\"));\n\t\t\tif (!Array.isArray(storage)) throw \"\";\n\t\t} catch {\n\t\t\tstorage = buttons.map((el) => [el.dataset.id, STICKY]);\n\t\t}\n\n\t\tconst observer = new MutationObserver((mutations) => {\n\t\t\tfor (const mutation of mutations) {\n\t\t\t\tif (mutation.type === \"childList\" && mutation.addedNodes.length) {\n\t\t\t\t\tmutation.addedNodes[0].id = \"spicetify-playlist-list\";\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tobserver.observe(playlistList.parentElement, { childList: true });\n\n\t\tarrangeItems(storage);\n\t\tappendItems();\n\t}\n\n\tinitConfig();\n\n\tconst customButtonStyle = document.createElement(\"style\");\n\tcustomButtonStyle.innerHTML = `\ndiv.GlueDropTarget.personal-library  {\npadding: 0 8px;\n}\ndiv.GlueDropTarget.personal-library >* {\npadding: 0 16px;\nheight: 40px;\nborder-radius: 4px;\n}\ndiv.GlueDropTarget.personal-library >*.active {\nbackground: var(--spice-card);\n}\n.main-rootlist-rootlist {\nmargin-top: 0;\n}\n.Root__nav-bar :not(.main-yourLibraryX-entryPoints) > #spicetify-show-list >* {\npadding: 0 24px 0 8px;\n}\n.main-yourLibraryX-entryPoints #spicetify-show-list,\n.main-yourLibraryX-entryPoints #spicetify-hidden-list {\npadding: 0 12px;\n}\n`;\n\tdocument.head.append(customButtonStyle);\n})();\n"
  },
  {
    "path": "jsHelper/spicetifyWrapper.js",
    "content": "window.Spicetify = {\n\tPlayer: {\n\t\taddEventListener: (type, callback) => {\n\t\t\tif (!(type in Spicetify.Player.eventListeners)) {\n\t\t\t\tSpicetify.Player.eventListeners[type] = [];\n\t\t\t}\n\t\t\tSpicetify.Player.eventListeners[type].push(callback);\n\t\t},\n\t\tdispatchEvent: (event) => {\n\t\t\tif (!(event.type in Spicetify.Player.eventListeners)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tconst stack = Spicetify.Player.eventListeners[event.type];\n\t\t\tfor (let i = 0; i < stack.length; i++) {\n\t\t\t\tif (typeof stack[i] === \"function\") {\n\t\t\t\t\tstack[i](event);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn !event.defaultPrevented;\n\t\t},\n\t\teventListeners: {},\n\t\tseek: (p) => {\n\t\t\tconst duration = p <= 1 ? Math.round(p * Spicetify.Player.origin._state.duration) : p;\n\t\t\tSpicetify.Player.origin.seekTo(duration);\n\t\t},\n\t\tgetProgress: () =>\n\t\t\t(Spicetify.Player.origin._state.isPaused ? 0 : Date.now() - Spicetify.Player.origin._state.timestamp) +\n\t\t\tSpicetify.Player.origin._state.positionAsOfTimestamp,\n\t\tgetProgressPercent: () => Spicetify.Player.getProgress() / Spicetify.Player.origin._state.duration,\n\t\tgetDuration: () => Spicetify.Player.origin._state.duration,\n\t\tsetVolume: (v) => {\n\t\t\tSpicetify.Platform.PlaybackAPI.setVolume(v);\n\t\t},\n\t\tincreaseVolume: () => {\n\t\t\tSpicetify.Platform.PlaybackAPI.raiseVolume();\n\t\t},\n\t\tdecreaseVolume: () => {\n\t\t\tSpicetify.Platform.PlaybackAPI.lowerVolume();\n\t\t},\n\t\tgetVolume: () => Spicetify.Platform.PlaybackAPI._volume,\n\t\tnext: () => {\n\t\t\tSpicetify.Player.origin.skipToNext();\n\t\t},\n\t\tback: () => {\n\t\t\tSpicetify.Player.origin.skipToPrevious();\n\t\t},\n\t\ttogglePlay: () => {\n\t\t\tSpicetify.Player.isPlaying() ? Spicetify.Player.pause() : Spicetify.Player.play();\n\t\t},\n\t\tisPlaying: () => !Spicetify.Player.origin._state.isPaused,\n\t\ttoggleShuffle: () => {\n\t\t\tSpicetify.Player.origin.setShuffle(!Spicetify.Player.origin._state.shuffle);\n\t\t},\n\t\tgetShuffle: () => Spicetify.Player.origin._state.shuffle,\n\t\tsetShuffle: (b) => {\n\t\t\tSpicetify.Player.origin.setShuffle(b);\n\t\t},\n\t\ttoggleRepeat: () => {\n\t\t\tSpicetify.Player.origin.setRepeat((Spicetify.Player.origin._state.repeat + 1) % 3);\n\t\t},\n\t\tgetRepeat: () => Spicetify.Player.origin._state.repeat,\n\t\tsetRepeat: (r) => {\n\t\t\tSpicetify.Player.origin.setRepeat(r);\n\t\t},\n\t\tgetMute: () => Spicetify.Player.getVolume() === 0,\n\t\ttoggleMute: () => {\n\t\t\tSpicetify.Player.setMute(!Spicetify.Player.getMute());\n\t\t},\n\t\tsetMute: (b) => {\n\t\t\tif (b !== Spicetify.Player.getMute()) {\n\t\t\t\tdocument.querySelector(\".volume-bar__icon-button\")?.click();\n\t\t\t}\n\t\t},\n\t\tformatTime: (ms) => {\n\t\t\tlet seconds = Math.floor(ms / 1e3);\n\t\t\tconst minutes = Math.floor(seconds / 60);\n\t\t\tseconds -= minutes * 60;\n\t\t\treturn `${minutes}:${seconds > 9 ? \"\" : \"0\"}${String(seconds)}`;\n\t\t},\n\t\tgetHeart: () => Spicetify.Player.origin._state.item.metadata[\"collection.in_collection\"] === \"true\",\n\t\tpause: () => {\n\t\t\tSpicetify.Player.origin.pause();\n\t\t},\n\t\tplay: () => {\n\t\t\tSpicetify.Player.origin.resume();\n\t\t},\n\t\tplayUri: async (uri, context = {}, options = {}) => {\n\t\t\treturn await Spicetify.Player.origin.play({ uri: uri }, context, options);\n\t\t},\n\t\tremoveEventListener: (type, callback) => {\n\t\t\tif (!(type in Spicetify.Player.eventListeners)) return;\n\t\t\tconst stack = Spicetify.Player.eventListeners[type];\n\t\t\tfor (let i = 0; i < stack.length; i++) {\n\t\t\t\tif (stack[i] === callback) {\n\t\t\t\t\tstack.splice(i, 1);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\tskipBack: (amount = 15e3) => {\n\t\t\tSpicetify.Player.origin.seekBackward(amount);\n\t\t},\n\t\tskipForward: (amount = 15e3) => {\n\t\t\tSpicetify.Player.origin.seekForward(amount);\n\t\t},\n\t\tsetHeart: (b) => {\n\t\t\tconst uris = [Spicetify.Player.origin._state.item.uri];\n\t\t\tif (b) {\n\t\t\t\tSpicetify.Platform.LibraryAPI.add({ uris });\n\t\t\t} else {\n\t\t\t\tSpicetify.Platform.LibraryAPI.remove({ uris });\n\t\t\t}\n\t\t},\n\t\ttoggleHeart: () => {\n\t\t\tSpicetify.Player.setHeart(!Spicetify.Player.getHeart());\n\t\t},\n\t},\n\ttest: () => {\n\t\tfunction checkObject(object) {\n\t\t\tconst { objectToCheck, methods, name } = object;\n\t\t\tlet count = methods.size;\n\n\t\t\tfor (const method of methods) {\n\t\t\t\tif (objectToCheck[method] === undefined || objectToCheck[method] === null) {\n\t\t\t\t\tconsole.error(`${name}.${method} is not available. Please open an issue in the Spicetify repository to inform us about it.`);\n\t\t\t\t\tcount--;\n\t\t\t\t}\n\t\t\t}\n\t\t\tconsole.log(`${count}/${methods.size} ${name} methods and objects are OK.`);\n\n\t\t\tfor (const key of Object.keys(objectToCheck)) {\n\t\t\t\tif (!methods.has(key)) {\n\t\t\t\t\tconsole.warn(`${name} method ${key} exists but is not in the method list. Consider adding it.`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst objectsToCheck = new Set([\n\t\t\t{\n\t\t\t\tobjectToCheck: Spicetify,\n\t\t\t\tname: \"Spicetify\",\n\t\t\t\tmethods: new Set([\n\t\t\t\t\t\"Player\",\n\t\t\t\t\t\"addToQueue\",\n\t\t\t\t\t\"CosmosAsync\",\n\t\t\t\t\t\"getAudioData\",\n\t\t\t\t\t\"Keyboard\",\n\t\t\t\t\t\"URI\",\n\t\t\t\t\t\"LocalStorage\",\n\t\t\t\t\t\"Queue\",\n\t\t\t\t\t\"removeFromQueue\",\n\t\t\t\t\t\"showNotification\",\n\t\t\t\t\t\"Menu\",\n\t\t\t\t\t\"ContextMenu\",\n\t\t\t\t\t\"React\",\n\t\t\t\t\t\"Mousetrap\",\n\t\t\t\t\t\"Locale\",\n\t\t\t\t\t\"ReactDOM\",\n\t\t\t\t\t\"Topbar\",\n\t\t\t\t\t\"ReactComponent\",\n\t\t\t\t\t\"PopupModal\",\n\t\t\t\t\t\"SVGIcons\",\n\t\t\t\t\t\"colorExtractor\",\n\t\t\t\t\t\"test\",\n\t\t\t\t\t\"Platform\",\n\t\t\t\t\t\"_platform\",\n\t\t\t\t\t\"Config\",\n\t\t\t\t\t\"expFeatureOverride\",\n\t\t\t\t\t\"createInternalMap\",\n\t\t\t\t\t\"RemoteConfigResolver\",\n\t\t\t\t\t\"Playbar\",\n\t\t\t\t\t\"Tippy\",\n\t\t\t\t\t\"_getStyledClassName\",\n\t\t\t\t\t\"GraphQL\",\n\t\t\t\t\t\"ReactHook\",\n\t\t\t\t\t\"AppTitle\",\n\t\t\t\t\t\"_reservedPanelIds\",\n\t\t\t\t\t\"ReactFlipToolkit\",\n\t\t\t\t\t\"classnames\",\n\t\t\t\t\t\"ReactQuery\",\n\t\t\t\t\t\"Color\",\n\t\t\t\t\t\"extractColorPreset\",\n\t\t\t\t\t\"ReactDOMServer\",\n\t\t\t\t\t\"Snackbar\",\n\t\t\t\t\t\"ContextMenuV2\",\n\t\t\t\t\t\"ReactJSX\",\n\t\t\t\t\t\"_renderNavLinks\",\n\t\t\t\t\t\"Events\",\n\t\t\t\t]),\n\t\t\t},\n\t\t\t{\n\t\t\t\tobjectToCheck: Spicetify.Player,\n\t\t\t\tname: \"Spicetify.Player\",\n\t\t\t\tmethods: new Set([\n\t\t\t\t\t\"addEventListener\",\n\t\t\t\t\t\"back\",\n\t\t\t\t\t\"data\",\n\t\t\t\t\t\"decreaseVolume\",\n\t\t\t\t\t\"dispatchEvent\",\n\t\t\t\t\t\"eventListeners\",\n\t\t\t\t\t\"formatTime\",\n\t\t\t\t\t\"getDuration\",\n\t\t\t\t\t\"getHeart\",\n\t\t\t\t\t\"getMute\",\n\t\t\t\t\t\"getProgress\",\n\t\t\t\t\t\"getProgressPercent\",\n\t\t\t\t\t\"getRepeat\",\n\t\t\t\t\t\"getShuffle\",\n\t\t\t\t\t\"getVolume\",\n\t\t\t\t\t\"increaseVolume\",\n\t\t\t\t\t\"isPlaying\",\n\t\t\t\t\t\"next\",\n\t\t\t\t\t\"pause\",\n\t\t\t\t\t\"play\",\n\t\t\t\t\t\"removeEventListener\",\n\t\t\t\t\t\"seek\",\n\t\t\t\t\t\"setMute\",\n\t\t\t\t\t\"setRepeat\",\n\t\t\t\t\t\"setShuffle\",\n\t\t\t\t\t\"setVolume\",\n\t\t\t\t\t\"skipBack\",\n\t\t\t\t\t\"skipForward\",\n\t\t\t\t\t\"toggleHeart\",\n\t\t\t\t\t\"toggleMute\",\n\t\t\t\t\t\"togglePlay\",\n\t\t\t\t\t\"toggleRepeat\",\n\t\t\t\t\t\"toggleShuffle\",\n\t\t\t\t\t\"origin\",\n\t\t\t\t\t\"playUri\",\n\t\t\t\t\t\"setHeart\",\n\t\t\t\t]),\n\t\t\t},\n\t\t\t{\n\t\t\t\tobjectToCheck: Spicetify.ReactComponent,\n\t\t\t\tname: \"Spicetify.ReactComponent\",\n\t\t\t\tmethods: new Set([\n\t\t\t\t\t\"RightClickMenu\",\n\t\t\t\t\t\"ContextMenu\",\n\t\t\t\t\t\"Menu\",\n\t\t\t\t\t\"MenuItem\",\n\t\t\t\t\t\"AlbumMenu\",\n\t\t\t\t\t\"PodcastShowMenu\",\n\t\t\t\t\t\"ArtistMenu\",\n\t\t\t\t\t\"PlaylistMenu\",\n\t\t\t\t\t\"TrackMenu\",\n\t\t\t\t\t\"TooltipWrapper\",\n\t\t\t\t\t\"TextComponent\",\n\t\t\t\t\t\"IconComponent\",\n\t\t\t\t\t\"ConfirmDialog\",\n\t\t\t\t\t\"Slider\",\n\t\t\t\t\t\"RemoteConfigProvider\",\n\t\t\t\t\t\"ButtonPrimary\",\n\t\t\t\t\t\"ButtonSecondary\",\n\t\t\t\t\t\"ButtonTertiary\",\n\t\t\t\t\t\"Snackbar\",\n\t\t\t\t\t\"Chip\",\n\t\t\t\t\t\"Toggle\",\n\t\t\t\t\t\"Cards\",\n\t\t\t\t\t\"Router\",\n\t\t\t\t\t\"Routes\",\n\t\t\t\t\t\"Route\",\n\t\t\t\t\t\"StoreProvider\",\n\t\t\t\t\t\"PlatformProvider\",\n\t\t\t\t\t\"Dropdown\",\n\t\t\t\t\t\"MenuSubMenuItem\",\n\t\t\t\t\t\"Navigation\",\n\t\t\t\t\t\"ScrollableContainer\",\n\t\t\t\t]),\n\t\t\t},\n\t\t\t{\n\t\t\t\tobjectToCheck: Spicetify.ReactComponent.Cards,\n\t\t\t\tname: \"Spicetify.ReactComponent.Cards\",\n\t\t\t\tmethods: new Set([\n\t\t\t\t\t\"Default\",\n\t\t\t\t\t\"Hero\",\n\t\t\t\t\t\"CardImage\",\n\t\t\t\t\t\"Album\",\n\t\t\t\t\t\"Artist\",\n\t\t\t\t\t\"Audiobook\",\n\t\t\t\t\t\"Episode\",\n\t\t\t\t\t\"Playlist\",\n\t\t\t\t\t\"Profile\",\n\t\t\t\t\t\"Show\",\n\t\t\t\t\t\"Track\",\n\t\t\t\t\t\"FeatureCard\",\n\t\t\t\t]),\n\t\t\t},\n\t\t\t{\n\t\t\t\tobjectToCheck: Spicetify.ReactHook,\n\t\t\t\tname: \"Spicetify.ReactHook\",\n\t\t\t\tmethods: new Set([\"DragHandler\", \"useExtractedColor\"]),\n\t\t\t},\n\t\t]);\n\n\t\tfor (const object of objectsToCheck) {\n\t\t\tcheckObject(object);\n\t\t}\n\t},\n\tGraphQL: {\n\t\tDefinitions: {},\n\t},\n\tReactComponent: {},\n\tReactHook: {},\n\tReactFlipToolkit: {},\n\tSnackbar: {},\n\tPlatform: {},\n};\n\n(function waitForPlatform() {\n\tif (!Spicetify._platform) {\n\t\tsetTimeout(waitForPlatform, 50);\n\t\treturn;\n\t}\n\tconst { _platform } = Spicetify;\n\tfor (const key of Object.keys(_platform)) {\n\t\tif (key.startsWith(\"get\") && typeof _platform[key] === \"function\") {\n\t\t\tSpicetify.Platform[key.slice(3)] = _platform[key]();\n\t\t} else {\n\t\t\tSpicetify.Platform[key] = _platform[key];\n\t\t}\n\t}\n})();\n\n(function addMissingPlatformAPIs() {\n\tif (!Spicetify.Platform?.version && !Spicetify.Platform?.Registry) {\n\t\tsetTimeout(addMissingPlatformAPIs, 50);\n\t\treturn;\n\t}\n\tconst os = Spicetify.Platform.operatingSystem;\n\tconst version = Spicetify.Platform.version.split(\".\").map((i) => Number.parseInt(i, 10));\n\tif (version[0] === 1 && version[1] === 2 && version[2] < 38) return;\n\n\tfor (const [key, _] of Spicetify.Platform.Registry._map.entries()) {\n\t\tif (typeof key?.description !== \"string\" || !key?.description.endsWith(\"API\")) continue;\n\t\tconst symbolName = key.description;\n\t\tif (symbolName === \"ExclusiveModeAPI\" && os === \"Linux\") continue;\n\t\tif (Object.hasOwn(Spicetify.Platform, symbolName)) continue;\n\t\ttry {\n\t\t\tconst resolvedAPI = Spicetify.Platform.Registry.resolve(key);\n\t\t\tSpicetify.Platform[symbolName] = resolvedAPI;\n\n\t\t\tconsole.debug(`[spicetifyWrapper] Resolved PlatformAPI from Registry: ${symbolName}`);\n\t\t} catch (err) {\n\t\t\tconsole.error(`[spicetifyWrapper] Error resolving PlatformAPI from Registry: ${symbolName}`, err);\n\t\t}\n\t}\n})();\n\n// Based on https://blog.aziz.tn/2025/01/spotify-fix-lagging-issue-on-scrolling.html\nfunction applyScrollingFix() {\n\tif (!Spicetify.Platform?.version) {\n\t\tsetTimeout(applyScrollingFix, 50);\n\t\treturn;\n\t}\n\n\t// Run only for 1.2.56 and lower\n\tconst version = Spicetify.Platform.version.split(\".\").map((i) => Number.parseInt(i, 10));\n\tif (version[1] >= 2 && version[2] >= 57) return;\n\n\tconst scrollableElements = Array.from(document.querySelectorAll(\"*\")).filter((el) => {\n\t\tif (\n\t\t\tel.id === \"context-menu\" ||\n\t\t\tel.closest(\"#context-menu\") ||\n\t\t\tel.getAttribute(\"role\") === \"dialog\" ||\n\t\t\tel.classList.contains(\"popup\") ||\n\t\t\tel.getAttribute(\"aria-haspopup\") === \"true\"\n\t\t)\n\t\t\treturn false;\n\n\t\tconst style = window.getComputedStyle(el);\n\t\treturn style.overflow === \"auto\" || style.overflow === \"scroll\" || style.overflowY === \"auto\" || style.overflowY === \"scroll\";\n\t});\n\n\tfor (const el of scrollableElements) {\n\t\tif (!el.hasAttribute(\"data-scroll-optimized\")) {\n\t\t\tel.style.willChange = \"transform\";\n\t\t\tel.style.transform = \"translate3d(0, 0, 0)\";\n\t\t\tel.setAttribute(\"data-scroll-optimized\", \"true\");\n\t\t}\n\t}\n}\n\nconst observer = new MutationObserver(applyScrollingFix);\n\nobserver.observe(document.body, {\n\tchildList: true,\n\tsubtree: true,\n\tattributes: false,\n});\n\nconst originalPushState = history.pushState;\nhistory.pushState = function (...args) {\n\toriginalPushState.apply(this, args);\n\tsetTimeout(applyScrollingFix, 100);\n};\n\nwindow.addEventListener(\"popstate\", () => {\n\tsetTimeout(applyScrollingFix, 100);\n});\n\napplyScrollingFix();\n\n(async function addProxyCosmos() {\n\tif (!Spicetify.Player.origin?._cosmos && !Spicetify.Platform?.Registry) {\n\t\tsetTimeout(addProxyCosmos, 50);\n\t\treturn;\n\t}\n\n\tconst _cosmos = Spicetify.Player.origin?._cosmos ?? Spicetify.Platform?.Registry.resolve(Symbol.for(\"Cosmos\"));\n\n\tconst allowedMethodsMap = {\n\t\tget: \"get\",\n\t\tpost: \"post\",\n\t\tdel: \"delete\",\n\t\tput: \"put\",\n\t\tpatch: \"patch\",\n\t};\n\tconst allowedMethodsSet = new Set(Object.keys(allowedMethodsMap));\n\tconst internalEndpoints = new Set([\"sp:\", \"wg:\"]);\n\n\tconst handler = {\n\t\tget: (target, prop, receiver) => {\n\t\t\tconst internalFetch = Reflect.get(target, prop, receiver);\n\n\t\t\tif (typeof internalFetch !== \"function\" || !allowedMethodsSet.has(prop)) return internalFetch;\n\t\t\tconst version = Spicetify.Platform.version.split(\".\").map((i) => Number.parseInt(i));\n\t\t\tif (version[1] >= 2 && version[2] < 31) return internalFetch;\n\n\t\t\treturn async function (url, body) {\n\t\t\t\tconst urlObj = new URL(url);\n\n\t\t\t\tconst corsProxyURLTemplate = window.localStorage.getItem(\"spicetify:corsProxyTemplate\") ?? \"https://cors-proxy.spicetify.app/{url}\";\n\t\t\t\tconst isWebAPI = urlObj.hostname === \"api.spotify.com\";\n\t\t\t\tconst isSpClientAPI = urlObj.hostname.includes(\"spotify.com\") && urlObj.hostname.includes(\"spclient\");\n\t\t\t\tconst isInternalURL = internalEndpoints.has(urlObj.protocol);\n\t\t\t\tif (isInternalURL) return internalFetch.apply(this, [url, body]);\n\n\t\t\t\tconst shouldUseCORSProxy = !isWebAPI && !isSpClientAPI && !isInternalURL;\n\n\t\t\t\tconst method = allowedMethodsMap[prop.toLowerCase()];\n\t\t\t\tconst headers = {\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t};\n\n\t\t\t\tconst options = {\n\t\t\t\t\tmethod,\n\t\t\t\t\theaders,\n\t\t\t\t\ttimeout: 1000 * 15,\n\t\t\t\t};\n\n\t\t\t\tlet finalURL = urlObj.toString();\n\t\t\t\tif (body) {\n\t\t\t\t\tif (method === \"get\") {\n\t\t\t\t\t\tconst params = new URLSearchParams(body);\n\t\t\t\t\t\tconst useSeparator = shouldUseCORSProxy && new URL(finalURL).search.startsWith(\"?\");\n\t\t\t\t\t\tfinalURL += `${useSeparator ? \"&\" : \"?\"}${params.toString()}`;\n\t\t\t\t\t} else options.body = !Array.isArray(body) && typeof body === \"object\" ? JSON.stringify(body) : body;\n\t\t\t\t}\n\t\t\t\tif (shouldUseCORSProxy) {\n\t\t\t\t\tfinalURL = corsProxyURLTemplate.replace(/{url}/, finalURL);\n\t\t\t\t\ttry {\n\t\t\t\t\t\tnew URL(finalURL);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tconsole.error(\"[spicetifyWrapper] Invalid CORS Proxy URL template\");\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst Authorization = `Bearer ${Spicetify.Platform.AuthorizationAPI.getState().token.accessToken}`;\n\t\t\t\tlet injectedHeaders = {};\n\t\t\t\tif (isWebAPI) injectedHeaders = { Authorization };\n\t\t\t\tif (isSpClientAPI) {\n\t\t\t\t\tinjectedHeaders = {\n\t\t\t\t\t\tAuthorization,\n\t\t\t\t\t\t\"Spotify-App-Version\": Spicetify.Platform.version,\n\t\t\t\t\t\t\"App-Platform\": Spicetify.Platform.PlatformData.app_platform,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tObject.assign(options.headers, injectedHeaders);\n\n\t\t\t\ttry {\n\t\t\t\t\treturn fetch(finalURL, options).then((res) => {\n\t\t\t\t\t\tif (!res.ok) return { code: res.status, error: res.statusText, message: \"Failed to fetch\", stack: undefined };\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\treturn res.clone().json();\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\treturn res.clone().blob();\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\treturn res.clone().text();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconsole.error(e);\n\t\t\t\t}\n\t\t\t};\n\t\t},\n\t};\n\n\twhile (!Spicetify.Player.origin) await new Promise((r) => setTimeout(r, 50));\n\tSpicetify.Player.origin._cosmos = new Proxy(_cosmos, handler);\n\tObject.defineProperty(Spicetify, \"CosmosAsync\", {\n\t\tget: () => {\n\t\t\treturn Spicetify.Player.origin?._cosmos;\n\t\t},\n\t});\n})();\n\nconst fnStr = (f) => {\n\ttry {\n\t\treturn f.toString();\n\t} catch {\n\t\ttry {\n\t\t\treturn Function.prototype.toString.call(f);\n\t\t} catch {\n\t\t\treturn \"\";\n\t\t}\n\t}\n};\n\n(async function hotloadWebpackModules() {\n\twhile (!window?.webpackChunkclient_web) {\n\t\tawait new Promise((r) => setTimeout(r, 50));\n\t}\n\n\t// Force all webpack modules to load\n\tconst require = webpackChunkclient_web.push([[Symbol()], {}, (re) => re]);\n\twhile (!require.m) await new Promise((r) => setTimeout(r, 50));\n\tconsole.log(\"[spicetifyWrapper] Waiting for required webpack modules to load\");\n\tlet webpackDidCallback = false;\n\t// https://github.com/webpack/webpack/blob/main/lib/runtime/OnChunksLoadedRuntimeModule.js\n\trequire.O(\n\t\tnull,\n\t\t[],\n\t\t() => {\n\t\t\twebpackDidCallback = true;\n\t\t},\n\t\t1\n\t);\n\n\tlet chunks = Object.entries(require.m);\n\tlet cache = Object.keys(require.m).map((id) => require(id));\n\n\t// For _renderNavLinks to work\n\tSpicetify.React = cache.find((m) => m?.useMemo);\n\n\twhile (!webpackDidCallback) {\n\t\tawait new Promise((r) => setTimeout(r, 100));\n\t}\n\tconsole.log(\"[spicetifyWrapper] All required webpack modules loaded\");\n\tchunks = Object.entries(require.m);\n\tcache = Object.keys(require.m).map((id) => require(id));\n\tSpicetify.Events.platformLoaded.fire();\n\n\tconst modules = cache\n\t\t.filter((module) => typeof module === \"object\")\n\t\t.flatMap((module) => {\n\t\t\ttry {\n\t\t\t\treturn Object.values(module);\n\t\t\t} catch {}\n\t\t});\n\t// polyfill for chromium <117\n\tconst groupBy = (values, keyFinder) => {\n\t\tif (typeof Object.groupBy === \"function\") return Object.groupBy(values, keyFinder);\n\t\treturn values.reduce((a, b) => {\n\t\t\tconst key = typeof keyFinder === \"function\" ? keyFinder(b) : b[keyFinder];\n\t\t\ta[key] = a[key] ? [...a[key], b] : [b];\n\t\t\treturn a;\n\t\t}, {});\n\t};\n\tconst webpackFactories = new Set(Object.values(require.m));\n\tconst functionModules = modules.flatMap((module) =>\n\t\ttypeof module === \"function\"\n\t\t\t? [module]\n\t\t\t: typeof module === \"object\" && module\n\t\t\t\t? Object.values(module).filter((v) => typeof v === \"function\" && !webpackFactories.has(v))\n\t\t\t\t: []\n\t);\n\tconst exportedReactObjects = groupBy(modules.filter(Boolean), (x) => x.$$typeof);\n\tconst exportedMemos = exportedReactObjects[Symbol.for(\"react.memo\")];\n\tconst exportedForwardRefs = exportedReactObjects[Symbol.for(\"react.forward_ref\")];\n\tconst exportedMemoFRefs = exportedMemos.filter((m) => m.type.$$typeof === Symbol.for(\"react.forward_ref\"));\n\tconst exposeReactComponentsUI = ({ modules, functionModules, exportedForwardRefs }) => {\n\t\tconst componentNames = Object.keys(modules.filter(Boolean).find((e) => e.BrowserDefaultFocusStyleProvider));\n\t\tconst componentRegexes = componentNames.map((n) => new RegExp(`\"data-encore-id\":(?:[a-zA-Z_$][w$]*\\\\.){2}${n}\\\\b`));\n\t\tconst componentPairs = [functionModules.map((f) => [f, f]), exportedForwardRefs.map((f) => [f.render, f])]\n\t\t\t.flat()\n\t\t\t.map(([s, f]) => [componentNames.find((_, i) => fnStr(s)?.match(componentRegexes[i])), f]);\n\n\t\treturn Object.fromEntries(componentPairs);\n\t};\n\tconst reactComponentsUI = exposeReactComponentsUI({ modules, functionModules, exportedForwardRefs });\n\n\tconst knownMenuTypes = [\"album\", \"show\", \"artist\", \"track\", \"playlist\"];\n\tconst menus = modules\n\t\t.map((m) => {\n\t\t\tconst valueMatch = (m?.type ? fnStr(m.type) : \"\").match(/value:\"([\\w-]+)\"/);\n\t\t\tif (valueMatch) return [m, valueMatch[1]];\n\t\t\tconst typeMatch = (m?.type ? fnStr(m.type) : \"\").match(/type:[\\w$]+\\.[\\w$]+\\.([A-Z_]+)/);\n\t\t\tif (typeMatch) return [m, typeMatch[1].toLowerCase()];\n\t\t\treturn null;\n\t\t})\n\t\t.filter(Boolean)\n\t\t.filter((m) => m[1] !== 'value:\"row\"')\n\t\t.map(([module, type]) => {\n\t\t\ttype = type.match(/value:\"([\\w-]+)\"/)?.[1] ?? type;\n\n\t\t\tif (!knownMenuTypes.includes(type)) return;\n\t\t\tif (type === \"show\") type = \"podcast-show\";\n\n\t\t\ttype = `${type\n\t\t\t\t.split(\"-\")\n\t\t\t\t.map((str) => str[0].toUpperCase() + str.slice(1))\n\t\t\t\t.join(\"\")}Menu`;\n\t\t\treturn [type, module];\n\t\t})\n\t\t.filter(Boolean);\n\n\tconst cardTypesToFind = [\"album\", \"artist\", \"audiobook\", \"episode\", \"playlist\", \"profile\", \"show\", \"track\"];\n\tconst cards = [\n\t\t...functionModules\n\t\t\t.flatMap((m) => {\n\t\t\t\treturn cardTypesToFind.map((type) => {\n\t\t\t\t\tif (fnStr(m).includes(`featureIdentifier:\"${type}\"`)) {\n\t\t\t\t\t\tcardTypesToFind.splice(cardTypesToFind.indexOf(type), 1);\n\t\t\t\t\t\treturn [type[0].toUpperCase() + type.slice(1), m];\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t})\n\t\t\t.filter(Boolean),\n\t\t...modules\n\t\t\t.flatMap((m) => {\n\t\t\t\treturn cardTypesToFind.map((type) => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tif ((m?.type ? fnStr(m.type) : \"\").includes(`featureIdentifier:\"${type}\"`)) {\n\t\t\t\t\t\t\tcardTypesToFind.splice(cardTypesToFind.indexOf(type), 1);\n\t\t\t\t\t\t\treturn [type[0].toUpperCase() + type.slice(1), m];\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch {}\n\t\t\t\t});\n\t\t\t})\n\t\t\t.filter(Boolean),\n\t];\n\n\tconst _ScrollableContainer = (() => {\n\t\tconst SHOW_BUTTONS = { NEVER: \"never\", ALWAYS: \"always\", ON_HOVER: \"on-hover\" };\n\t\tconst SCROLLING_METHOD = { BY_RATIO: \"by-ratio\", SNAP: \"snap\" };\n\t\tconst EDGE_GRADIENTS = { NONE: \"none\", MASK: \"mask\", LINEAR_GRADIENT: \"linear-gradient\" };\n\t\tconst DIRECTION = { START: -1, END: 1 };\n\n\t\tconst CHEVRON_LEFT = '<path d=\"M11.521 1.38l-.65-.76L2.23 8l8.641 7.38.65-.76L3.77 8z\"/>';\n\t\tconst CHEVRON_RIGHT = '<path d=\"M5.129.62l-.65.76L12.231 8l-7.752 6.62.65.76L13.771 8z\"/>';\n\t\tlet stylesInjected = false;\n\n\t\tfunction injectStyles() {\n\t\t\tif (stylesInjected) return;\n\t\t\tstylesInjected = true;\n\t\t\tconst style = document.createElement(\"style\");\n\t\t\tstyle.className = \"spicetify-scrollable-container\";\n\t\t\tstyle.textContent = `\n.spicetify-sc-contentArea { overflow: hidden; position: relative; }\n.spicetify-sc-scroller { display: flex; align-items: center; overflow-x: auto; scrollbar-width: none; white-space: nowrap; width: 100%; -ms-overflow-style: none; overscroll-behavior-x: contain; will-change: transform; }\n@media (prefers-reduced-motion: no-preference) { .spicetify-sc-scroller { scroll-behavior: smooth; } }\n.spicetify-sc-scroller::-webkit-scrollbar { display: none; }\n.spicetify-sc-scroller.spicetify-sc-snap { scroll-snap-type: inline mandatory; }\n.spicetify-sc-scroller.spicetify-sc-snap .spicetify-sc-snapCenter [data-carousel-item] { scroll-snap-align: center; }\n.spicetify-sc-scroller.spicetify-sc-snap .spicetify-sc-snapStart [data-carousel-item] { scroll-snap-align: start; }\n.spicetify-sc-scroller.spicetify-sc-wheelEnabled { overscroll-behavior: contain; }\n.spicetify-sc-scroller.spicetify-sc-maskGradient { --sc-start-color: #000; --sc-end-color: #000; -webkit-mask-composite: source-in, xor; mask-composite: intersect; -webkit-mask-image: linear-gradient(90deg, var(--sc-start-color) 0, #000 120px), linear-gradient(90deg, #000 calc(100% - 120px), var(--sc-end-color) 100%); mask-image: linear-gradient(90deg, var(--sc-start-color) 0, #000 120px), linear-gradient(90deg, #000 calc(100% - 120px), var(--sc-end-color) 100%); -webkit-mask-size: 100% 100%; mask-size: 100% 100%; }\n.spicetify-sc-scroller.spicetify-sc-maskGradient.spicetify-sc-maskStart { --sc-start-color: transparent; }\n.spicetify-sc-scroller.spicetify-sc-maskGradient.spicetify-sc-maskEnd { --sc-end-color: transparent; }\n.spicetify-sc-linearGradient::before, .spicetify-sc-linearGradient::after { bottom: 0; content: \"\"; height: 100%; opacity: 0; pointer-events: none; position: absolute; top: 0; transition: opacity .15s ease-out; width: 120px; z-index: 2; }\n.spicetify-sc-linearGradient::before { background: linear-gradient(90deg, var(--carousel-start-chevron-gradient, var(--spice-main)) 0, transparent 100%); inset-inline-start: 0; }\n.spicetify-sc-linearGradient::after { background: linear-gradient(-90deg, var(--carousel-end-chevron-gradient, var(--spice-main)) 0, transparent 100%); inset-inline-end: 0; }\n.spicetify-sc-linearGradient.spicetify-sc-lgStart::before { opacity: 1; }\n.spicetify-sc-linearGradient.spicetify-sc-lgEnd::after { opacity: 1; }\n.spicetify-sc-carousel { bottom: 0; left: 0; position: absolute; right: 0; top: 0; justify-content: space-between; align-items: center; display: flex; pointer-events: none; }\n.spicetify-sc-chevronBtn { display: flex; border: none; border-radius: 50%; cursor: pointer; justify-content: center; align-items: center; backdrop-filter: var(--chevrons-button-backdrop-filter, none); background: transparent; background-color: var(--chevrons-button-color, var(--background-elevated-base)); height: 24px; opacity: 0; position: relative; transition: color .15s ease-out, opacity .15s ease-out, background-color .15s ease-out, translate .15s ease-out; translate: 0; width: 24px; z-index: 3; pointer-events: none; color: var(--text-base, #fff); }\n.spicetify-sc-chevronBtn > * { opacity: .7; z-index: 2; }\n.spicetify-sc-chevronBtn:hover { background-color: var(--chevrons-button-hover-color, var(--background-elevated-highlight)); }\n.spicetify-sc-chevronBtn:hover > * { opacity: 1; }\n.spicetify-sc-chevronBtn.spicetify-sc-chevronVisible { opacity: 1; pointer-events: auto; }\n.spicetify-sc-onHover .spicetify-sc-chevronBtn { opacity: 0; }\n.spicetify-sc-contentArea:hover .spicetify-sc-onHover .spicetify-sc-chevronBtn.spicetify-sc-chevronVisible { opacity: 1; }\n.spicetify-sc-contentArea:hover .spicetify-sc-onHover .spicetify-sc-chevronStart.spicetify-sc-chevronVisible { translate: 8px; }\n.spicetify-sc-contentArea:hover .spicetify-sc-onHover .spicetify-sc-chevronEnd.spicetify-sc-chevronVisible { translate: -8px; }\nbody[data-dragging-uri-type] .spicetify-sc-chevronBtn { pointer-events: none; }`;\n\t\t\tdocument.head.appendChild(style);\n\t\t}\n\n\t\tfunction useDragToScroll({ isDisabled = true } = {}) {\n\t\t\tconst { useRef, useCallback } = Spicetify.React;\n\t\t\tconst frameRef = useRef(0);\n\t\t\tconst savedBehavior = useRef(null);\n\t\t\tconst savedSnapType = useRef(null);\n\n\t\t\treturn useCallback(\n\t\t\t\tisDisabled\n\t\t\t\t\t? () => {}\n\t\t\t\t\t: ({ currentTarget, clientX }) => {\n\t\t\t\t\t\t\tif (!(currentTarget instanceof HTMLElement)) return;\n\t\t\t\t\t\t\tconst el = currentTarget;\n\n\t\t\t\t\t\t\tconst restore = () => {\n\t\t\t\t\t\t\t\tel.style.removeProperty(\"user-select\");\n\t\t\t\t\t\t\t\tif (savedBehavior.current !== null) el.style.scrollBehavior = savedBehavior.current;\n\t\t\t\t\t\t\t\tif (savedSnapType.current !== null) el.style.scrollSnapType = savedSnapType.current;\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\tconst fullCleanup = () => {\n\t\t\t\t\t\t\t\tcancelAnimationFrame(frameRef.current);\n\t\t\t\t\t\t\t\trestore();\n\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t\tfullCleanup();\n\t\t\t\t\t\t\tconst computed = window.getComputedStyle(el);\n\t\t\t\t\t\t\tsavedBehavior.current = computed.scrollBehavior;\n\t\t\t\t\t\t\tsavedSnapType.current = computed.scrollSnapType;\n\t\t\t\t\t\t\tel.style.userSelect = \"none\";\n\t\t\t\t\t\t\tel.style.scrollBehavior = \"auto\";\n\t\t\t\t\t\t\tel.style.scrollSnapType = \"none\";\n\n\t\t\t\t\t\t\tlet dragged = false;\n\t\t\t\t\t\t\tconst startScroll = el.scrollLeft;\n\t\t\t\t\t\t\tconst startX = clientX;\n\t\t\t\t\t\t\tlet velocity = 0;\n\n\t\t\t\t\t\t\tconst coast = () => {\n\t\t\t\t\t\t\t\tel.scrollLeft += velocity;\n\t\t\t\t\t\t\t\tvelocity *= 0.95;\n\t\t\t\t\t\t\t\tif (Math.abs(velocity) > 0.5) frameRef.current = requestAnimationFrame(coast);\n\t\t\t\t\t\t\t\telse fullCleanup();\n\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t\tconst onMove = (e) => {\n\t\t\t\t\t\t\t\tconst dx = e.clientX - startX;\n\t\t\t\t\t\t\t\tif (Math.abs(dx) > 10) dragged = true;\n\t\t\t\t\t\t\t\tconst prev = el.scrollLeft;\n\t\t\t\t\t\t\t\tel.scrollLeft = startScroll - dx;\n\t\t\t\t\t\t\t\tvelocity = el.scrollLeft - prev;\n\t\t\t\t\t\t\t};\n\n\t\t\t\t\t\t\tdocument.addEventListener(\"mousemove\", onMove);\n\t\t\t\t\t\t\tdocument.addEventListener(\n\t\t\t\t\t\t\t\t\"mouseup\",\n\t\t\t\t\t\t\t\t() => {\n\t\t\t\t\t\t\t\t\tif (dragged) {\n\t\t\t\t\t\t\t\t\t\tconst block = (e) => {\n\t\t\t\t\t\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\t\t\t\t\t\te.stopImmediatePropagation();\n\t\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t\t\tel.addEventListener(\"click\", block, { once: true, capture: true });\n\t\t\t\t\t\t\t\t\t\tsetTimeout(() => el.removeEventListener(\"click\", block, { capture: true }));\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tdocument.removeEventListener(\"mousemove\", onMove);\n\t\t\t\t\t\t\t\t\tcancelAnimationFrame(frameRef.current);\n\t\t\t\t\t\t\t\t\tframeRef.current = requestAnimationFrame(coast);\n\t\t\t\t\t\t\t\t\tdocument.addEventListener(\"wheel\", fullCleanup, { once: true });\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{ once: true }\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t},\n\t\t\t\t[isDisabled]\n\t\t\t);\n\t\t}\n\n\t\tfunction useWheelScroll(onlyHorizontalWheel) {\n\t\t\tconst { useRef, useCallback } = Spicetify.React;\n\t\t\tconst isFirst = useRef(true);\n\t\t\tconst savedBehavior = useRef(null);\n\t\t\tconst timer = useRef(null);\n\n\t\t\treturn useCallback(\n\t\t\t\t(e) => {\n\t\t\t\t\tif (!e.deltaY) return;\n\t\t\t\t\tif (onlyHorizontalWheel && Math.abs(e.deltaY) > Math.abs(e.deltaX)) return;\n\t\t\t\t\tconst el = e.currentTarget;\n\t\t\t\t\tif (isFirst.current) {\n\t\t\t\t\t\tisFirst.current = false;\n\t\t\t\t\t\tsavedBehavior.current = el.style.scrollBehavior;\n\t\t\t\t\t\tel.style.scrollBehavior = \"auto\";\n\t\t\t\t\t}\n\n\t\t\t\t\tel.scrollLeft += e.deltaY + e.deltaX;\n\t\t\t\t\tclearTimeout(timer.current);\n\t\t\t\t\ttimer.current = setTimeout(() => {\n\t\t\t\t\t\tisFirst.current = true;\n\t\t\t\t\t\tel.style.scrollBehavior = savedBehavior.current ?? \"\";\n\t\t\t\t\t}, 100);\n\t\t\t\t},\n\t\t\t\t[onlyHorizontalWheel]\n\t\t\t);\n\t\t}\n\n\t\tfunction useScrollState(scrollerRef, contentRef) {\n\t\t\tconst { useState, useCallback, useEffect } = Spicetify.React;\n\t\t\tconst [canGoStart, setCanGoStart] = useState(false);\n\t\t\tconst [canGoEnd, setCanGoEnd] = useState(false);\n\n\t\t\tconst update = useCallback(() => {\n\t\t\t\tconst el = scrollerRef.current;\n\t\t\t\tconst child = contentRef.current;\n\t\t\t\tif (!el || !child) return;\n\t\t\t\tconst maxScroll = el.scrollWidth - el.clientWidth;\n\t\t\t\tconst pos = Math.abs(el.scrollLeft);\n\t\t\t\tconst rounded = pos < 1 ? Math.floor(pos) : Math.ceil(pos);\n\t\t\t\tconst overflows = child.offsetWidth > el.clientWidth;\n\t\t\t\tsetCanGoStart(overflows && rounded !== 0);\n\t\t\t\tsetCanGoEnd(overflows && rounded < maxScroll);\n\t\t\t}, [scrollerRef, contentRef]);\n\n\t\t\tuseEffect(() => {\n\t\t\t\tconst el = scrollerRef.current;\n\t\t\t\tconst child = contentRef.current;\n\t\t\t\tif (!el || !child) return;\n\n\t\t\t\tupdate();\n\t\t\t\tel.addEventListener(\"scroll\", update);\n\t\t\t\tconst ro = new ResizeObserver(update);\n\t\t\t\tro.observe(el);\n\t\t\t\tro.observe(child);\n\t\t\t\treturn () => {\n\t\t\t\t\tel.removeEventListener(\"scroll\", update);\n\t\t\t\t\tro.disconnect();\n\t\t\t\t};\n\t\t\t}, [update, scrollerRef, contentRef]);\n\n\t\t\treturn { canGoStart, canGoEnd };\n\t\t}\n\n\t\tfunction ScrollableContainerComponent(props) {\n\t\t\tconst { useRef, useCallback, useMemo } = Spicetify.React;\n\t\t\tconst h = Spicetify.ReactJSX.jsx;\n\t\t\tconst hsf = Spicetify.ReactJSX.jsxs;\n\t\t\tconst cn = Spicetify.classnames;\n\n\t\t\tconst {\n\t\t\t\tchildren,\n\t\t\t\tclassName,\n\t\t\t\tchevronsClassName,\n\t\t\t\tshowButtons = SHOW_BUTTONS.ALWAYS,\n\t\t\t\tariaLabel,\n\t\t\t\tonlyHorizontalWheel = false,\n\t\t\t\twheelScrollEnabled = true,\n\t\t\t\tscrollContentClassName,\n\t\t\t\tscrollerClassName,\n\t\t\t\tscrollRatio = 0.9,\n\t\t\t\tscrollingMethod = SCROLLING_METHOD.BY_RATIO,\n\t\t\t\tscrollPadding,\n\t\t\t\tscrollSnapAlign,\n\t\t\t\tscrollSnapByItems = 1,\n\t\t\t\tedgeGradients = EDGE_GRADIENTS.MASK,\n\t\t\t\tdragToScrollOptions = { isDisabled: true },\n\t\t\t\tonScroll,\n\t\t\t\tactiveElementThreshold = 10,\n\t\t\t\tonNavigationClick,\n\t\t\t\trole = \"list\",\n\t\t\t} = props;\n\n\t\t\tinjectStyles();\n\n\t\t\tconst scrollerRef = useRef(null);\n\t\t\tconst contentRef = useRef(null);\n\t\t\tconst lastIndex = useRef(-1);\n\n\t\t\tconst { canGoStart, canGoEnd } = useScrollState(scrollerRef, contentRef);\n\t\t\tconst dragHandler = useDragToScroll(dragToScrollOptions);\n\t\t\tconst wheelHandler = useWheelScroll(onlyHorizontalWheel);\n\t\t\tconst isRtl = useMemo(() => document.documentElement.dir === \"rtl\", []);\n\n\t\t\tconst getActiveIndex = useCallback(() => {\n\t\t\t\tconst scrollPos = Math.abs(scrollerRef.current?.scrollLeft ?? 0);\n\t\t\t\tlet index = -1;\n\t\t\t\tif (contentRef.current?.children) {\n\t\t\t\t\tlet idx = -1;\n\t\t\t\t\tfor (const child of contentRef.current.children) {\n\t\t\t\t\t\tif (child instanceof HTMLElement) {\n\t\t\t\t\t\t\tidx++;\n\t\t\t\t\t\t\tif (Math.abs(child.offsetLeft - scrollPos) <= child.offsetWidth / activeElementThreshold) index = idx;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn index;\n\t\t\t}, [activeElementThreshold]);\n\n\t\t\tconst fireScroll = useCallback(() => {\n\t\t\t\tif (!onScroll) return;\n\t\t\t\tconst index = getActiveIndex();\n\t\t\t\tif (lastIndex.current !== index) {\n\t\t\t\t\tlastIndex.current = index;\n\t\t\t\t\tonScroll(index);\n\t\t\t\t}\n\t\t\t}, [getActiveIndex, onScroll]);\n\n\t\t\tconst navigate = useCallback(\n\t\t\t\t(direction) => {\n\t\t\t\t\tif (!scrollerRef.current) return;\n\t\t\t\t\tconst dir = isRtl ? -1 : 1;\n\n\t\t\t\t\tif (scrollingMethod === SCROLLING_METHOD.SNAP) {\n\t\t\t\t\t\tconst item = contentRef.current?.querySelector(\"[data-carousel-item]\");\n\t\t\t\t\t\tif (!item) return;\n\t\t\t\t\t\tscrollerRef.current.scrollBy({ left: dir * scrollSnapByItems * item.getBoundingClientRect().width * direction });\n\t\t\t\t\t} else scrollerRef.current.scrollBy({ left: dir * direction * scrollerRef.current.clientWidth * scrollRatio });\n\n\t\t\t\t\tfireScroll();\n\t\t\t\t\tonNavigationClick?.(direction);\n\t\t\t\t},\n\t\t\t\t[scrollingMethod, fireScroll, isRtl, scrollSnapByItems, scrollRatio, onNavigationClick]\n\t\t\t);\n\n\t\t\tconst isSnap = scrollingMethod === SCROLLING_METHOD.SNAP;\n\t\t\tconst isMask = edgeGradients === EDGE_GRADIENTS.MASK;\n\t\t\tconst isLinearGradient = edgeGradients === EDGE_GRADIENTS.LINEAR_GRADIENT;\n\n\t\t\tconst makeChevron = (svgPath, position, visible, dir) =>\n\t\t\t\th(\"div\", {\n\t\t\t\t\tclassName: cn(\"spicetify-sc-chevronBtn\", `spicetify-sc-chevron${position}`, { \"spicetify-sc-chevronVisible\": visible }),\n\t\t\t\t\tonClick: (e) => {\n\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\te.stopPropagation();\n\t\t\t\t\t\tnavigate(dir);\n\t\t\t\t\t},\n\t\t\t\t\t\"aria-hidden\": \"true\",\n\t\t\t\t\tchildren: h(\"svg\", { height: 12, width: 12, viewBox: \"0 0 16 16\", fill: \"currentColor\", dangerouslySetInnerHTML: { __html: svgPath } }),\n\t\t\t\t});\n\n\t\t\treturn hsf(\"div\", {\n\t\t\t\tclassName: cn(\"spicetify-sc-contentArea\", className, {\n\t\t\t\t\t\"spicetify-sc-linearGradient\": isLinearGradient,\n\t\t\t\t\t\"spicetify-sc-lgStart\": isLinearGradient && canGoStart,\n\t\t\t\t\t\"spicetify-sc-lgEnd\": isLinearGradient && canGoEnd,\n\t\t\t\t}),\n\t\t\t\tchildren: [\n\t\t\t\t\th(\"div\", {\n\t\t\t\t\t\tref: scrollerRef,\n\t\t\t\t\t\tclassName: cn(\"spicetify-sc-scroller\", scrollerClassName, {\n\t\t\t\t\t\t\t\"spicetify-sc-snap\": isSnap,\n\t\t\t\t\t\t\t\"spicetify-sc-maskGradient\": isMask,\n\t\t\t\t\t\t\t\"spicetify-sc-wheelEnabled\": wheelScrollEnabled,\n\t\t\t\t\t\t\t\"spicetify-sc-maskStart\": isMask && canGoStart,\n\t\t\t\t\t\t\t\"spicetify-sc-maskEnd\": isMask && canGoEnd,\n\t\t\t\t\t\t}),\n\t\t\t\t\t\tonScroll: onScroll ? fireScroll : undefined,\n\t\t\t\t\t\tonMouseDownCapture: dragHandler,\n\t\t\t\t\t\tonWheel: wheelScrollEnabled ? wheelHandler : undefined,\n\t\t\t\t\t\trole,\n\t\t\t\t\t\t\"aria-label\": ariaLabel,\n\t\t\t\t\t\tstyle: isSnap ? { scrollPadding } : undefined,\n\t\t\t\t\t\tchildren: h(\"div\", {\n\t\t\t\t\t\t\tref: contentRef,\n\t\t\t\t\t\t\trole: \"presentation\",\n\t\t\t\t\t\t\tclassName: cn(scrollContentClassName, {\n\t\t\t\t\t\t\t\t\"spicetify-sc-snapStart\": scrollSnapAlign === \"start\",\n\t\t\t\t\t\t\t\t\"spicetify-sc-snapCenter\": scrollSnapAlign === \"center\",\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\tchildren,\n\t\t\t\t\t\t}),\n\t\t\t\t\t}),\n\t\t\t\t\tshowButtons !== SHOW_BUTTONS.NEVER &&\n\t\t\t\t\t\thsf(\"div\", {\n\t\t\t\t\t\t\tclassName: cn(\"spicetify-sc-carousel\", chevronsClassName, {\n\t\t\t\t\t\t\t\t\"spicetify-sc-onHover\": showButtons === SHOW_BUTTONS.ON_HOVER,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\tchildren: [makeChevron(CHEVRON_LEFT, \"Start\", canGoStart, DIRECTION.START), makeChevron(CHEVRON_RIGHT, \"End\", canGoEnd, DIRECTION.END)],\n\t\t\t\t\t\t}),\n\t\t\t\t],\n\t\t\t});\n\t\t}\n\n\t\tScrollableContainerComponent.SHOW_BUTTONS = SHOW_BUTTONS;\n\t\tScrollableContainerComponent.SCROLLING_METHOD = SCROLLING_METHOD;\n\t\tScrollableContainerComponent.EDGE_GRADIENTS = EDGE_GRADIENTS;\n\t\tScrollableContainerComponent.DIRECTION = DIRECTION;\n\n\t\treturn ScrollableContainerComponent;\n\t})();\n\n\tObject.assign(Spicetify, {\n\t\tReact: cache.find((m) => m?.useMemo),\n\t\tReactJSX: cache.find((m) => m?.jsx),\n\t\tReactDOM: cache.find((m) => m?.createPortal),\n\t\tReactDOMServer: cache.find((m) => m?.renderToString),\n\t\t// https://github.com/JedWatson/classnames/\n\t\tclassnames: chunks\n\t\t\t.filter(([_, v]) => v.toString().includes(\"[native code]\"))\n\t\t\t.map(([i]) => require(i))\n\t\t\t.find((e) => typeof e === \"function\"),\n\t\tColor: functionModules.find((m) => fnStr(m).includes(\"static fromHex\") || fnStr(m).includes(\"this.rgb\")),\n\t\tPlayer: {\n\t\t\t...Spicetify.Player,\n\t\t\tget origin() {\n\t\t\t\treturn Spicetify.Platform?.PlayerAPI;\n\t\t\t},\n\t\t},\n\t\tGraphQL: {\n\t\t\t...Spicetify.GraphQL,\n\t\t\tget Request() {\n\t\t\t\treturn Spicetify.Platform?.GraphQLLoader || Spicetify.GraphQL.Handler?.(Spicetify.GraphQL.Context);\n\t\t\t},\n\t\t\tContext: functionModules.find((m) => fnStr(m).includes(\"subscription\") && fnStr(m).includes(\"mutation\")),\n\t\t\tHandler: functionModules.find((m) => fnStr(m).includes(\"GraphQL subscriptions are not supported\")),\n\t\t},\n\t\tReactComponent: {\n\t\t\t...Spicetify.ReactComponent,\n\t\t\tTextComponent: modules.find((m) => m?.h1 && m?.render),\n\t\t\tMenu: functionModules.find((m) => fnStr(m).includes(\"getInitialFocusElement\") && fnStr(m).includes(\"children\")),\n\t\t\tMenuItem: functionModules.find((m) => fnStr(m).includes(\"handleMouseEnter\") && fnStr(m).includes(\"onClick\")),\n\t\t\tMenuSubMenuItem: functionModules.find((f) => fnStr(f).includes(\"subMenuIcon\")),\n\t\t\tSlider: wrapProvider(functionModules.find((m) => fnStr(m).includes(\"progressBarRef\"))),\n\t\t\tRemoteConfigProvider: functionModules.find((m) => fnStr(m).includes(\"resolveSuspense\") && fnStr(m).includes(\"configuration\")),\n\t\t\tRightClickMenu: functionModules.find(\n\t\t\t\t(m) => fnStr(m).includes(\"action\") && fnStr(m).includes(\"open\") && fnStr(m).includes(\"trigger\") && fnStr(m).includes(\"right-click\")\n\t\t\t),\n\t\t\tTooltipWrapper: functionModules.find((m) => fnStr(m).includes(\"renderInline\") && fnStr(m).includes(\"showDelay\")),\n\t\t\tButtonPrimary: reactComponentsUI.ButtonPrimary,\n\t\t\tButtonSecondary: reactComponentsUI.ButtonSecondary,\n\t\t\tButtonTertiary: reactComponentsUI.ButtonTertiary,\n\t\t\tSnackbar: {\n\t\t\t\twrapper: functionModules.find((m) => fnStr(m).includes(\"encore-light-theme\") && fnStr(m).includes(\"elevated\")),\n\t\t\t\tsimpleLayout: functionModules.find((m) => [\"leading\", \"center\", \"trailing\"].every((keyword) => fnStr(m).includes(keyword))),\n\t\t\t\tctaText: functionModules.find((m) => fnStr(m).includes(\"ctaText\")),\n\t\t\t\tstyledImage: functionModules.find((m) => fnStr(m).includes(\"placeholderSrc\")),\n\t\t\t},\n\t\t\tChip: reactComponentsUI.Chip,\n\t\t\tToggle: functionModules.find((m) => fnStr(m).includes(\"onSelected\") && fnStr(m).includes('type:\"checkbox\"')),\n\t\t\tCards: {\n\t\t\t\tDefault: reactComponentsUI.Card,\n\t\t\t\tFeatureCard: functionModules.find(\n\t\t\t\t\t(m) => fnStr(m).includes(\"?highlight\") && fnStr(m).includes(\"headerText\") && fnStr(m).includes(\"imageContainer\")\n\t\t\t\t),\n\t\t\t\tHero: functionModules.find((m) => fnStr(m).includes('\"herocard-click-handler\"')),\n\t\t\t\tCardImage: functionModules.find(\n\t\t\t\t\t(m) =>\n\t\t\t\t\t\tfnStr(m).includes(\"isHero\") && (fnStr(m).includes(\"withWaves\") || fnStr(m).includes(\"isCircular\")) && fnStr(m).includes(\"imageWrapper\")\n\t\t\t\t),\n\t\t\t\t...Object.fromEntries(cards),\n\t\t\t},\n\t\t\tRouter: functionModules.find((m) => fnStr(m).includes(\"navigationType\") && fnStr(m).includes(\"static\")),\n\t\t\tRoutes: functionModules.find((m) => fnStr(m).match(/\\([\\w$]+\\)\\{let\\{children:[\\w$]+,location:[\\w$]+\\}=[\\w$]+/)),\n\t\t\tRoute: functionModules.find((m) => fnStr(m).match(/^function [\\w$]+\\([\\w$]+\\)\\{\\(0,[\\w$]+\\.[\\w$]+\\)\\(!1\\)\\}$/)),\n\t\t\tStoreProvider: functionModules.find((m) => fnStr(m).includes(\"notifyNestedSubs\") && fnStr(m).includes(\"serverState\")),\n\t\t\tScrollableContainer: _ScrollableContainer,\n\t\t\tIconComponent: reactComponentsUI.Icon,\n\t\t\t...Object.fromEntries(menus),\n\t\t},\n\t\tReactHook: {\n\t\t\tDragHandler: functionModules.find((m) => fnStr(m).includes(\"dataTransfer\") && fnStr(m).includes(\"data-dragging\")),\n\t\t\tuseExtractedColor: functionModules.find(\n\t\t\t\t(m) => fnStr(m).includes(\"extracted-color\") || (fnStr(m).includes(\"colorRaw\") && fnStr(m).includes(\"useEffect\"))\n\t\t\t),\n\t\t},\n\t\t// React Query\n\t\t// https://github.com/TanStack/query\n\t\t// v3 until Spotify v1.2.29\n\t\t// v5 since Spotify v1.2.30\n\t\tReactQuery: cache.find((module) => module.useQuery) || {\n\t\t\tPersistQueryClientProvider: functionModules.find((m) => fnStr(m).includes(\"persistOptions\")),\n\t\t\tQueryClient: functionModules.find((m) => fnStr(m).includes(\"defaultMutationOptions\")),\n\t\t\tQueryClientProvider: functionModules.find((m) => fnStr(m).includes(\"use QueryClientProvider\")),\n\t\t\tnotifyManager: modules.find((m) => m?.setBatchNotifyFunction),\n\t\t\tuseMutation: functionModules.find((m) => fnStr(m).includes(\"mutateAsync\")),\n\t\t\tuseQuery: functionModules.find((m) =>\n\t\t\t\tfnStr(m).match(/^function [\\w_$]+\\(([\\w_$]+),([\\w_$]+)\\)\\{return\\(0,[\\w_$]+\\.[\\w_$]+\\)\\(\\1,[\\w_$]+\\.[\\w_$]+,\\2\\)\\}$/)\n\t\t\t),\n\t\t\tuseQueryClient: functionModules.find((m) => fnStr(m).includes(\"client\") && fnStr(m).includes(\"Provider\") && fnStr(m).includes(\"mount\")),\n\t\t\tuseSuspenseQuery: functionModules.find(\n\t\t\t\t(m) => fnStr(m).includes(\"throwOnError\") && fnStr(m).includes(\"suspense\") && fnStr(m).includes(\"enabled\")\n\t\t\t),\n\t\t},\n\t\tReactFlipToolkit: {\n\t\t\t...Spicetify.ReactFlipToolkit,\n\t\t\tFlipper: functionModules.find((m) => m?.prototype?.getSnapshotBeforeUpdate),\n\t\t\tFlipped: functionModules.find((m) => m.displayName === \"Flipped\"),\n\t\t},\n\t\t_reservedPanelIds: modules.find((m) => m?.BuddyFeed),\n\t\tMousetrap: cache.find((m) => m?.addKeycodes),\n\t\tLocale: modules.find((m) => m?._dictionary),\n\t});\n\n\tif (!Spicetify.ContextMenuV2._context) Spicetify.ContextMenuV2._context = Spicetify.React.createContext({});\n\tif (!Spicetify.ReactComponent.Navigation)\n\t\tSpicetify.ReactComponent.Navigation = exportedMemoFRefs.find((m) => fnStr(m.type.render).includes(\"navigationalRoot\"));\n\n\t(function waitForChunks() {\n\t\tconst listOfComponents = [\n\t\t\t\"Slider\",\n\t\t\t\"Dropdown\",\n\t\t\t\"Toggle\",\n\t\t\t// \"Cards.Artist\",\n\t\t\t// \"Cards.Audiobook\",\n\t\t\t// \"Cards.Profile\",\n\t\t\t// \"Cards.Show\",\n\t\t\t// \"Cards.Track\",\n\t\t];\n\t\tif (listOfComponents.every((component) => component.split(\".\").reduce((o, k) => o?.[k], Spicetify.ReactComponent) !== undefined)) return;\n\t\tconst cache = Object.keys(require.m).map((id) => require(id));\n\t\tconst modules = cache\n\t\t\t.filter((module) => typeof module === \"object\")\n\t\t\t.flatMap((module) => {\n\t\t\t\ttry {\n\t\t\t\t\treturn Object.values(module);\n\t\t\t\t} catch {}\n\t\t\t});\n\t\tconst functionModules = modules.flatMap((module) =>\n\t\t\ttypeof module === \"function\"\n\t\t\t\t? [module]\n\t\t\t\t: typeof module === \"object\" && module\n\t\t\t\t\t? Object.values(module).filter((v) => typeof v === \"function\" && !webpackFactories.has(v))\n\t\t\t\t\t: []\n\t\t);\n\t\tconst exportedMemos = modules.filter((m) => m?.$$typeof === Symbol.for(\"react.memo\"));\n\t\tconst cardTypesToFind = [\"artist\", \"audiobook\", \"profile\", \"show\", \"track\"];\n\t\t// const cards = [\n\t\t// \t...functionModules\n\t\t// \t\t.flatMap((m) => {\n\t\t// \t\t\treturn cardTypesToFind.map((type) => {\n\t\t// \t\t\t\tif (m.toString().includes(`featureIdentifier:\"${type}\"`)) {\n\t\t// \t\t\t\t\tcardTypesToFind.splice(cardTypesToFind.indexOf(type), 1);\n\t\t// \t\t\t\t\treturn [type[0].toUpperCase() + type.slice(1), m];\n\t\t// \t\t\t\t}\n\t\t// \t\t\t});\n\t\t// \t\t})\n\t\t// \t\t.filter(Boolean),\n\t\t// \t...modules\n\t\t// \t\t.flatMap((m) => {\n\t\t// \t\t\treturn cardTypesToFind.map((type) => {\n\t\t// \t\t\t\ttry {\n\t\t// \t\t\t\t\tif (m?.type?.toString().includes(`featureIdentifier:\"${type}\"`)) {\n\t\t// \t\t\t\t\t\tcardTypesToFind.splice(cardTypesToFind.indexOf(type), 1);\n\t\t// \t\t\t\t\t\treturn [type[0].toUpperCase() + type.slice(1), m];\n\t\t// \t\t\t\t\t}\n\t\t// \t\t\t\t} catch {}\n\t\t// \t\t\t});\n\t\t// \t\t})\n\t\t// \t\t.filter(Boolean),\n\t\t// ];\n\n\t\tSpicetify.ReactComponent.Slider = wrapProvider(functionModules.find((m) => fnStr(m).includes(\"progressBarRef\")));\n\t\tSpicetify.ReactComponent.Toggle = functionModules.find((m) => fnStr(m).includes(\"onSelected\") && fnStr(m).includes('type:\"checkbox\"'));\n\t\t// Object.assign(Spicetify.ReactComponent.Cards, Object.fromEntries(cards));\n\n\t\t// chunks\n\t\tconst dropdownChunk = chunks.find(([, value]) => fnStr(value).includes(\"dropDown\") && fnStr(value).includes(\"isSafari\"));\n\t\tif (dropdownChunk) {\n\t\t\tSpicetify.ReactComponent.Dropdown =\n\t\t\t\tObject.values(require(dropdownChunk[0]))?.[0]?.render ?? Object.values(require(dropdownChunk[0])).find((m) => typeof m === \"function\");\n\t\t}\n\n\t\tconst toggleChunk = chunks.find(([, value]) => fnStr(value).includes(\"onSelected\") && fnStr(value).includes('type:\"checkbox\"'));\n\t\tif (toggleChunk && !Spicetify.ReactComponent.Toggle) {\n\t\t\tSpicetify.ReactComponent.Toggle = Object.values(require(toggleChunk[0]))[0].render;\n\t\t}\n\n\t\tif (!listOfComponents.every((component) => component.split(\".\").reduce((o, k) => o?.[k], Spicetify.ReactComponent) !== undefined)) {\n\t\t\tsetTimeout(waitForChunks, 100);\n\t\t\treturn;\n\t\t}\n\n\t\tif (Spicetify.ReactComponent.ScrollableContainer) setTimeout(refreshNavLinks, 100);\n\t})();\n\n\t(function waitForSnackbar() {\n\t\tif (!Object.keys(Spicetify.Snackbar).length) {\n\t\t\tsetTimeout(waitForSnackbar, 100);\n\t\t\treturn;\n\t\t}\n\t\t// Snackbar notifications\n\t\t// https://github.com/iamhosseindhv/notistack\n\t\tSpicetify.Snackbar = {\n\t\t\t...Spicetify.Snackbar,\n\t\t\tSnackbarProvider: functionModules.find((m) => fnStr(m).includes(\"enqueueSnackbar called with invalid argument\")),\n\t\t\tuseSnackbar: functionModules.find((m) => fnStr(m).match(/^function\\(\\)\\{return\\(0,[\\w$]+\\.useContext\\)\\([\\w$]+\\)\\}$/)),\n\t\t};\n\t})();\n\n\tconst localeModule = modules.find((m) => m?.getTranslations);\n\tif (localeModule) {\n\t\tconst createUrlLocale = functionModules.find((m) => fnStr(m).includes(\"has\") && fnStr(m).includes(\"baseName\") && fnStr(m).includes(\"language\"));\n\t\tSpicetify.Locale = {\n\t\t\tget _relativeTimeFormat() {\n\t\t\t\treturn localeModule._relativeTimeFormat;\n\t\t\t},\n\t\t\tget _dateTimeFormats() {\n\t\t\t\treturn localeModule._dateTimeFormats;\n\t\t\t},\n\t\t\tget _locale() {\n\t\t\t\treturn localeModule._localeForTranslation.baseName;\n\t\t\t},\n\t\t\tget _urlLocale() {\n\t\t\t\treturn localeModule._localeForURLPath;\n\t\t\t},\n\t\t\tget _dictionary() {\n\t\t\t\treturn localeModule._translations;\n\t\t\t},\n\t\t\tformatDate: (date, options) => localeModule.formatDate(date, options),\n\t\t\tformatRelativeTime: (date, options) => localeModule.formatRelativeDate(date, options),\n\t\t\tformatNumber: (number, options) => localeModule.formatNumber(number, options),\n\t\t\tformatNumberCompact: (number, options) => localeModule.formatNumberCompact(number, options),\n\t\t\tget: (key, children) => localeModule.get(key, children),\n\t\t\tgetDateTimeFormat: (options) => localeModule.getDateTimeFormat(options),\n\t\t\tgetDictionary: () => localeModule.getTranslations(),\n\t\t\tgetLocale: () => localeModule._localeForTranslation.baseName,\n\t\t\tgetSmartlingLocale: () => localeModule.getLocaleForSmartling(),\n\t\t\tgetUrlLocale: () => localeModule.getLocaleForURLPath(),\n\t\t\tgetRelativeTimeFormat: () => localeModule.getRelativeTimeFormat(),\n\t\t\tgetSeparator: () => localeModule.getSeparator(),\n\t\t\tsetLocale: (locale) => {\n\t\t\t\treturn localeModule.initialize({\n\t\t\t\t\tlocaleForTranslation: locale,\n\t\t\t\t\tlocaleForFormatting: localeModule._localeForFormatting.baseName,\n\t\t\t\t\ttranslations: localeModule._translations,\n\t\t\t\t});\n\t\t\t},\n\t\t\tsetUrlLocale: (locale) => {\n\t\t\t\tif (createUrlLocale) localeModule._localeForURLPath = createUrlLocale(locale);\n\t\t\t},\n\t\t\tsetDictionary: (dictionary) => {\n\t\t\t\treturn localeModule.initialize({\n\t\t\t\t\tlocaleForTranslation: localeModule._localeForTranslation.baseName,\n\t\t\t\t\tlocaleForFormatting: localeModule._localeForFormatting.baseName,\n\t\t\t\t\ttranslations: dictionary,\n\t\t\t\t});\n\t\t\t},\n\t\t\ttoLocaleLowerCase: (text) => localeModule.toLocaleLowerCase(text),\n\t\t\ttoLocaleUpperCase: (text) => localeModule.toLocaleUpperCase(text),\n\t\t};\n\t}\n\n\tif (Spicetify.Locale) Spicetify.Locale._supportedLocales = cache.find((m) => typeof m?.ja === \"string\");\n\n\tObject.defineProperty(Spicetify, \"Queue\", {\n\t\tget() {\n\t\t\treturn Spicetify.Player.origin?._queue?._state ?? Spicetify.Player.origin?._queue?._queue;\n\t\t},\n\t});\n\n\tconst confirmDialogChunk = chunks.find(\n\t\t([, value]) =>\n\t\t\tvalue.toString().includes(\"main-confirmDialog-container\") ||\n\t\t\t(value.toString().includes(\"confirmDialog\") && value.toString().includes(\"shouldCloseOnEsc\") && value.toString().includes(\"isOpen\"))\n\t);\n\tif (!Spicetify.ReactComponent?.ConfirmDialog && confirmDialogChunk) {\n\t\tSpicetify.ReactComponent.ConfirmDialog = Object.values(require(confirmDialogChunk[0])).find((m) => typeof m === \"object\");\n\t} else {\n\t\tSpicetify.ReactComponent.ConfirmDialog = functionModules.find(\n\t\t\t(m) => fnStr(m).includes(\"isOpen\") && fnStr(m).includes(\"shouldCloseOnEsc\") && fnStr(m).includes(\"onClose\")\n\t\t);\n\t}\n\n\tconst contextMenuChunk = chunks.find(([, value]) => value.toString().includes(\"handleContextMenu\"));\n\tif (contextMenuChunk) {\n\t\tSpicetify.ReactComponent.ContextMenu = Object.values(require(contextMenuChunk[0])).find((m) => typeof m === \"function\");\n\t}\n\n\tconst playlistMenuChunk = chunks.find(\n\t\t([, value]) => value.toString().includes('value:\"playlist\"') && value.toString().includes(\"canView\") && value.toString().includes(\"permissions\")\n\t);\n\tif (playlistMenuChunk && !Spicetify.ReactComponent?.PlaylistMenu) {\n\t\tSpicetify.ReactComponent.PlaylistMenu = Object.values(require(playlistMenuChunk[0])).find(\n\t\t\t(m) => typeof m === \"function\" || typeof m === \"object\"\n\t\t);\n\t}\n\n\tconst infiniteQueryChunk = chunks.find(\n\t\t([_, value]) => value.toString().includes(\"fetchPreviousPage\") && value.toString().includes(\"getOptimisticResult\")\n\t);\n\tif (infiniteQueryChunk) {\n\t\tSpicetify.ReactQuery.useInfiniteQuery = Object.values(require(infiniteQueryChunk[0])).find((m) => typeof m === \"function\");\n\t}\n\n\tif (Spicetify.Color) Spicetify.Color.CSSFormat = modules.find((m) => m?.RGBA);\n\n\t// Combine snackbar and notification\n\t(function bindShowNotification() {\n\t\tif (!Spicetify.Snackbar?.enqueueSnackbar && !Spicetify.showNotification) {\n\t\t\tsetTimeout(bindShowNotification, 250);\n\t\t\treturn;\n\t\t}\n\n\t\tif (Spicetify.Snackbar?.enqueueSnackbar) {\n\t\t\tSpicetify.showNotification = (message, isError, msTimeout) => {\n\t\t\t\tSpicetify.Snackbar.enqueueSnackbar(message, {\n\t\t\t\t\tvariant: isError ? \"error\" : \"default\",\n\t\t\t\t\tautoHideDuration: msTimeout,\n\t\t\t\t});\n\t\t\t};\n\n\t\t\treturn;\n\t\t}\n\n\t\tSpicetify.Snackbar.enqueueSnackbar = (message, { variant = \"default\", autoHideDuration } = {}) => {\n\t\t\tisError = variant === \"error\";\n\t\t\tSpicetify.showNotification(message, isError, autoHideDuration);\n\t\t};\n\t})();\n\n\t// Image color extractor\n\t(async function bindColorExtractor() {\n\t\tif (!Spicetify.GraphQL.Request) {\n\t\t\tsetTimeout(bindColorExtractor, 10);\n\t\t\treturn;\n\t\t}\n\t\tlet imageAnalysis = functionModules.find((m) => m.toString().match(/![\\w$]+\\.isFallback|\\{extractColor/g));\n\t\tconst fallbackPreset = modules.find((m) => m?.colorDark);\n\n\t\t// Search chunk in Spotify 1.2.13 or much older because it is impossible to find any distinguishing features\n\t\tif (!imageAnalysis) {\n\t\t\tlet chunk = chunks.find(\n\t\t\t\t([, value]) =>\n\t\t\t\t\t(value.toString().match(/[\\w$]+\\.isFallback/g) || value.toString().includes(\"colorRaw:\")) && value.toString().match(/.extractColor/g)\n\t\t\t);\n\t\t\tif (!chunk) {\n\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, 100));\n\t\t\t\tchunk = chunks.find(([, value]) => value.toString().match(/[\\w$]+\\.isFallback/g) && value.toString().match(/.extractColor/g));\n\t\t\t}\n\t\t\timageAnalysis = Object.values(require(chunk[0])).find((m) => typeof m === \"function\");\n\t\t}\n\n\t\tSpicetify.extractColorPreset = async (image) => {\n\t\t\tconst analysis = await imageAnalysis(Spicetify.GraphQL.Request, image);\n\t\t\tfor (const result of analysis) {\n\t\t\t\tif (\"isFallback\" in result === false) result.isFallback = fallbackPreset === result;\n\t\t\t}\n\n\t\t\treturn analysis;\n\t\t};\n\t})();\n\n\tfunction wrapProvider(component) {\n\t\tif (!component) return null;\n\t\treturn (props) =>\n\t\t\tSpicetify.React.createElement(\n\t\t\t\tSpicetify.ReactComponent.RemoteConfigProvider,\n\t\t\t\t{ configuration: Spicetify.Platform.RemoteConfiguration },\n\t\t\t\tSpicetify.React.createElement(component, props)\n\t\t\t);\n\t}\n\n\t(function waitForURI() {\n\t\tif (!Spicetify.URI) {\n\t\t\tsetTimeout(waitForURI, 10);\n\t\t\treturn;\n\t\t}\n\n\t\t// Ignore on versions older than 1.2.4\n\t\tif (Spicetify.URI.Type) return;\n\n\t\tconst URIChunk = cache\n\t\t\t.filter((module) => typeof module === \"object\")\n\t\t\t.find((m) => {\n\t\t\t\t// Avoid creating 2 arrays of the same values\n\t\t\t\ttry {\n\t\t\t\t\tconst values = Object.values(m);\n\t\t\t\t\treturn values.some((m) => typeof m === \"function\") && values.some((m) => m?.PLAYLIST_V2);\n\t\t\t\t} catch {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t});\n\t\tconst URIModules = Object.values(URIChunk);\n\n\t\t// URI.Type\n\t\tSpicetify.URI.Type = URIModules.find((m) => m?.PLAYLIST_V2);\n\n\t\t// Parse functions\n\t\tSpicetify.URI.from = URIModules.find((m) => typeof m === \"function\" && m.toString().includes(\"allowedTypes\"));\n\t\tSpicetify.URI.fromString = URIModules.find((m) => typeof m === \"function\" && m.toString().includes(\"Argument `uri`\"));\n\n\t\t// createURI functions\n\t\tconst createURIFunctions = URIModules.filter((m) => typeof m === \"function\" && m.toString().match(/\\([\\w$]+\\./));\n\t\tfor (const type of Object.keys(Spicetify.URI.Type)) {\n\t\t\tconst func = createURIFunctions.find((m) => m.toString().match(new RegExp(`\\\\([\\\\w$]+\\\\.${type}(?!_)`)));\n\t\t\tif (!func) continue;\n\n\t\t\tconst camelCaseType = type\n\t\t\t\t.toLowerCase()\n\t\t\t\t.split(\"_\")\n\t\t\t\t.map((word, index) => {\n\t\t\t\t\tif (index === 0) return word;\n\t\t\t\t\treturn word[0].toUpperCase() + word.slice(1);\n\t\t\t\t})\n\t\t\t\t.join(\"\");\n\t\t\tSpicetify.URI[`${camelCaseType}URI`] = func;\n\t\t}\n\n\t\t// isURI functions\n\t\tconst isURIFUnctions = URIModules.filter((m) => typeof m === \"function\" && m.toString().match(/=[\\w$]+\\./));\n\t\tfor (const type of Object.keys(Spicetify.URI.Type)) {\n\t\t\tconst func = isURIFUnctions.find((m) => m.toString().match(new RegExp(`===[\\\\w$]+\\\\.${type}(?!_)\\\\}`)));\n\t\t\tconst camelCaseType = type\n\t\t\t\t.toLowerCase()\n\t\t\t\t.split(\"_\")\n\t\t\t\t.map((word) => word[0].toUpperCase() + word.slice(1))\n\t\t\t\t.join(\"\");\n\n\t\t\t// Fill in missing functions, only serves as placebo as they cannot be as accurate as the original functions\n\t\t\tSpicetify.URI[`is${camelCaseType}`] =\n\t\t\t\tfunc ??\n\t\t\t\t((uri) => {\n\t\t\t\t\tlet uriObj;\n\t\t\t\t\ttry {\n\t\t\t\t\t\turiObj = Spicetify.URI.from?.(uri) ?? Spicetify.URI.fromString?.(uri);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\tif (!uriObj) return false;\n\t\t\t\t\treturn uriObj.type === Spicetify.URI.Type[type];\n\t\t\t\t});\n\t\t}\n\n\t\tSpicetify.URI.isPlaylistV1OrV2 = (uri) => Spicetify.URI.isPlaylist(uri) || Spicetify.URI.isPlaylistV2(uri);\n\n\t\t// Conversion functions\n\t\tSpicetify.URI.idToHex = URIModules.find((m) => typeof m === \"function\" && m.toString().includes(\"22===\"));\n\t\tSpicetify.URI.hexToId = URIModules.find((m) => typeof m === \"function\" && m.toString().includes(\"32===\"));\n\n\t\t// isSameIdentity\n\t\tSpicetify.URI.isSameIdentity = URIModules.find((m) => typeof m === \"function\" && m.toString().match(/[\\w$]+\\.id===[\\w$]+\\.id/));\n\t})();\n\n\tSpicetify.Events.webpackLoaded.fire();\n\trefreshNavLinks?.();\n})();\n\nSpicetify.Events = (() => {\n\tclass Event {\n\t\tcallbacks = [];\n\t\ton(callback) {\n\t\t\tif (!this.callbacks) return void callback();\n\t\t\tthis.callbacks.push(callback);\n\t\t}\n\t\tfire() {\n\t\t\tfor (const callback of this.callbacks) callback();\n\t\t\tthis.callbacks = undefined;\n\t\t}\n\t}\n\n\treturn { webpackLoaded: new Event(), platformLoaded: new Event() };\n})();\n\n// Wait for Spicetify.Player.origin._state before adding following APIs\n(function waitOrigins() {\n\tif (!Spicetify?.Player?.origin?._state) {\n\t\tsetTimeout(waitOrigins, 10);\n\t\treturn;\n\t}\n\n\tconst playerState = {\n\t\tcache: null,\n\t\tcurrent: null,\n\t};\n\n\tconst interval = setInterval(() => {\n\t\tif (!Spicetify.Player.origin._state?.item) return;\n\t\tSpicetify.Player.data = Spicetify.Player.origin._state;\n\t\tplayerState.cache = Spicetify.Player.data;\n\t\tclearInterval(interval);\n\t}, 10);\n\n\tSpicetify.Player.origin._events.addListener(\"update\", ({ data: playerEventData }) => {\n\t\tplayerState.current = playerEventData.item ? playerEventData : null;\n\t\tSpicetify.Player.data = playerState.current;\n\n\t\tif (playerState.cache?.item?.uri !== playerState.current?.item?.uri) {\n\t\t\tconst event = new Event(\"songchange\");\n\t\t\tevent.data = Spicetify.Player.data;\n\t\t\tSpicetify.Player.dispatchEvent(event);\n\t\t}\n\n\t\tif (playerState.cache?.isPaused !== playerState.current?.isPaused) {\n\t\t\tconst event = new Event(\"onplaypause\");\n\t\t\tevent.data = Spicetify.Player.data;\n\t\t\tSpicetify.Player.dispatchEvent(event);\n\t\t}\n\n\t\tplayerState.cache = playerState.current;\n\t});\n\n\t(function waitProductStateAPI() {\n\t\tif (!Spicetify.Platform?.UserAPI) {\n\t\t\tsetTimeout(waitProductStateAPI, 100);\n\t\t\treturn;\n\t\t}\n\n\t\tconst productState = Spicetify.Platform.UserAPI._product_state || Spicetify.Platform.UserAPI._product_state_service;\n\t\tif (productState) return;\n\t\tif (!Spicetify.Platform?.ProductStateAPI) {\n\t\t\tsetTimeout(waitProductStateAPI, 100);\n\t\t\treturn;\n\t\t}\n\n\t\tconst productStateApi = Spicetify.Platform.ProductStateAPI.productStateApi;\n\t\tSpicetify.Platform.UserAPI._product_state_service = productStateApi;\n\t})();\n\n\t(async function setButtonsHeight() {\n\t\twhile (!Spicetify.CosmosAsync) {\n\t\t\tawait new Promise((res) => setTimeout(res, 100));\n\t\t}\n\t\tconst expFeatures = JSON.parse(localStorage.getItem(\"spicetify-exp-features\") || \"{}\");\n\t\tconst isGlobalNavbar = expFeatures?.enableGlobalNavBar?.value;\n\n\t\tif (typeof isGlobalNavbar !== \"undefined\" && isGlobalNavbar === \"control\") {\n\t\t\tawait Spicetify.CosmosAsync.post(\"sp://messages/v1/container/control\", {\n\t\t\t\ttype: \"update_titlebar\",\n\t\t\t\theight: Spicetify.Platform.PlatformData.os_name === \"osx\" ? \"42\" : \"40\",\n\t\t\t});\n\t\t}\n\t})();\n\n\tsetInterval(() => {\n\t\tif (playerState.cache?.isPaused === false) {\n\t\t\tconst event = new Event(\"onprogress\");\n\t\t\tevent.data = Spicetify.Player.getProgress();\n\t\t\tSpicetify.Player.dispatchEvent(event);\n\t\t}\n\t}, 100);\n\n\tSpicetify.addToQueue = (uri) => {\n\t\treturn Spicetify.Player.origin._queue.addToQueue(uri);\n\t};\n\tSpicetify.removeFromQueue = (uri) => {\n\t\treturn Spicetify.Player.origin._queue.removeFromQueue(uri);\n\t};\n})();\n\nSpicetify.getAudioData = async (uri) => {\n\tconst providedURI = uri || Spicetify.Player.data.item.uri;\n\tconst uriObj = Spicetify.URI.from?.(providedURI) ?? Spicetify.URI.fromString?.(providedURI);\n\tif (!uriObj || (uriObj.Type || uriObj.type) !== Spicetify.URI.Type.TRACK) {\n\t\tthrow \"URI is invalid.\";\n\t}\n\n\treturn await Spicetify.CosmosAsync.get(\n\t\t`https://spclient.wg.spotify.com/audio-attributes/v1/audio-analysis/${uriObj.getBase62Id?.() ?? uriObj.id}?format=json`\n\t);\n};\n\nSpicetify.colorExtractor = async (uri) => {\n\tconst body = await Spicetify.CosmosAsync.get(`https://spclient.wg.spotify.com/colorextractor/v1/extract-presets?uri=${uri}&format=json`);\n\n\tif (body.entries?.length) {\n\t\tconst list = {};\n\t\tfor (const color of body.entries[0].color_swatches) {\n\t\t\tlist[color.preset] = `#${color.color?.toString(16).padStart(6, \"0\")}`;\n\t\t}\n\t\treturn list;\n\t}\n\treturn null;\n};\n\nSpicetify.LocalStorage = {\n\tclear: () => localStorage.clear(),\n\tget: (key) => localStorage.getItem(key),\n\tremove: (key) => localStorage.removeItem(key),\n\tset: (key, value) => localStorage.setItem(key, value),\n};\n\nSpicetify._getStyledClassName = (args, component) => {\n\tconst includedKeys = [\n\t\t\"role\",\n\t\t\"variant\",\n\t\t\"semanticColor\",\n\t\t\"iconColor\",\n\t\t\"color\",\n\t\t\"weight\",\n\t\t\"buttonSize\",\n\t\t\"iconSize\",\n\t\t\"position\",\n\t\t\"data-encore-id\",\n\t\t\"$size\",\n\t\t\"$iconColor\",\n\t\t\"$variant\",\n\t\t\"$semanticColor\",\n\t\t\"$buttonSize\",\n\t\t\"$position\",\n\t\t\"$iconSize\",\n\t\t\"$lineClamp\",\n\t];\n\tconst customKeys = [\"blocksize\"];\n\tconst customExactKeys = [\"$padding\", \"$paddingBottom\", \"paddingBottom\", \"padding\"];\n\n\tconst element = Array.from(args).find(\n\t\t(e) =>\n\t\t\te?.children ||\n\t\t\te?.dangerouslySetInnerHTML ||\n\t\t\ttypeof e?.className !== \"undefined\" ||\n\t\t\tincludedKeys.some((key) => typeof e?.[key] !== \"undefined\") ||\n\t\t\tcustomExactKeys.some((key) => typeof e?.[key] !== \"undefined\") ||\n\t\t\tcustomKeys.some((key) => Object.keys(e).some((k) => k.toLowerCase().includes(key)))\n\t);\n\n\tif (!element) return;\n\n\tlet className = /(?:\\w+__)?(\\w+)-[\\w-]+/.exec(component.componentId)?.[1];\n\n\tfor (const key of includedKeys) {\n\t\tif ((typeof element[key] === \"string\" && element[key].length) || typeof element[key] === \"number\") {\n\t\t\tclassName += `-${element[key]}`;\n\t\t}\n\t}\n\n\tconst excludedKeys = [\"children\", \"className\", \"style\", \"dir\", \"key\", \"ref\", \"as\", \"$autoMirror\", \"autoMirror\", \"$hasFocus\", \"\"];\n\tconst excludedPrefix = [\"aria-\"];\n\n\tconst childrenProps = [\"iconLeading\", \"iconTrailing\", \"iconOnly\", \"$iconOnly\", \"$iconLeading\", \"$iconTrailing\"];\n\n\tfor (const key of childrenProps) {\n\t\tconst sanitizedKey = key.startsWith(\"$\") ? key.slice(1) : key;\n\t\tif (element[key]) className += `-${sanitizedKey}`;\n\t}\n\n\tconst booleanKeys = Object.keys(element).filter((key) => typeof element[key] === \"boolean\" && element[key]);\n\n\tfor (const key of booleanKeys) {\n\t\tif (excludedKeys.includes(key)) continue;\n\t\tif (excludedPrefix.some((prefix) => key.startsWith(prefix))) continue;\n\t\tconst sanitizedKey = key.startsWith(\"$\") ? key.slice(1) : key;\n\t\tclassName += `-${sanitizedKey}`;\n\t}\n\n\tconst customEntries = Object.entries(element).filter(\n\t\t([key, value]) =>\n\t\t\t(customKeys.some((k) => key.toLowerCase().includes(k)) || customExactKeys.some((k) => key === k)) && typeof value === \"string\" && value.length\n\t);\n\n\tfor (const [key, value] of customEntries) {\n\t\tconst sanitizedKey = key.startsWith(\"$\") ? key.slice(1) : key;\n\t\tclassName += `-${sanitizedKey}_${value.replace(/[^a-z0-9]/gi, \"_\")}`;\n\t}\n\n\treturn className;\n};\n\n(function waitMouseTrap() {\n\tif (!Spicetify.Mousetrap) {\n\t\tsetTimeout(waitMouseTrap, 10);\n\t\treturn;\n\t}\n\tconst KEYS = {\n\t\tBACKSPACE: \"backspace\",\n\t\tTAB: \"tab\",\n\t\tENTER: \"enter\",\n\t\tSHIFT: \"shift\",\n\t\tCTRL: \"ctrl\",\n\t\tALT: \"alt\",\n\t\tCAPS: \"capslock\",\n\t\tESCAPE: \"esc\",\n\t\tSPACE: \"space\",\n\t\tPAGE_UP: \"pageup\",\n\t\tPAGE_DOWN: \"pagedown\",\n\t\tEND: \"end\",\n\t\tHOME: \"home\",\n\t\tARROW_LEFT: \"left\",\n\t\tARROW_UP: \"up\",\n\t\tARROW_RIGHT: \"right\",\n\t\tARROW_DOWN: \"down\",\n\t\tINSERT: \"ins\",\n\t\tDELETE: \"del\",\n\t\tA: \"a\",\n\t\tB: \"b\",\n\t\tC: \"c\",\n\t\tD: \"d\",\n\t\tE: \"e\",\n\t\tF: \"f\",\n\t\tG: \"g\",\n\t\tH: \"h\",\n\t\tI: \"i\",\n\t\tJ: \"j\",\n\t\tK: \"k\",\n\t\tL: \"l\",\n\t\tM: \"m\",\n\t\tN: \"n\",\n\t\tO: \"o\",\n\t\tP: \"p\",\n\t\tQ: \"q\",\n\t\tR: \"r\",\n\t\tS: \"s\",\n\t\tT: \"t\",\n\t\tU: \"u\",\n\t\tV: \"v\",\n\t\tW: \"w\",\n\t\tX: \"x\",\n\t\tY: \"y\",\n\t\tZ: \"z\",\n\t\tWINDOW_LEFT: \"meta\",\n\t\tWINDOW_RIGHT: \"meta\",\n\t\tSELECT: \"meta\",\n\t\tNUMPAD_0: \"0\",\n\t\tNUMPAD_1: \"1\",\n\t\tNUMPAD_2: \"2\",\n\t\tNUMPAD_3: \"3\",\n\t\tNUMPAD_4: \"4\",\n\t\tNUMPAD_5: \"5\",\n\t\tNUMPAD_6: \"6\",\n\t\tNUMPAD_7: \"7\",\n\t\tNUMPAD_8: \"8\",\n\t\tNUMPAD_9: \"9\",\n\t\tMULTIPLY: \"*\",\n\t\tADD: \"+\",\n\t\tSUBTRACT: \"-\",\n\t\tDECIMAL_POINT: \".\",\n\t\tDIVIDE: \"/\",\n\t\tF1: \"f1\",\n\t\tF2: \"f2\",\n\t\tF3: \"f3\",\n\t\tF4: \"f4\",\n\t\tF5: \"f5\",\n\t\tF6: \"f6\",\n\t\tF7: \"f7\",\n\t\tF8: \"f8\",\n\t\tF9: \"f9\",\n\t\tF10: \"f10\",\n\t\tF11: \"f11\",\n\t\tF12: \"f12\",\n\t\t\";\": \";\",\n\t\t\"=\": \"=\",\n\t\t\",\": \",\",\n\t\t\"-\": \"-\",\n\t\t\".\": \".\",\n\t\t\"/\": \"/\",\n\t\t\"`\": \"`\",\n\t\t\"[\": \"[\",\n\t\t\"\\\\\": \"\\\\\",\n\t\t\"]\": \"]\",\n\t\t// biome-ignore lint/suspicious/noDuplicateObjectKeys: Not an issue\n\t\t'\"': '\"',\n\t\t\"~\": \"`\",\n\t\t\"!\": \"1\",\n\t\t\"@\": \"2\",\n\t\t\"#\": \"3\",\n\t\t$: \"4\",\n\t\t\"%\": \"5\",\n\t\t\"^\": \"6\",\n\t\t\"&\": \"7\",\n\t\t\"*\": \"8\",\n\t\t\"(\": \"9\",\n\t\t\")\": \"0\",\n\t\t_: \"-\",\n\t\t\"+\": \"=\",\n\t\t\":\": \";\",\n\t\t'\"': \"'\",\n\t\t\"<\": \",\",\n\t\t\">\": \".\",\n\t\t\"?\": \"/\",\n\t\t\"|\": \"\\\\\",\n\t};\n\n\tfunction formatKeys(keys) {\n\t\tlet keystroke = \"\";\n\t\tif (typeof keys === \"object\") {\n\t\t\tif (!keys.key || !Object.values(KEYS).includes(keys.key)) {\n\t\t\t\tthrow `Spicetify.Keyboard.registerShortcut: Invalid key ${keys.key}`;\n\t\t\t}\n\t\t\tif (keys.ctrl) keystroke += \"mod+\";\n\t\t\tif (keys.meta) keystroke += \"meta+\";\n\t\t\tif (keys.alt) keystroke += \"alt+\";\n\t\t\tif (keys.shift) keystroke += \"shift+\";\n\t\t\tkeystroke += keys.key;\n\t\t} else if (typeof keys === \"string\" && Object.values(KEYS).includes(keys)) {\n\t\t\tkeystroke = keys;\n\t\t} else {\n\t\t\tthrow `Spicetify.Keyboard.registerShortcut: Invalid keys ${keys}`;\n\t\t}\n\t\treturn keystroke;\n\t}\n\n\tSpicetify.Keyboard = {\n\t\tKEYS,\n\t\tregisterShortcut: (keys, callback) => {\n\t\t\tSpicetify.Mousetrap.bind(formatKeys(keys), callback);\n\t\t},\n\t\t_deregisterShortcut: (keys) => {\n\t\t\tSpicetify.Mousetrap.unbind(formatKeys(keys));\n\t\t},\n\t\tchangeShortcut: (keys, newKeys) => {\n\t\t\tif (!keys || !newKeys) throw \"Spicetify.Keyboard.changeShortcut: Invalid keys\";\n\n\t\t\tconst callback = Object.keys(Spicetify.Mousetrap.trigger()._directMap).find((key) => key.startsWith(formatKeys(keys)));\n\t\t\tif (!callback) throw \"Spicetify.Keyboard.changeShortcut: Shortcut not found\";\n\n\t\t\tSpicetify.Keyboard.registerShortcut(newKeys, Spicetify.Mousetrap.trigger()._directMap[callback]);\n\t\t\tSpicetify.Keyboard._deregisterShortcut(keys);\n\t\t},\n\t};\n\tSpicetify.Keyboard.registerIsolatedShortcut = Spicetify.Keyboard.registerShortcut;\n\tSpicetify.Keyboard.registerImportantShortcut = Spicetify.Keyboard.registerShortcut;\n\tSpicetify.Keyboard.deregisterImportantShortcut = Spicetify.Keyboard._deregisterShortcut;\n})();\n\nSpicetify.SVGIcons = {\n\tcollaborative:\n\t\t'<path d=\"M4.765 1.423c-.42.459-.713.992-.903 1.554-.144.421-.264 1.173-.22 1.894.077 1.321.638 2.408 1.399 3.316v.002l.083.098c.611.293 1.16.696 1.621 1.183a2.244 2.244 0 00-.426-2.092l-.127-.153-.002-.001c-.612-.73-.997-1.52-1.051-2.442-.032-.54.066-1.097.143-1.323a2.85 2.85 0 01.589-1.022 2.888 2.888 0 014.258 0c.261.284.456.628.59 1.022.076.226.175.783.143 1.323-.055.921-.44 1.712-1.052 2.442l-.002.001-.127.153a2.25 2.25 0 00.603 3.39l2.209 1.275a3.248 3.248 0 011.605 2.457h-5.99a5.466 5.466 0 01-.594 1.5h8.259l-.184-1.665a4.75 4.75 0 00-2.346-3.591l-2.209-1.275a.75.75 0 01-.201-1.13l.126-.152h.001c.76-.909 1.32-1.995 1.399-3.316.043-.721-.077-1.473-.22-1.894a4.46 4.46 0 00-.644-1.24v-.002h-.002a4.388 4.388 0 00-6.728-.312zM2 12.5v-2h1.5v2h2V14h-2v2H2v-2H0v-1.5h2z\"/>',\n\talbum:\n\t\t'<path d=\"M7.5 0a7.5 7.5 0 100 15 7.5 7.5 0 000-15zm0 14.012C3.909 14.012.988 11.091.988 7.5S3.909.988 7.5.988s6.512 2.921 6.512 6.512-2.921 6.512-6.512 6.512zM7.5 5a2.5 2.5 0 100 5 2.5 2.5 0 000-5zm0 4.012c-.834 0-1.512-.678-1.512-1.512S6.666 5.988 7.5 5.988s1.512.679 1.512 1.512S8.334 9.012 7.5 9.012z\"/>',\n\tartist:\n\t\t'<path d=\"M9.692 9.133a.202.202 0 01-.1-.143.202.202 0 01.046-.169l.925-1.084a4.035 4.035 0 00.967-2.619v-.353a4.044 4.044 0 00-1.274-2.94A4.011 4.011 0 007.233.744C5.124.881 3.472 2.7 3.472 4.886v.232c0 .96.343 1.89.966 2.618l.925 1.085a.203.203 0 01.047.169.202.202 0 01-.1.143l-2.268 1.304a4.04 4.04 0 00-2.041 3.505V15h1v-1.058c0-1.088.588-2.098 1.537-2.637L5.808 10a1.205 1.205 0 00.316-1.828l-.926-1.085a3.028 3.028 0 01-.726-1.969v-.232c0-1.66 1.241-3.041 2.826-3.144a2.987 2.987 0 012.274.812c.618.579.958 1.364.958 2.21v.354c0 .722-.258 1.421-.728 1.969l-.925 1.085A1.205 1.205 0 009.194 10l.341.196c.284-.248.6-.459.954-.605l-.797-.458zM13 6.334v4.665a2.156 2.156 0 00-1.176-.351c-1.2 0-2.176.976-2.176 2.176S10.625 15 11.824 15 14 14.024 14 12.824V8.065l1.076.622.5-.866L13 6.334zM11.824 14a1.177 1.177 0 01-1.176-1.176A1.177 1.177 0 1111.824 14z\"/>',\n\tblock:\n\t\t'<path fill=\"none\" d=\"M16 0v16H0V0z\"/><path d=\"M4 8h7V7H4v1zm3.5-8a7.5 7.5 0 100 15 7.5 7.5 0 000-15zm0 14C3.916 14 1 11.084 1 7.5S3.916 1 7.5 1 14 3.916 14 7.5 11.084 14 7.5 14z\"/>',\n\tbrightness:\n\t\t'<path d=\"M8 5.25a2.75 2.75 0 100 5.5 2.75 2.75 0 000-5.5zM3.75 8a4.25 4.25 0 118.5 0 4.25 4.25 0 01-8.5 0zm3.5-6V0h1.5v2h-1.5zm0 14v-2h1.5v2h-1.5zm4.462-12.773l1.415-1.414 1.06 1.06-1.414 1.415-1.06-1.061zm-9.899 9.9l1.414-1.415 1.06 1.061-1.414 1.414-1.06-1.06zM14 7.25h2v1.5h-2v-1.5zm-14 0h2v1.5H0v-1.5zm12.773 4.462l1.414 1.415-1.06 1.06-1.415-1.414 1.061-1.06zM2.874 1.813l1.414 1.414-1.06 1.06-1.415-1.413 1.06-1.061z\"/>',\n\tcar: '<path d=\"M2.92 2.375A2.75 2.75 0 015.303 1h5.395c.983 0 1.89.524 2.382 1.375L14.017 4h1.233a.75.75 0 010 1.5h-.237c.989.9.988 2.117.987 2.707v7.043a.75.75 0 01-.75.75h-1.5a.75.75 0 01-.75-.75V14H3v1.25a.75.75 0 01-.75.75H.75a.75.75 0 01-.75-.75V8.207C0 7.617-.002 6.4.987 5.5H.75a.75.75 0 010-1.5h1.233l.938-1.625zm2.382.125c-.446 0-.859.238-1.082.625L3.137 5h9.726L11.78 3.125a1.25 1.25 0 00-1.083-.625H5.302zm8.57 4H2.128a2.72 2.72 0 01-.055.046c-.473.377-.556.894-.57 1.454h2.429a1 1 0 011 1v.5H1.5v3h13v-3h-3.43V9a1 1 0 011-1h2.427c-.013-.56-.096-1.077-.569-1.454a2.585 2.585 0 01-.055-.046z\"/>',\n\t\"chart-down\": '<path d=\"M3 6l5 5.794L13 6z\"/>',\n\t\"chart-up\": '<path d=\"M13 10L8 4.206 3 10z\"/>',\n\tcheck: '<path d=\"M15.53 2.47a.75.75 0 0 1 0 1.06L4.907 14.153.47 9.716a.75.75 0 0 1 1.06-1.06l3.377 3.376L14.47 2.47a.75.75 0 0 1 1.06 0z\"/>',\n\t\"check-alt-fill\":\n\t\t'<path d=\"M7.5 0C3.354 0 0 3.354 0 7.5S3.354 15 7.5 15 15 11.646 15 7.5 11.646 0 7.5 0zM6.246 12.086l-3.16-3.707 1.05-1.232 2.111 2.464 4.564-5.346 1.221 1.05-5.786 6.771z\"/><path fill=\"none\" d=\"M0 0h16v16H0z\"/>',\n\t\"chevron-left\": '<path d=\"M11.521 1.38l-.65-.76L2.23 8l8.641 7.38.65-.76L3.77 8z\"/>',\n\t\"chevron-right\": '<path d=\"M5.129.62l-.65.76L12.231 8l-7.752 6.62.65.76L13.771 8z\"/>',\n\t\"chromecast-disconnected\":\n\t\t'<path d=\"M.667 12v2h2q0-.825-.588-1.413Q1.492 12 .667 12zm0-2.667v1.334q1.38 0 2.357.976Q4 12.619 4 14h1.333q0-.952-.369-1.817-.369-.866-.992-1.489-.623-.623-1.488-.992T.667 9.333zm0-2.666V8q1.627 0 3.008.806 1.38.805 2.186 2.186.806 1.381.806 3.008H8q0-1.198-.369-2.317-.37-1.12-1.048-2.02Q5.905 8.762 5 8.083q-.905-.678-2.024-1.047-1.119-.37-2.31-.37zM14 2H2q-.548 0-.94.393-.393.393-.393.94v2H2v-2h12v9.334H9.333V14H14q.548 0 .94-.393.393-.393.393-.94V3.333q0-.547-.393-.94Q14.548 2 14 2z\"/>',\n\tclock:\n\t\t'<path d=\"M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8z\"/><path d=\"M8 3.25a.75.75 0 01.75.75v3.25H11a.75.75 0 010 1.5H7.25V4A.75.75 0 018 3.25z\"/>',\n\tcomputer:\n\t\t'<path d=\"M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0114.25 12H1.75A1.75 1.75 0 010 10.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25H1.75zm1.5 12.75A.75.75 0 014 14.5h8a.75.75 0 010 1.5H4a.75.75 0 01-.75-.75z\"/>',\n\tcopy: '<path d=\"M8.492 6.619a.522.522 0 00.058.737c.45.385.723.921.77 1.511.046.59-.14 1.163-.524 1.613l-2.372 2.777c-.385.45-.921.724-1.512.77a2.21 2.21 0 01-1.613-.524 2.22 2.22 0 01-.246-3.125l1.482-1.735a.522.522 0 10-.795-.679L2.259 9.7a3.266 3.266 0 00.362 4.599 3.237 3.237 0 002.374.771 3.234 3.234 0 002.224-1.134l2.372-2.777c.566-.663.84-1.505.771-2.375A3.238 3.238 0 009.228 6.56a.523.523 0 00-.736.059zm4.887-4.918A3.233 3.233 0 0011.004.93 3.234 3.234 0 008.78 2.064L6.409 4.84a3.241 3.241 0 00-.772 2.374 3.238 3.238 0 001.134 2.224.519.519 0 00.738-.058.522.522 0 00-.058-.737 2.198 2.198 0 01-.771-1.511 2.208 2.208 0 01.524-1.613l2.372-2.777c.385-.45.921-.724 1.512-.77a2.206 2.206 0 011.613.524 2.22 2.22 0 01.246 3.125l-1.482 1.735a.522.522 0 10.795.679L13.741 6.3a3.266 3.266 0 00-.362-4.599z\"/>',\n\tdownload:\n\t\t'<path d=\"M7.999 9.657V4h-1v5.65L5.076 7.414l-.758.651 3.183 3.701 3.193-3.7-.758-.653-1.937 2.244zM7.5 0a7.5 7.5 0 100 15 7.5 7.5 0 000-15zm0 14C3.916 14 1 11.084 1 7.5S3.916 1 7.5 1 14 3.916 14 7.5 11.084 14 7.5 14z\"/>',\n\tdownloaded:\n\t\t'<path d=\"M7.5 0a7.5 7.5 0 100 15 7.5 7.5 0 000-15zm.001 11.767L4.318 8.065l.758-.652L6.999 9.65V3h1v6.657l1.937-2.244.757.653-3.192 3.701z\"/>',\n\tedit: '<path d=\"M11.472.279L2.583 10.686l-.887 4.786 4.588-1.625L15.173 3.44 11.472.279zM5.698 12.995l-2.703.957.523-2.819v-.001l2.18 1.863zm-1.53-2.623l7.416-8.683 2.18 1.862-7.415 8.683-2.181-1.862z\"/>',\n\tenhance:\n\t\t'<path d=\"M11.777.972c-.364 1.054-1.195 2.322-2.798 2.83-.115.036-.115.36 0 .396 1.603.508 2.434 1.775 2.798 2.83.04.114.406.114.446 0 .364-1.055 1.195-2.322 2.798-2.83.115-.036.115-.36 0-.396-1.603-.508-2.434-1.776-2.798-2.83-.04-.114-.406-.114-.446 0zM5.295 4.5a.75.75 0 01.747.682c.06.65.334 1.68.954 2.572.606.87 1.527 1.596 2.927 1.75a.75.75 0 010 1.491c-1.4.154-2.321.88-2.927 1.751a5.683 5.683 0 00-.954 2.572.75.75 0 01-1.493 0 5.683 5.683 0 00-.954-2.572c-.606-.87-1.527-1.597-2.927-1.75a.75.75 0 010-1.492c1.4-.154 2.321-.88 2.927-1.75.62-.892.894-1.922.954-2.572a.75.75 0 01.746-.682z\"/>',\n\t\"exclamation-circle\":\n\t\t'<path fill=\"none\" d=\"M0 0h16v16H0z\"/><path d=\"M8 0a8 8 0 100 16A8 8 0 008 0zm0 14.946c-3.83 0-6.946-3.116-6.946-6.946S4.17 1.054 8 1.054 14.946 4.17 14.946 8 11.83 14.946 8 14.946z\"/><path d=\"M7.214 11.639c0-.216.076-.402.228-.558a.742.742 0 01.552-.234c.216 0 .402.078.558.234.156.155.234.342.234.558s-.078.4-.234.552a.773.773 0 01-.558.229.749.749 0 01-.552-.229.752.752 0 01-.228-.552zm1.188-1.716h-.804l-.312-6.072h1.416l-.3 6.072z\"/>',\n\t\"external-link\": '<path fill-rule=\"evenodd\" d=\"M15 7V1H9v1h4.29L7.11 8.18l.71.71L14 2.71V7h1zM1 15h12V9h-1v5H2V4h5V3H1v12z\" clip-rule=\"evenodd\"/>',\n\tfacebook:\n\t\t'<path d=\"M8.929 14.992H6.032v-7H4.587V5.587h1.445V4.135q0-1.548.73-2.341Q7.492 1 9.167 1h1.936v2.413H9.897q-.334 0-.532.055-.198.056-.294.195-.095.139-.119.29-.023.15-.023.428v1.206h2.182l-.254 2.405H8.93v7z\"/>',\n\tfollow:\n\t\t'<path d=\"M3.645 6.352a2.442 2.442 0 01-.586-1.587v-.194c0-1.339 1-2.454 2.277-2.536a2.409 2.409 0 011.833.655c.129.121.241.254.34.395.266-.197.55-.368.851-.51a3.345 3.345 0 00-.507-.615 3.42 3.42 0 00-2.581-.923c-1.802.117-3.213 1.669-3.213 3.534v.193c0 .82.293 1.614.825 2.236l.772.904s.07.081-.024.134L1.743 9.125A3.449 3.449 0 000 12.118V13h1v-.882c0-.877.474-1.691 1.24-2.125l1.891-1.088a1.089 1.089 0 00.286-1.649l-.772-.904zm10.614 5.774l-1.892-1.087c-.077-.044-.023-.134-.023-.134l.771-.904a3.446 3.446 0 00.825-2.236v-.294c0-.947-.396-1.862-1.088-2.511a3.419 3.419 0 00-2.581-.923c-1.801.117-3.212 1.67-3.212 3.535v.193c0 .82.293 1.614.825 2.236l.771.904s.059.087-.023.134l-1.889 1.086A3.45 3.45 0 005 15.118V16h1v-.882c0-.877.474-1.691 1.239-2.125l1.892-1.087a1.089 1.089 0 00.286-1.65l-.773-.904a2.447 2.447 0 01-.585-1.587v-.193c0-1.339 1-2.454 2.277-2.537a2.413 2.413 0 011.833.654c.498.467.771 1.1.771 1.781v.294c0 .582-.208 1.145-.586 1.587l-.771.904a1.09 1.09 0 00.285 1.651l1.894 1.088A2.448 2.448 0 0115 15.118V16h1v-.882a3.447 3.447 0 00-1.741-2.992z\"/>',\n\tfullscreen:\n\t\t'<path d=\"M6.064 10.229l-2.418 2.418L2 11v4h4l-1.647-1.646 2.418-2.418-.707-.707zM11 2l1.647 1.647-2.418 2.418.707.707 2.418-2.418L15 6V2h-4z\"/>',\n\tgamepad:\n\t\t'<path d=\"M4.423 2.5a1.25 1.25 0 00-1.224.995l-1.652 7.923a1.313 1.313 0 002.423.925l1.57-2.718A1.25 1.25 0 016.622 9h2.756c.447 0 .86.238 1.083.625l1.57 2.718a1.313 1.313 0 002.422-.924l-1.652-7.924a1.25 1.25 0 00-1.224-.995H4.423zm-2.692.689A2.75 2.75 0 014.423 1h7.154a2.75 2.75 0 012.692 2.189l1.653 7.923a2.813 2.813 0 01-5.19 1.981L9.233 10.5H6.766L5.27 13.093a2.813 2.813 0 01-5.19-1.98l1.65-7.925z\"/><path d=\"M7 5.5a1.25 1.25 0 11-2.5 0 1.25 1.25 0 012.5 0zm2 0a1.25 1.25 0 102.5 0 1.25 1.25 0 00-2.5 0z\"/>',\n\t\"grid-view\": '<path d=\"M9 1v6h6V1H9zm5 5h-4V2h4v4zM.999 7h6V1h-6v6zM2 2h4v4H2V2zm7 13h6V9H9v6zm1-5h4v4h-4v-4zM.999 15h6V9h-6v6zM2 10h4v4H2v-4z\"/>',\n\theart:\n\t\t'<path d=\"M13.764 2.727a4.057 4.057 0 00-5.488-.253.558.558 0 01-.31.112.531.531 0 01-.311-.112 4.054 4.054 0 00-5.487.253A4.05 4.05 0 00.974 5.61c0 1.089.424 2.113 1.168 2.855l4.462 5.223a1.791 1.791 0 002.726 0l4.435-5.195A4.052 4.052 0 0014.96 5.61a4.057 4.057 0 00-1.196-2.883zm-.722 5.098L8.58 13.048c-.307.36-.921.36-1.228 0L2.864 7.797a3.072 3.072 0 01-.905-2.187c0-.826.321-1.603.905-2.187a3.091 3.091 0 012.191-.913 3.05 3.05 0 011.957.709c.041.036.408.351.954.351.531 0 .906-.31.94-.34a3.075 3.075 0 014.161.192 3.1 3.1 0 01-.025 4.403z\"/>',\n\t\"heart-active\":\n\t\t'<path fill=\"none\" d=\"M0 0h16v16H0z\"/><path d=\"M13.797 2.727a4.057 4.057 0 00-5.488-.253.558.558 0 01-.31.112.531.531 0 01-.311-.112 4.054 4.054 0 00-5.487.253c-.77.77-1.194 1.794-1.194 2.883s.424 2.113 1.168 2.855l4.462 5.223a1.791 1.791 0 002.726 0l4.435-5.195a4.052 4.052 0 001.195-2.883 4.057 4.057 0 00-1.196-2.883z\"/>',\n\tinstagram:\n\t\t'<path d=\"M11.183 1.595Q10.175 1.548 8 1.548t-3.183.047q-.865.04-1.46.27-.516.198-.905.587-.389.39-.587.905-.23.595-.27 1.46Q1.548 5.825 1.548 8t.047 3.183q.04.865.27 1.46.198.516.587.905.39.389.905.587.595.23 1.46.27 1.008.047 3.183.047t3.183-.047q.865-.04 1.46-.27.516-.198.905-.587.389-.39.587-.905.23-.595.27-1.46.047-1.008.047-3.183t-.047-3.183q-.04-.865-.27-1.46-.198-.516-.587-.905-.39-.389-.905-.587-.595-.23-1.46-.27zM4.754.175Q5.794.127 8 .127t3.246.048q1.095.047 1.913.365.793.31 1.393.908.599.6.908 1.393.318.818.365 1.913.048 1.04.048 3.246t-.048 3.246q-.047 1.095-.365 1.913-.31.793-.908 1.393-.6.599-1.393.908-.818.318-1.913.365-1.04.048-3.246.048t-3.246-.048q-1.095-.047-1.913-.365-.793-.31-1.393-.908-.599-.6-.908-1.393-.318-.818-.365-1.913Q.127 10.206.127 8t.048-3.246Q.222 3.659.54 2.841q.31-.793.908-1.393.6-.599 1.393-.908Q3.66.222 4.754.175zm1.675 4.103Q7.175 3.96 8 3.96t1.571.318q.746.317 1.29.86.544.545.861 1.29.318.747.318 1.572 0 .825-.318 1.571-.317.746-.86 1.29-.545.544-1.29.861-.747.318-1.572.318-.825 0-1.571-.318-.746-.317-1.29-.86-.544-.545-.861-1.29Q3.96 8.824 3.96 8q0-.825.318-1.571.317-.746.86-1.29.545-.544 1.29-.861zm.254 5.996q.603.353 1.317.353t1.317-.353q.604-.353.957-.957.353-.603.353-1.317t-.353-1.317q-.353-.604-.957-.957Q8.714 5.373 8 5.373t-1.317.353q-.604.353-.957.957-.353.603-.353 1.317t.353 1.317q.353.604.957.957zm4.849-5.806q-.278-.278-.278-.67 0-.393.278-.671t.67-.278q.393 0 .671.278t.278.67q0 .393-.278.671t-.67.278q-.393 0-.671-.278z\"/>',\n\tlaptop:\n\t\t'<path d=\"M2 3.75C2 2.784 2.784 2 3.75 2h8.5c.966 0 1.75.784 1.75 1.75v6.5A1.75 1.75 0 0112.25 12h-8.5A1.75 1.75 0 012 10.25v-6.5zm1.75-.25a.25.25 0 00-.25.25v6.5c0 .138.112.25.25.25h8.5a.25.25 0 00.25-.25v-6.5a.25.25 0 00-.25-.25h-8.5zM.25 15.25A.75.75 0 011 14.5h14a.75.75 0 010 1.5H1a.75.75 0 01-.75-.75z\"/>',\n\tlibrary:\n\t\t'<path d=\"M8.375 1.098a.75.75 0 01.75 0l5.5 3.175a.75.75 0 01.375.65V15.25a.75.75 0 01-.75.75h-5.5a.75.75 0 01-.75-.75V1.747a.75.75 0 01.375-.65zM9.5 3.046V14.5h4V5.356l-4-2.31zM1 1.75a.75.75 0 011.5 0v13.5a.75.75 0 01-1.5 0V1.75zm3.5 0a.75.75 0 011.5 0v13.5a.75.75 0 01-1.5 0V1.75z\"/>',\n\t\"list-view\": '<path d=\"M1 3h1V2H1v1zm3-1v1h11V2H4zM1 9h1V8H1v1zm3 0h11V8H4v1zm0 6h11v-1H4v1zm-3 0h1v-1H1v1z\"/>',\n\tlocation:\n\t\t'<path d=\"M8 1.562a4.732 4.732 0 00-3.47 7.95l.013.014L8 13.646l3.456-4.12.013-.013A4.732 4.732 0 008 1.563zM1.768 6.294a6.232 6.232 0 1110.813 4.225L8 15.98l-4.582-5.46a6.212 6.212 0 01-1.65-4.225z\"/><path d=\"M8 5.05a1.243 1.243 0 100 2.488A1.243 1.243 0 008 5.05zM5.257 6.295a2.743 2.743 0 115.486 0 2.743 2.743 0 01-5.486 0z\"/>',\n\tlocked:\n\t\t'<path d=\"M13 6h-1V4.5a4 4 0 00-8 0V6H3a1 1 0 00-1 1v7a1 1 0 001 1h10a1 1 0 001-1V7a1 1 0 00-1-1zM5 4.5c0-1.654 1.346-3 3-3s3 1.346 3 3V6H5V4.5zm8 9.5H3V7h10v7z\"/>',\n\t\"locked-active\":\n\t\t'<path fill=\"none\" d=\"M0 0h16v16H0z\"/><path d=\"M13 6h-1V4.5c0-2.2-1.8-4-4-4s-4 1.8-4 4V6H3c-.6 0-1 .4-1 1v7c0 .6.4 1 1 1h10c.6 0 1-.4 1-1V7c0-.6-.4-1-1-1zM5 4.5c0-1.7 1.3-3 3-3s3 1.3 3 3V6H5V4.5z\"/>',\n\tlyrics:\n\t\t'<path d=\"M8.5 1A4.505 4.505 0 004 5.5c0 .731.191 1.411.502 2.022L1.99 13.163a1.307 1.307 0 00.541 1.666l.605.349a1.307 1.307 0 001.649-.283L9.009 9.95C11.248 9.692 13 7.807 13 5.5 13 3.019 10.981 1 8.5 1zM4.023 14.245a.307.307 0 01-.388.066l-.605-.349a.309.309 0 01-.128-.393l2.26-5.078A4.476 4.476 0 007.715 9.92l-3.692 4.325zM8.5 9C6.57 9 5 7.43 5 5.5S6.57 2 8.5 2 12 3.57 12 5.5 10.429 9 8.5 9z\"/>',\n\tmenu: '<path d=\"M15.5 13.5H.5V12h15v1.5zm0-4.75H.5v-1.5h15v1.5zm0-4.75H.5V2.5h15V4z\"/>',\n\tminimize:\n\t\t'<path d=\"M3.646 11.648l-2.418 2.417.707.707 2.418-2.418L5.999 14v-4h-4l1.647 1.648zm11.125-9.712l-.707-.707-2.418 2.418L10 2v4h4l-1.647-1.647 2.418-2.417z\"/>',\n\tminus: '<path d=\"M2 7h12v2H0z\"></path>',\n\tmore: '<path d=\"M2 6.5a1.5 1.5 0 10-.001 2.999A1.5 1.5 0 002 6.5zm6 0a1.5 1.5 0 100 3 1.5 1.5 0 000-3zm6 0a1.5 1.5 0 10-.001 2.999A1.5 1.5 0 0014 6.5z\"/>',\n\t\"new-spotify-connect\":\n\t\t'<path d=\"M4 9.984v-4h1.802L9 4.143v7.69L5.802 9.984H4zm4 5.048q1.341 0 2.603-.508l.683.801q-1.58.707-3.286.707-1.627 0-3.107-.635-1.48-.635-2.552-1.707Q1.27 12.62.635 11.14 0 9.659 0 8.032q0-1.627.635-3.107.635-1.48 1.706-2.552Q3.413 1.302 4.893.667T8 .032q1.706 0 3.286.706l-.683.802Q9.341 1.032 8 1.032q-1.42 0-2.718.555-1.298.556-2.234 1.492-.937.937-1.492 2.234Q1 6.611 1 8.032q0 1.42.556 2.718.555 1.298 1.492 2.234.936.937 2.234 1.492 1.297.556 2.718.556zm4.357-12.469l.65-.761q1.398 1.119 2.195 2.746Q16 6.175 16 8.032t-.798 3.484q-.797 1.627-2.194 2.746l-.65-.762q1.23-.984 1.936-2.413Q15 9.66 15 8.032q0-1.627-.706-3.056-.707-1.428-1.937-2.413zM10.405 4.85l.643-.746q.904.699 1.428 1.722Q13 6.85 13 8.032q0 1.182-.524 2.206t-1.428 1.722l-.643-.746q.738-.563 1.166-1.393.429-.829.429-1.79 0-.96-.429-1.789-.428-.83-1.166-1.393z\"/>',\n\toffline:\n\t\t'<path d=\"M12.715 3.341L13.89.703l-.913-.406-6.679 15 .913.406L8.414 13H11c2.75 0 5-2.25 5-5 0-2.143-1.38-3.954-3.285-4.659zM11 12H8.859l3.456-7.763C13.874 4.784 15 6.257 15 8c0 2.206-1.794 4-4 4zM8.79.297L7.586 3H5C2.25 3 0 5.25 0 8c0 2.143 1.38 3.954 3.285 4.659L2.11 15.297l.913.406 6.679-15L8.79.297zM3.684 11.763C2.126 11.216 1 9.743 1 8c0-2.206 1.794-4 4-4h2.141l-3.457 7.763z\"/><path fill=\"none\" d=\"M16 0v16H0V0z\"/>',\n\tpause: '<path fill=\"none\" d=\"M0 0h16v16H0z\"/><path d=\"M3 2h3v12H3zM10 2h3v12h-3z\"/>',\n\tphone:\n\t\t'<path d=\"M8 13a1 1 0 100-2 1 1 0 000 2z\"/><path d=\"M4.75 0A1.75 1.75 0 003 1.75v12.5c0 .966.784 1.75 1.75 1.75h6.5A1.75 1.75 0 0013 14.25V1.75A1.75 1.75 0 0011.25 0h-6.5zM4.5 1.75a.25.25 0 01.25-.25h6.5a.25.25 0 01.25.25v12.5a.25.25 0 01-.25.25h-6.5a.25.25 0 01-.25-.25V1.75z\"/>',\n\tplay: '<path d=\"M4.018 14L14.41 8 4.018 2z\"/>',\n\tplaylist: '<path d=\"M15 14.5H5V13h10v1.5zm0-5.75H5v-1.5h10v1.5zM15 3H5V1.5h10V3zM3 3H1V1.5h2V3zm0 11.5H1V13h2v1.5zm0-5.75H1v-1.5h2v1.5z\"/>',\n\t\"playlist-folder\":\n\t\t'<path d=\"M1.75 1A1.75 1.75 0 000 2.75v11.5C0 15.216.784 16 1.75 16h12.5A1.75 1.75 0 0016 14.25v-9.5A1.75 1.75 0 0014.25 3H7.82l-.65-1.125A1.75 1.75 0 005.655 1H1.75zM1.5 2.75a.25.25 0 01.25-.25h3.905a.25.25 0 01.216.125L6.954 4.5h7.296a.25.25 0 01.25.25v9.5a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25V2.75z\"/>',\n\tplus2px: '<path d=\"M14 7H9V2H7v5H2v2h5v5h2V9h5z\"/><path fill=\"none\" d=\"M0 0h16v16H0z\"/>',\n\t\"plus-alt\":\n\t\t'<path d=\"M7.5 0a7.5 7.5 0 100 15 7.5 7.5 0 000-15zm0 14C3.916 14 1 11.084 1 7.5S3.916 1 7.5 1 14 3.916 14 7.5 11.084 14 7.5 14zM8 3H7v4H3v1h4v4h1V8h4V7H8V3z\"/>',\n\tpodcasts:\n\t\t'<path d=\"M4.011 8.226a3.475 3.475 0 011.216-2.387c.179-.153.373-.288.578-.401l-.485-.875a4.533 4.533 0 00-.742.515 4.476 4.476 0 00-1.564 3.069 4.476 4.476 0 002.309 4.287l.483-.875a3.483 3.483 0 01-1.795-3.333zm-1.453 4.496a6.506 6.506 0 01.722-9.164c.207-.178.425-.334.647-.48l-.551-.835c-.257.169-.507.35-.746.554A7.449 7.449 0 00.024 7.912a7.458 7.458 0 003.351 6.848l.55-.835a6.553 6.553 0 01-1.367-1.203zm10.645-9.093a7.48 7.48 0 00-1.578-1.388l-.551.835c.518.342.978.746 1.368 1.203a6.452 6.452 0 011.537 4.731 6.455 6.455 0 01-2.906 4.914l.55.835c.257-.169.507-.351.747-.555a7.453 7.453 0 002.606-5.115 7.447 7.447 0 00-1.773-5.46zm-2.281 1.948a4.497 4.497 0 00-1.245-1.011l-.483.875a3.476 3.476 0 011.796 3.334 3.472 3.472 0 01-1.217 2.387 3.478 3.478 0 01-.577.401l.485.875a4.57 4.57 0 00.742-.515 4.476 4.476 0 001.564-3.069 4.482 4.482 0 00-1.065-3.277zM7.5 7A1.495 1.495 0 007 9.908V16h1V9.908A1.495 1.495 0 007.5 7z\"/><path fill=\"none\" d=\"M16 0v16H0V0z\"/><path fill=\"none\" d=\"M16 0v16H0V0z\"/>',\n\tprojector:\n\t\t'<path d=\"M11.5 9.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3z\"/><path d=\"M1.75 3A1.75 1.75 0 000 4.75v6.5C0 12.216.784 13 1.75 13H2v1.25a.75.75 0 001.5 0V13h9v1.25a.75.75 0 001.5 0V13h.25A1.75 1.75 0 0016 11.25v-6.5A1.75 1.75 0 0014.25 3H1.75zM1.5 4.75a.25.25 0 01.25-.25h12.5a.25.25 0 01.25.25v6.5a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25v-6.5z\"/>',\n\tqueue:\n\t\t'<path d=\"M15 15H1v-1.5h14V15zm0-4.5H1V9h14v1.5zm-14-7A2.5 2.5 0 013.5 1h9a2.5 2.5 0 010 5h-9A2.5 2.5 0 011 3.5zm2.5-1a1 1 0 000 2h9a1 1 0 100-2h-9z\"/>',\n\trepeat:\n\t\t'<path d=\"M5.5 5H10v1.5l3.5-2-3.5-2V4H5.5C3 4 1 6 1 8.5c0 .6.1 1.2.4 1.8l.9-.5C2.1 9.4 2 9 2 8.5 2 6.6 3.6 5 5.5 5zm9.1 1.7l-.9.5c.2.4.3.8.3 1.3 0 1.9-1.6 3.5-3.5 3.5H6v-1.5l-3.5 2 3.5 2V13h4.5C13 13 15 11 15 8.5c0-.6-.1-1.2-.4-1.8z\"/>',\n\t\"repeat-once\":\n\t\t'<path fill=\"none\" d=\"M0 0h16v16H0z\"/><path d=\"M5 5v-.5V4c-2.2.3-4 2.2-4 4.5 0 .6.1 1.2.4 1.8l.9-.5C2.1 9.4 2 9 2 8.5 2 6.7 3.3 5.3 5 5zM10.5 12H6v-1.5l-3.5 2 3.5 2V13h4.5c1.9 0 3.5-1.2 4.2-2.8-.5.3-1 .5-1.5.6-.7.7-1.6 1.2-2.7 1.2zM11.5 0C9 0 7 2 7 4.5S9 9 11.5 9 16 7 16 4.5 14 0 11.5 0zm.9 7h-1.3V3.6H10v-1h.1c.2 0 .3 0 .4-.1.1 0 .3-.1.4-.2.1-.1.2-.2.2-.3.1-.1.1-.2.1-.3v-.1h1.1V7z\"/>',\n\tsearch:\n\t\t'<path d=\"M11.618 11.532A5.589 5.589 0 0013.22 7.61a5.61 5.61 0 10-5.61 5.61 5.58 5.58 0 003.246-1.04l2.912 3.409.76-.649-2.91-3.408zm-4.008.688C5.068 12.22 3 10.152 3 7.61S5.068 3 7.61 3s4.61 2.068 4.61 4.61-2.068 4.61-4.61 4.61z\"/>',\n\t\"search-active\":\n\t\t'<path d=\"M11.955 11.157A5.61 5.61 0 107.61 13.22c1.03 0 1.992-.282 2.822-.767l2.956 3.46 1.521-1.299-2.954-3.457zm-4.345.063A3.614 3.614 0 014 7.61 3.614 3.614 0 017.61 4a3.614 3.614 0 013.61 3.61 3.614 3.614 0 01-3.61 3.61z\"/>',\n\tshuffle:\n\t\t'<path d=\"M4.5 6.8l.7-.8C4.1 4.7 2.5 4 .9 4v1c1.3 0 2.6.6 3.5 1.6l.1.2zm7.5 4.7c-1.2 0-2.3-.5-3.2-1.3l-.6.8c1 1 2.4 1.5 3.8 1.5V14l3.5-2-3.5-2v1.5zm0-6V7l3.5-2L12 3v1.5c-1.6 0-3.2.7-4.2 2l-3.4 3.9c-.9 1-2.2 1.6-3.5 1.6v1c1.6 0 3.2-.7 4.2-2l3.4-3.9c.9-1 2.2-1.6 3.5-1.6z\"/>',\n\t\"skip-back\": '<path d=\"M13 2.5L5 7.119V3H3v10h2V8.881l8 4.619z\"/>',\n\t\"skip-back15\":\n\t\t'<path d=\"M10 4.001H6V2.5l-3.464 2L6 6.5V5h4c2.206 0 4 1.794 4 4s-1.794 4-4 4v1c2.75 0 5-2.25 5-5s-2.25-4.999-5-4.999zM2.393 8.739c-.083.126-.19.236-.32.332a1.642 1.642 0 01-.452.229 1.977 1.977 0 01-.56.092v.752h1.36V14h1.096V8.327h-.96c-.027.15-.081.287-.164.412zm5.74 2.036a1.762 1.762 0 00-.612-.368 2.295 2.295 0 00-.78-.128c-.191 0-.387.031-.584.092a1.188 1.188 0 00-.479.268l.327-1.352H8.38v-.96H5.252l-.688 2.872c.037.017.105.042.204.076l.308.108.309.107.212.076c.096-.112.223-.205.38-.28.157-.075.337-.112.54-.112.133 0 .264.021.392.063.128.043.24.105.336.188a.907.907 0 01.233.316c.059.128.088.275.088.44a.927.927 0 01-.628.916 1.19 1.19 0 01-.404.068c-.16 0-.306-.025-.435-.076a1.046 1.046 0 01-.34-.212.992.992 0 01-.229-.32 1.171 1.171 0 01-.1-.4l-1.04.248c.021.225.086.439.195.645.109.205.258.388.444.548.187.16.406.287.66.38.253.093.534.14.844.14.336 0 .636-.052.9-.156.264-.104.487-.246.672-.424.184-.179.325-.385.424-.62.099-.235.148-.485.148-.752 0-.298-.049-.565-.145-.8a1.686 1.686 0 00-.399-.591z\"/>',\n\t\"skip-forward\": '<path d=\"M11 3v4.119L3 2.5v11l8-4.619V13h2V3z\"/>',\n\t\"skip-forward15\":\n\t\t'<path d=\"M6 5h4v1.5l3.464-2L10 2.5V4H6C3.25 4 1 6.25 1 9s2.25 5 5 5v-1c-2.206 0-4-1.794-4-4s1.794-4 4-4zm1.935 3.739a1.306 1.306 0 01-.32.332c-.13.096-.281.172-.451.228a1.956 1.956 0 01-.562.092v.752h1.36v3.856h1.096V8.327h-.96c-.026.15-.08.287-.163.412zm6.139 2.628a1.664 1.664 0 00-.399-.592 1.747 1.747 0 00-.612-.368 2.295 2.295 0 00-.78-.128c-.191 0-.387.03-.584.092-.197.061-.357.15-.479.268l.327-1.352h2.376v-.96h-3.128l-.688 2.872c.037.016.106.041.204.076l.308.108.309.108.212.076c.096-.112.223-.206.38-.28.157-.075.337-.112.54-.112.133 0 .264.021.392.064a.97.97 0 01.336.188.907.907 0 01.233.316c.058.128.088.274.088.44a.941.941 0 01-.3.721.995.995 0 01-.328.196 1.19 1.19 0 01-.404.068c-.16 0-.306-.025-.436-.076a1.03 1.03 0 01-.569-.532 1.171 1.171 0 01-.1-.4l-1.04.248c.02.224.086.439.195.644.109.205.258.388.444.548.186.16.406.287.66.38.253.093.534.14.844.14.336 0 .636-.052.9-.156.264-.104.487-.245.672-.424.184-.179.325-.385.424-.62a1.91 1.91 0 00.148-.752c0-.3-.049-.566-.145-.801z\"/>',\n\tsoundbetter:\n\t\t'<path fill-rule=\"evenodd\" d=\"M5.272 12.542h1.655V2H5.15v3.677C4.782 4.758 4.046 4.33 3.065 4.33c-.98 0-1.716.43-2.268 1.226C.245 6.352 0 7.332 0 8.435c0 1.226.245 2.207.736 3.004.49.796 1.226 1.226 2.207 1.226 1.103 0 1.9-.552 2.329-1.717v1.594zm-.49-6.068c.306.368.429.858.429 1.47v1.35c0 .55-.184 1.041-.49 1.409-.369.368-.737.552-1.166.552-1.103 0-1.655-.92-1.655-2.636 0-.92.123-1.594.49-2.023.307-.429.736-.674 1.227-.674.49 0 .858.184 1.164.552zM8.03 12.542V2h4.108c.674 0 1.287.061 1.716.245.43.123.859.43 1.165.92.307.429.49.98.49 1.593 0 .552-.183 1.103-.49 1.532-.368.43-.797.674-1.41.797.736.123 1.288.43 1.655.92.368.49.552 1.041.552 1.654 0 .797-.245 1.471-.797 2.023-.552.551-1.348.858-2.452.858H8.031zm1.778-6.13h2.33c.49 0 .858-.122 1.103-.428.245-.307.429-.674.429-1.103 0-.49-.184-.859-.49-1.042a1.712 1.712 0 00-1.104-.368H9.808v2.942zm2.452 4.536H9.808V7.884h2.452c.49 0 .92.122 1.226.429.245.306.43.674.43 1.103 0 .49-.123.858-.43 1.103-.306.307-.736.43-1.226.43z\" clip-rule=\"evenodd\"/><path d=\"M.674 13.523H16v1.226H.674z\"/>',\n\tspeaker:\n\t\t'<path d=\"M11 12.75a2 2 0 100-4 2 2 0 000 4z\"/><path d=\"M6 2.75C6 1.784 6.783 1 7.75 1h6.5c.966 0 1.75.784 1.75 1.75v11.5A1.75 1.75 0 0114.25 16h-6.5A1.75 1.75 0 016 14.25V2.75zm1.75-.25a.25.25 0 00-.25.25v11.5c0 .138.112.25.25.25h6.5a.25.25 0 00.25-.25V2.75a.25.25 0 00-.25-.25h-6.5zm-6 0a.25.25 0 00-.25.25v11.5c0 .138.112.25.25.25h2.375V16H1.75A1.75 1.75 0 010 14.25V2.75C0 1.784.784 1 1.75 1h2.375v1.5H1.75z\"/><path d=\"M12 5.5a1 1 0 11-2 0 1 1 0 012 0z\"/>',\n\tspotify:\n\t\t'<path d=\"M8 0a8 8 0 100 16A8 8 0 008 0zm3.669 11.539a.498.498 0 01-.686.166c-1.878-1.148-4.243-1.408-7.028-.772a.499.499 0 01-.222-.972c3.048-.696 5.662-.396 7.77.892a.5.5 0 01.166.686zm.979-2.178a.624.624 0 01-.858.205c-2.15-1.322-5.428-1.705-7.972-.932a.624.624 0 11-.362-1.194c2.905-.882 6.517-.455 8.987 1.063a.624.624 0 01.205.858zm.084-2.269C10.153 5.561 5.9 5.42 3.438 6.167a.748.748 0 11-.434-1.432c2.826-.857 7.523-.692 10.492 1.07a.748.748 0 01-.764 1.287z\"/>',\n\tsubtitles: '<path fill=\"none\" d=\"M0 0h16v16H0z\"/><path d=\"M3 7h10v1H3zM5 10h6v1H5z\"/><path d=\"M15 3v10H1V3h14m1-1H0v12h16V2z\"/>',\n\ttablet:\n\t\t'<path d=\"M1 1.75C1 .784 1.784 0 2.75 0h10.5C14.216 0 15 .784 15 1.75v12.5A1.75 1.75 0 0113.25 16H2.75A1.75 1.75 0 011 14.25V1.75zm1.75-.25a.25.25 0 00-.25.25v12.5c0 .138.112.25.25.25h10.5a.25.25 0 00.25-.25V1.75a.25.25 0 00-.25-.25H2.75z\"/><path d=\"M9 12a1 1 0 11-2 0 1 1 0 012 0z\"/>',\n\tticket:\n\t\t'<path d=\"M0 2h16v5.486l-.563.145a.898.898 0 000 1.739l.563.144V15H0V9.514l.563-.144a.898.898 0 000-1.74L0 7.487V2zm1.5 1.5v2.902a2.396 2.396 0 010 4.196V13.5h13v-2.902a2.396 2.396 0 010-4.196V3.5h-13z\"/><path d=\"M8 7.25a1.25 1.25 0 100 2.5 1.25 1.25 0 000-2.5zM5.25 8.5a2.75 2.75 0 115.5 0 2.75 2.75 0 01-5.5 0z\"/>',\n\ttwitter:\n\t\t'<path d=\"M13.54 3.889q.984-.595 1.333-1.683-.905.54-1.929.738-.42-.452-.996-.706-.575-.254-1.218-.254-1.254 0-2.143.889-.889.889-.889 2.15 0 .318.08.691-1.857-.095-3.484-.932-1.627-.838-2.762-2.242-.413.714-.413 1.523 0 .778.361 1.445t.988 1.08q-.714-.009-1.373-.374v.04q0 1.087.69 1.92.691.834 1.739 1.048-.397.111-.794.111-.254 0-.571-.055.285.912 1.063 1.5.778.587 1.77.603-1.659 1.302-3.77 1.302-.365 0-.722-.048Q2.619 14 5.15 14q1.358 0 2.572-.361 1.215-.361 2.147-.988.933-.627 1.683-1.46.75-.834 1.234-1.798.484-.964.738-1.988t.254-2.032q0-.262-.008-.397.88-.635 1.508-1.563-.841.373-1.738.476z\"/>',\n\tvisualizer: '<path d=\"M.999 15h2V5h-2v10zm4 0h2V1h-2v14zM9 15h2v-4H9v4zm4-7v7h2V8h-2z\"/>',\n\tvoice:\n\t\t'<path d=\"M4 4a4 4 0 118 0v3a4 4 0 01-8 0V4zm4-2.5A2.5 2.5 0 005.5 4v3a2.5 2.5 0 005 0V4A2.5 2.5 0 008 1.5z\"/><path d=\"M2.25 6v1a5.75 5.75 0 0011.5 0V6h1.5v1a7.251 7.251 0 01-6.5 7.212V16h-1.5v-1.788A7.251 7.251 0 01.75 7V6h1.5z\"/>',\n\tvolume:\n\t\t'<path d=\"M12.945 1.379l-.652.763c1.577 1.462 2.57 3.544 2.57 5.858s-.994 4.396-2.57 5.858l.651.763a8.966 8.966 0 00.001-13.242zm-2.272 2.66l-.651.763a4.484 4.484 0 01-.001 6.397l.651.763c1.04-1 1.691-2.404 1.691-3.961s-.65-2.962-1.69-3.962zM0 5v6h2.804L8 14V2L2.804 5H0zm7-1.268v8.536L3.072 10H1V6h2.072L7 3.732z\"/>',\n\t\"volume-off\":\n\t\t'<path d=\"M0 5v6h2.804L8 14V2L2.804 5H0zm7-1.268v8.536L3.072 10H1V6h2.072L7 3.732zm8.623 2.121l-.707-.707-2.147 2.147-2.146-2.147-.707.707L12.062 8l-2.146 2.146.707.707 2.146-2.147 2.147 2.147.707-.707L13.477 8l2.146-2.147z\"/>',\n\t\"volume-one-wave\":\n\t\t'<path d=\"M10.04 5.984l.658-.77q.548.548.858 1.278.31.73.31 1.54 0 .54-.144 1.055-.143.516-.4.957-.259.44-.624.805l-.658-.77q.825-.865.825-2.047 0-1.183-.825-2.048zM0 11.032v-6h2.802l5.198-3v12l-5.198-3H0zm7 1.27v-8.54l-3.929 2.27H1v4h2.071L7 12.302z\"/>',\n\t\"volume-two-wave\":\n\t\t'<path d=\"M0 11.032v-6h2.802l5.198-3v12l-5.198-3H0zm7 1.27v-8.54l-3.929 2.27H1v4h2.071L7 12.302zm4.464-2.314q.401-.925.401-1.956 0-1.032-.4-1.957-.402-.924-1.124-1.623L11 3.69q.873.834 1.369 1.957.496 1.123.496 2.385 0 1.262-.496 2.385-.496 1.123-1.369 1.956l-.659-.762q.722-.698 1.123-1.623z\"/>',\n\twatch:\n\t\t'<path d=\"M4.347 1.122l-.403 1.899A2.25 2.25 0 002 5.25v5.5a2.25 2.25 0 001.944 2.23l.403 1.898c.14.654.717 1.122 1.386 1.122h4.535c.668 0 1.246-.468 1.385-1.122l.404-1.899A2.25 2.25 0 0014 10.75v-5.5a2.25 2.25 0 00-1.943-2.23l-.404-1.898A1.417 1.417 0 0010.267 0H5.734c-.67 0-1.247.468-1.386 1.122zM5.8 1.5h4.4l.319 1.5H5.48l.32-1.5zM10.52 13l-.319 1.5H5.8L5.481 13h5.038zM4.25 4.5h7.5a.75.75 0 01.75.75v5.5a.75.75 0 01-.75.75h-7.5a.75.75 0 01-.75-.75v-5.5a.75.75 0 01.75-.75z\"/>',\n\tx: '<path d=\"M14.354 2.353l-.708-.707L8 7.293 2.353 1.646l-.707.707L7.293 8l-5.647 5.646.707.708L8 8.707l5.646 5.647.708-.708L8.707 8z\"/>',\n};\n\n(async function waitUserAPI() {\n\tif (!Spicetify.Platform?.UserAPI) {\n\t\tsetTimeout(waitUserAPI, 1000);\n\t\treturn;\n\t}\n\n\tlet subRequest;\n\n\t// product_state was renamed to product_state_service in Spotify 1.2.21\n\tconst productState =\n\t\tSpicetify.Platform.UserAPI?._product_state ||\n\t\tSpicetify.Platform.UserAPI?._product_state_service ||\n\t\tSpicetify.Platform?.ProductStateAPI.productStateApi;\n\n\tSpicetify.AppTitle = {\n\t\tset: async (name) => {\n\t\t\tif (subRequest) subRequest.cancel();\n\t\t\tawait productState.putOverridesValues({ pairs: { name } });\n\t\t\tsubRequest = productState.subValues({ keys: [\"name\"] }, ({ pairs }) => {\n\t\t\t\tif (pairs.name !== name) {\n\t\t\t\t\tproductState.putOverridesValues({ pairs: { name } }); // Restore name\n\t\t\t\t}\n\t\t\t});\n\t\t\treturn subRequest;\n\t\t},\n\t\tget: async () => {\n\t\t\tconst value = await productState.getValues();\n\t\t\treturn value.pairs.name;\n\t\t},\n\t\treset: async () => {\n\t\t\tif (subRequest) subRequest.cancel();\n\t\t\tawait productState.delOverridesValues({ keys: [\"name\"] });\n\t\t},\n\t\tsub: (callback) => {\n\t\t\treturn productState.subValues({ keys: [\"name\"] }, ({ pairs }) => {\n\t\t\t\tcallback(pairs.name);\n\t\t\t});\n\t\t},\n\t};\n})();\n\nfunction parseIcon(icon, size = 16) {\n\tif (icon && Spicetify.SVGIcons[icon]) {\n\t\treturn `<svg height=\"${size}\" width=\"${size}\" viewBox=\"0 0 ${size} ${size}\" fill=\"currentColor\">${Spicetify.SVGIcons[icon]}</svg>`;\n\t}\n\treturn icon || \"\";\n}\n\nfunction createIconComponent(icon, size = 16) {\n\treturn Spicetify.React.createElement(\n\t\tSpicetify.ReactComponent.IconComponent,\n\t\t{\n\t\t\ticonSize: size,\n\t\t\tdangerouslySetInnerHTML: {\n\t\t\t\t__html: parseIcon(icon),\n\t\t\t},\n\t\t},\n\t\tnull\n\t);\n}\n\nSpicetify.ContextMenuV2 = (() => {\n\tconst registeredItems = new Map();\n\n\tfunction parseProps(props) {\n\t\tif (!props) return;\n\n\t\tconst uri = props.uri ?? props.item?.uri ?? props.reference?.uri;\n\t\tconst uris = props.uris ?? (uri ? [uri] : undefined);\n\t\tif (!uris) return;\n\n\t\tconst uid = props.uid ?? props.item?.uid;\n\t\tconst uids = props.uids ?? (uid ? [uid] : undefined);\n\n\t\tconst contextUri = props.contextUri ?? props.context?.uri;\n\n\t\treturn [uris, uids, contextUri];\n\t}\n\n\t// these classes bridge the gap between react and js, insuring reactivity\n\tclass Item {\n\t\tconstructor({ children, disabled = false, leadingIcon, trailingIcon, divider, onClick, shouldAdd = () => true }) {\n\t\t\t// maybe use a props object and a setProps\n\t\t\tthis.shouldAdd = shouldAdd;\n\n\t\t\tthis._children = children;\n\t\t\tthis._disabled = disabled;\n\t\t\tthis._leadingIcon = leadingIcon;\n\t\t\tthis._trailingIcon = trailingIcon;\n\t\t\tthis._divider = divider;\n\n\t\t\tthis._element = Spicetify.ReactJSX.jsx(() => {\n\t\t\t\tconst [_children, setChildren] = Spicetify.React.useState(this._children);\n\t\t\t\tconst [_disabled, setDisabled] = Spicetify.React.useState(this._disabled);\n\t\t\t\tconst [_leadingIcon, setLeadingIcon] = Spicetify.React.useState(this._leadingIcon);\n\t\t\t\tconst [_trailingIcon, setTrailingIcon] = Spicetify.React.useState(this._trailingIcon);\n\t\t\t\tconst [_divider, setDivider] = Spicetify.React.useState(this._divider);\n\n\t\t\t\tSpicetify.React.useEffect(() => {\n\t\t\t\t\tthis._setChildren = setChildren;\n\t\t\t\t\tthis._setDisabled = setDisabled;\n\t\t\t\t\tthis._setIcon = setLeadingIcon;\n\t\t\t\t\tthis._setTrailingIcon = setTrailingIcon;\n\t\t\t\t\tthis._setDivider = setDivider;\n\n\t\t\t\t\treturn () => {\n\t\t\t\t\t\tthis._setChildren = undefined;\n\t\t\t\t\t\tthis._setDisabled = undefined;\n\t\t\t\t\t\tthis._setIcon = undefined;\n\t\t\t\t\t\tthis._setTrailingIcon = undefined;\n\t\t\t\t\t\tthis._setDivider = undefined;\n\t\t\t\t\t};\n\t\t\t\t});\n\n\t\t\t\tconst context = Spicetify.React.useContext(Spicetify.ContextMenuV2._context) ?? {};\n\n\t\t\t\treturn Spicetify.React.createElement(Spicetify.ReactComponent.MenuItem, {\n\t\t\t\t\tdisabled: _disabled,\n\t\t\t\t\tdivider: _divider,\n\t\t\t\t\tonClick: (e) => {\n\t\t\t\t\t\tonClick(context, this, e);\n\t\t\t\t\t},\n\t\t\t\t\tleadingIcon: _leadingIcon && createIconComponent(_leadingIcon),\n\t\t\t\t\ttrailingIcon: _trailingIcon && createIconComponent(_trailingIcon),\n\t\t\t\t\tchildren: _children,\n\t\t\t\t});\n\t\t\t}, {});\n\t\t}\n\n\t\tset children(children) {\n\t\t\tthis._children = children;\n\t\t\tthis._setChildren?.(this._children);\n\t\t}\n\t\tget children() {\n\t\t\treturn this._children;\n\t\t}\n\n\t\tset disabled(bool) {\n\t\t\tthis._disabled = bool;\n\t\t\tthis._setDisabled?.(this._disabled);\n\t\t}\n\t\tget disabled() {\n\t\t\treturn this._disabled;\n\t\t}\n\n\t\tset leadingIcon(name) {\n\t\t\tthis._leadingIcon = name;\n\t\t\tthis._setIcon?.(this._leadingIcon);\n\t\t}\n\t\tget leadingIcon() {\n\t\t\treturn this._leadingIcon;\n\t\t}\n\n\t\tset trailingIcon(name) {\n\t\t\tthis._trailingIcon = name;\n\t\t\tthis._setTrailingIcon?.(this._trailingIcon);\n\t\t}\n\t\tget trailingIcon() {\n\t\t\treturn this._trailingIcon;\n\t\t}\n\n\t\tset divider(divider) {\n\t\t\tthis._divider = divider;\n\t\t\tthis._setDivider?.(this._divider);\n\t\t}\n\t\tget divider() {\n\t\t\treturn this._divider;\n\t\t}\n\n\t\tregister() {\n\t\t\tSpicetify.ContextMenuV2.registerItem(this._element, this.shouldAdd);\n\t\t}\n\t\tderegister() {\n\t\t\tSpicetify.ContextMenuV2.unregisterItem(this._element);\n\t\t}\n\t}\n\n\tclass ItemSubMenu {\n\t\tstatic itemsToComponents(items, props, trigger, target, parentDepth = 1) {\n\t\t\treturn items\n\t\t\t\t.filter((item) => (item.shouldAdd || (() => true))?.(props, trigger, target))\n\t\t\t\t.map((item) => {\n\t\t\t\t\tif (item instanceof ItemSubMenu) item.depth = parentDepth + 1;\n\t\t\t\t\treturn item._element;\n\t\t\t\t});\n\t\t}\n\n\t\tconstructor({ text, disabled = false, leadingIcon, divider, items, depth = 1, shouldAdd = () => true }) {\n\t\t\tthis.shouldAdd = shouldAdd;\n\n\t\t\tthis._text = text;\n\t\t\tthis._disabled = disabled;\n\t\t\tthis._leadingIcon = leadingIcon;\n\t\t\tthis._divider = divider;\n\t\t\tthis._items = items;\n\t\t\tthis._depth = depth;\n\t\t\tthis._element = Spicetify.ReactJSX.jsx(() => {\n\t\t\t\tconst [_text, setText] = Spicetify.React.useState(this._text);\n\t\t\t\tconst [_disabled, setDisabled] = Spicetify.React.useState(this._disabled);\n\t\t\t\tconst [_leadingIcon, setLeadingIcon] = Spicetify.React.useState(this._leadingIcon);\n\t\t\t\tconst [_divider, setDivider] = Spicetify.React.useState(this._divider);\n\t\t\t\tconst [_items, setItems] = Spicetify.React.useState(this._items);\n\t\t\t\tconst [_depth, setDepth] = Spicetify.React.useState(this._depth);\n\n\t\t\t\tSpicetify.React.useEffect(() => {\n\t\t\t\t\tthis._setText = setText;\n\t\t\t\t\tthis._setDisabled = setDisabled;\n\t\t\t\t\tthis._setLeadingIcon = setLeadingIcon;\n\t\t\t\t\tthis._setDivider = setDivider;\n\t\t\t\t\tthis._setItems = setItems;\n\t\t\t\t\tthis._setDepth = setDepth;\n\t\t\t\t\treturn () => {\n\t\t\t\t\t\tthis._setText = undefined;\n\t\t\t\t\t\tthis._setDisabled = undefined;\n\t\t\t\t\t\tthis._setLeadingIcon = undefined;\n\t\t\t\t\t\tthis._setDivider = undefined;\n\t\t\t\t\t\tthis._setItems = undefined;\n\t\t\t\t\t\tthis._setDepth = undefined;\n\t\t\t\t\t};\n\t\t\t\t});\n\n\t\t\t\tconst context = Spicetify.React.useContext(Spicetify.ContextMenuV2._context) ?? {};\n\t\t\t\tconst { props, trigger, target } = context;\n\n\t\t\t\treturn Spicetify.React.createElement(Spicetify.ReactComponent.MenuSubMenuItem, {\n\t\t\t\t\tdisplayText: _text,\n\t\t\t\t\tdivider: _divider,\n\t\t\t\t\tdepth: _depth,\n\t\t\t\t\tplacement: \"right-start\",\n\t\t\t\t\tonOpenChange: () => undefined,\n\t\t\t\t\tonClick: () => undefined,\n\t\t\t\t\tdisabled: _disabled,\n\t\t\t\t\tleadingIcon: _leadingIcon && createIconComponent(_leadingIcon),\n\t\t\t\t\tchildren: ItemSubMenu.itemsToComponents(_items, props, trigger, target, _depth),\n\t\t\t\t});\n\t\t\t}, {});\n\t\t}\n\n\t\tset text(text) {\n\t\t\tthis._text = text;\n\t\t\tthis._setText?.(this._text);\n\t\t}\n\t\tget text() {\n\t\t\treturn this._text;\n\t\t}\n\n\t\tset disabled(bool) {\n\t\t\tthis._disabled = bool;\n\t\t\tthis._setDisabled?.(this._disabled);\n\t\t}\n\t\tget disabled() {\n\t\t\treturn this._disabled;\n\t\t}\n\n\t\tset leadingIcon(name) {\n\t\t\tthis._leadingIcon = name;\n\t\t\tthis._setIcon?.(this._leadingIcon);\n\t\t}\n\t\tget leadingIcon() {\n\t\t\treturn this._leadingIcon;\n\t\t}\n\n\t\tset divider(divider) {\n\t\t\tthis._divider = divider;\n\t\t\tthis._setDivider?.(this._divider);\n\t\t}\n\t\tget divider() {\n\t\t\treturn this._divider;\n\t\t}\n\n\t\tset depth(value) {\n\t\t\tthis._depth = value;\n\t\t\tthis._setDepth?.(this._depth);\n\t\t}\n\t\tget depth() {\n\t\t\treturn this._depth;\n\t\t}\n\n\t\taddItem(item) {\n\t\t\tthis._items.add(item);\n\t\t\tthis._setItems?.(this._items);\n\t\t}\n\t\tremoveItem(item) {\n\t\t\tthis._items.delete(item);\n\t\t\tthis._setItems?.(this._items);\n\t\t}\n\n\t\tregister() {\n\t\t\tregisterItem(this._element, this.shouldAdd);\n\t\t}\n\t\tderegister() {\n\t\t\tunregisterItem(this._element);\n\t\t}\n\t}\n\n\tfunction registerItem(item, shouldAdd = () => true) {\n\t\tregisteredItems.set(item, shouldAdd);\n\t}\n\n\tfunction unregisterItem(item) {\n\t\tregisteredItems.delete(item);\n\t}\n\n\tconst renderItems = () => {\n\t\tconst { props, trigger, target } = Spicetify.React.useContext(Spicetify.ContextMenuV2._context) ?? {};\n\n\t\treturn Array.from(registeredItems.entries())\n\t\t\t.map(([item, shouldAdd]) => shouldAdd?.(props, trigger, target) && item)\n\t\t\t.filter(Boolean);\n\t};\n\n\treturn { parseProps, Item, ItemSubMenu, registerItem, unregisterItem, renderItems };\n})();\n\nSpicetify.Menu = (() => {\n\tconst shouldAdd = (_, trigger, target) =>\n\t\ttrigger === \"click\" && (target.classList.contains(\"main-userWidget-boxCondensed\") || target.classList.contains(\"main-userWidget-box\"));\n\n\tclass Item extends Spicetify.ContextMenuV2.Item {\n\t\tconstructor(children, isEnabled, onClick, leadingIcon) {\n\t\t\tsuper({ children, leadingIcon, onClick: (_, self) => onClick(self), shouldAdd });\n\n\t\t\tthis.isEnabled = isEnabled;\n\t\t}\n\n\t\tsetState(state) {\n\t\t\tthis.isEnabled = state;\n\t\t}\n\n\t\tset isEnabled(bool) {\n\t\t\tthis._isEnabled = bool;\n\t\t\tthis.trailingIcon = this.isEnabled ? \"check\" : \"\";\n\t\t}\n\t\tget isEnabled() {\n\t\t\treturn this._isEnabled;\n\t\t}\n\t}\n\n\tclass SubMenu extends Spicetify.ContextMenuV2.ItemSubMenu {\n\t\tconstructor(text, items, leadingIcon) {\n\t\t\tsuper({ text, leadingIcon, items, shouldAdd });\n\t\t}\n\n\t\tset name(text) {\n\t\t\tthis.text = text;\n\t\t}\n\t\tget name() {\n\t\t\treturn this.text;\n\t\t}\n\n\t\tset icon(icon) {\n\t\t\tthis.leadingIcon = icon;\n\t\t}\n\t\tget icon() {\n\t\t\treturn this.leadingIcon;\n\t\t}\n\t}\n\n\treturn { Item, SubMenu };\n})();\n\nSpicetify.ContextMenu = (() => {\n\tconst iconList = Object.keys(Spicetify.SVGIcons);\n\n\tclass Item extends Spicetify.ContextMenuV2.Item {\n\t\tstatic iconList = iconList;\n\n\t\tconstructor(name, onClick, shouldAdd = () => true, icon = undefined, trailingIcon = undefined, disabled = false) {\n\t\t\tsuper({\n\t\t\t\tchildren: name,\n\t\t\t\tdisabled,\n\t\t\t\tleadingIcon: icon,\n\t\t\t\ttrailingIcon,\n\t\t\t\tonClick: (context) => {\n\t\t\t\t\tconst [uris, uids, contextUri] = Spicetify.ContextMenuV2.parseProps(context.props);\n\t\t\t\t\tonClick(uris, uids, contextUri);\n\t\t\t\t},\n\t\t\t\tshouldAdd: (props) => {\n\t\t\t\t\tconst parsedProps = Spicetify.ContextMenuV2.parseProps(props);\n\t\t\t\t\treturn parsedProps && shouldAdd(...parsedProps);\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\tset name(name) {\n\t\t\tthis.children = name;\n\t\t}\n\t\tget name() {\n\t\t\treturn this.children;\n\t\t}\n\n\t\tset icon(name) {\n\t\t\tthis.leadingIcon = name;\n\t\t}\n\t\tget icon() {\n\t\t\treturn this.leadingIcon;\n\t\t}\n\t}\n\n\tclass SubMenu extends Spicetify.ContextMenuV2.ItemSubMenu {\n\t\tstatic iconList = iconList;\n\n\t\tconstructor(name, items, shouldAdd, disabled = false, icon = undefined) {\n\t\t\tsuper({\n\t\t\t\ttext: name,\n\t\t\t\tdisabled,\n\t\t\t\tleadingIcon: icon,\n\t\t\t\titems,\n\t\t\t\tshouldAdd: (props) => {\n\t\t\t\t\tconst parsedProps = Spicetify.ContextMenuV2.parseProps(props);\n\t\t\t\t\treturn parsedProps && shouldAdd(...parsedProps);\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\tset name(name) {\n\t\t\tthis.text = name;\n\t\t}\n\t\tget name() {\n\t\t\treturn this.text;\n\t\t}\n\t}\n\n\treturn { Item, SubMenu };\n})();\n\nlet navLinkFactoryCtx = null;\nlet refreshNavLinks = null;\n\nSpicetify._renderNavLinks = (list, isTouchScreenUi) => {\n\tconst [refreshCount, refresh] = Spicetify.React.useReducer((x) => x + 1, 0);\n\trefreshNavLinks = refresh;\n\n\tif (\n\t\t!Spicetify.ReactComponent.ButtonTertiary ||\n\t\t!Spicetify.ReactComponent.Navigation ||\n\t\t!Spicetify.ReactComponent.TooltipWrapper ||\n\t\t!Spicetify.ReactComponent.ScrollableContainer ||\n\t\t!Spicetify.Platform.History ||\n\t\t!Spicetify.Platform.LocalStorageAPI\n\t)\n\t\treturn;\n\n\tconst navLinkFactory = isTouchScreenUi ? NavLinkGlobal : NavLinkSidebar;\n\n\tif (!navLinkFactoryCtx) navLinkFactoryCtx = Spicetify.React.createContext(null);\n\tconst registered = [];\n\n\tfor (const app of list) {\n\t\tlet manifest;\n\t\ttry {\n\t\t\tconst request = new XMLHttpRequest();\n\t\t\trequest.open(\"GET\", `spicetify-routes-${app}.json`, false);\n\t\t\trequest.send(null);\n\t\t\tmanifest = JSON.parse(request.responseText);\n\t\t} catch {\n\t\t\tmanifest = {};\n\t\t}\n\n\t\tlet appProper = manifest.name;\n\t\tif (typeof appProper === \"object\") {\n\t\t\tappProper = appProper[Spicetify.Locale?.getLocale()] || appProper.en;\n\t\t}\n\t\tif (!appProper) {\n\t\t\tappProper = app[0].toUpperCase() + app.slice(1);\n\t\t}\n\t\tconst icon = manifest.icon || \"\";\n\t\tconst activeIcon = manifest[\"active-icon\"] || icon;\n\t\tconst appRoutePath = `/${app}`;\n\t\tregistered.push({ appProper, appRoutePath, icon, activeIcon });\n\t}\n\n\t(function addStyling() {\n\t\tif (document.querySelector(\"style.spicetify-navlinks\")) return;\n\t\tconst style = document.createElement(\"style\");\n\t\tstyle.className = \"spicetify-navlinks\";\n\t\tstyle.innerHTML = `\n\t:root {\n\t\t--max-custom-navlink-count: 4;\n\t}\n\n\t.custom-navlinks-scrollable_container {\n\t\tmax-width: calc(48px * var(--max-custom-navlink-count) + 8px * (var(--max-custom-navlink-count) - 1));\n\t\t-webkit-app-region: no-drag;\n\t}\n\n\t.custom-navlinks-scrollable_container div[role=\"presentation\"] > *:not(:last-child) {\n\t\tmargin-inline-end: 8px;\n\t}\n\n\t.custom-navlinks-scrollable_container div[role=\"presentation\"] {\n\t\tdisplay: flex;\n\t\tflex-direction: row;\n\t}\n\n\t.custom-navlink {\n\t\t-webkit-app-region: unset;\n\t}\n\t\t`;\n\t\tdocument.head.appendChild(style);\n\t})();\n\n\tconst wrapScrollableContainer = (element) =>\n\t\tSpicetify.React.createElement(\n\t\t\t\"div\",\n\t\t\t{ className: \"custom-navlinks-scrollable_container\" },\n\t\t\tSpicetify.React.createElement(Spicetify.ReactComponent.ScrollableContainer, null, element)\n\t\t);\n\n\tconst NavLinks = () =>\n\t\tSpicetify.React.createElement(\n\t\t\tnavLinkFactoryCtx.Provider,\n\t\t\t{ value: navLinkFactory },\n\t\t\tregistered.map((NavLinkElement) => Spicetify.React.createElement(NavLink, NavLinkElement, null))\n\t\t);\n\n\treturn isTouchScreenUi ? wrapScrollableContainer(NavLinks()) : NavLinks();\n};\n\nconst NavLink = ({ appProper, appRoutePath, icon, activeIcon }) => {\n\tconst isActive = Spicetify.Platform.History.location.pathname?.startsWith(appRoutePath);\n\tconst createIcon = () => createIconComponent(isActive ? activeIcon : icon, 24);\n\n\tconst NavLinkFactory = Spicetify.React.useContext(navLinkFactoryCtx);\n\n\treturn NavLinkFactory && Spicetify.React.createElement(NavLinkFactory, { appProper, appRoutePath, createIcon, isActive }, null);\n};\n\nconst NavLinkSidebar = ({ appProper, appRoutePath, createIcon, isActive }) => {\n\tconst isSidebarCollapsed = Spicetify.Platform.LocalStorageAPI.getItem(\"ylx-sidebar-state\") === 1;\n\n\treturn Spicetify.React.createElement(\n\t\t\"li\",\n\t\t{ className: \"main-yourLibraryX-navItem InvalidDropTarget\" },\n\t\tSpicetify.React.createElement(\n\t\t\tSpicetify.ReactComponent.TooltipWrapper,\n\t\t\t{ label: isSidebarCollapsed ? appProper : null, disabled: !isSidebarCollapsed, placement: \"right\" },\n\t\t\tSpicetify.React.createElement(\n\t\t\t\tSpicetify.ReactComponent.Navigation,\n\t\t\t\t{\n\t\t\t\t\tto: appRoutePath,\n\t\t\t\t\treferrer: \"other\",\n\t\t\t\t\tclassName: Spicetify.classnames(\"link-subtle\", \"main-yourLibraryX-navLink\", {\n\t\t\t\t\t\t\"main-yourLibraryX-navLinkActive\": isActive,\n\t\t\t\t\t}),\n\t\t\t\t\tonClick: () => undefined,\n\t\t\t\t\t\"aria-label\": appProper,\n\t\t\t\t},\n\t\t\t\tcreateIcon(),\n\t\t\t\t!isSidebarCollapsed && Spicetify.React.createElement(Spicetify.ReactComponent.TextComponent, { variant: \"balladBold\" }, appProper)\n\t\t\t)\n\t\t)\n\t);\n};\n\nconst NavLinkGlobal = ({ appProper, appRoutePath, createIcon, isActive }) => {\n\treturn Spicetify.React.createElement(\n\t\tSpicetify.ReactComponent.TooltipWrapper,\n\t\t{ label: appProper },\n\t\tSpicetify.React.createElement(Spicetify.ReactComponent.ButtonTertiary, {\n\t\t\ticonOnly: createIcon,\n\t\t\tclassName: Spicetify.classnames(\"link-subtle\", \"main-globalNav-navLink\", \"main-globalNav-link-icon\", \"custom-navlink\", {\n\t\t\t\t\"main-globalNav-navLinkActive\": isActive,\n\t\t\t}),\n\t\t\t\"aria-label\": appProper,\n\t\t\tonClick: () => Spicetify.Platform.History.push(appRoutePath),\n\t\t})\n\t);\n};\n\nclass _HTMLGenericModal extends HTMLElement {\n\thide() {\n\t\tSpicetify.ReactDOM.unmountComponentAtNode(this.querySelector(\"main\"));\n\t\tthis.remove();\n\t}\n\n\tdisplay({ title, content, isLarge = false }) {\n\t\tthis.innerHTML = `\n<div class=\"GenericModal__overlay\" style=\"z-index: 100;\">\n\t<div class=\"GenericModal\" tabindex=\"-1\" role=\"dialog\" aria-label=\"${title}\" aria-modal=\"true\">\n\t\t<div class=\"${isLarge ? \"main-embedWidgetGenerator-container\" : \"main-trackCreditsModal-container\"}\">\n\t\t\t<div class=\"main-trackCreditsModal-header\">\n\t\t\t\t<h1 class=\"main-type-alto\" as=\"h1\">${title}</h1>\n\t\t\t\t<button aria-label=\"Close\" class=\"main-trackCreditsModal-closeBtn\"><svg width=\"18\" height=\"18\" viewBox=\"0 0 32 32\" xmlns=\"http://www.w3.org/2000/svg\"><title>Close</title><path d=\"M31.098 29.794L16.955 15.65 31.097 1.51 29.683.093 15.54 14.237 1.4.094-.016 1.508 14.126 15.65-.016 29.795l1.414 1.414L15.54 17.065l14.144 14.143\" fill=\"currentColor\" fill-rule=\"evenodd\"></path></svg></button>\n\t\t\t</div>\n\t\t\t<div class=\"main-trackCreditsModal-mainSection\">\n\t\t\t\t<main class=\"main-trackCreditsModal-originalCredits\"></main>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n</div>`;\n\n\t\tthis.querySelector(\"button\").onclick = this.hide.bind(this);\n\t\tconst main = this.querySelector(\"main\");\n\n\t\tconst hidePopup = this.hide.bind(this);\n\n\t\t// Listen for click events on Overlay\n\t\tthis.querySelector(\".GenericModal__overlay\").addEventListener(\"click\", (event) => {\n\t\t\tif (!this.querySelector(\".GenericModal\").contains(event.target)) hidePopup();\n\t\t});\n\n\t\tif (Spicetify.React.isValidElement(content)) {\n\t\t\tSpicetify.ReactDOM.render(content, main);\n\t\t} else if (typeof content === \"string\") {\n\t\t\tmain.innerHTML = content;\n\t\t} else {\n\t\t\tmain.append(content);\n\t\t}\n\t\tdocument.body.append(this);\n\t}\n}\ncustomElements.define(\"generic-modal\", _HTMLGenericModal);\nSpicetify.PopupModal = new _HTMLGenericModal();\n\nObject.defineProperty(Spicetify, \"TippyProps\", {\n\tvalue: {\n\t\tdelay: [200, 0],\n\t\tanimation: true,\n\t\trender(instance) {\n\t\t\tconst popper = document.createElement(\"div\");\n\t\t\tconst box = document.createElement(\"div\");\n\n\t\t\tpopper.id = \"context-menu\";\n\t\t\tpopper.appendChild(box);\n\n\t\t\tbox.className = \"main-contextMenu-tippy\";\n\t\t\tbox[instance.props.allowHTML ? \"innerHTML\" : \"textContent\"] = instance.props.content;\n\n\t\t\tfunction onUpdate(prevProps, nextProps) {\n\t\t\t\tif (prevProps.content !== nextProps.content) {\n\t\t\t\t\tif (nextProps.allowHTML) box.innerHTML = nextProps.content;\n\t\t\t\t\telse box.textContent = nextProps.content;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { popper, onUpdate };\n\t\t},\n\t\tonShow(instance) {\n\t\t\tinstance.popper.firstChild.classList.add(\"main-contextMenu-tippyEnter\");\n\t\t},\n\t\tonMount(instance) {\n\t\t\trequestAnimationFrame(() => {\n\t\t\t\tinstance.popper.firstChild.classList.remove(\"main-contextMenu-tippyEnter\");\n\t\t\t\tinstance.popper.firstChild.classList.add(\"main-contextMenu-tippyEnterActive\");\n\t\t\t});\n\t\t},\n\t\tonHide(instance) {\n\t\t\trequestAnimationFrame(() => {\n\t\t\t\tinstance.popper.firstChild.classList.remove(\"main-contextMenu-tippyEnterActive\");\n\t\t\t\tinstance.unmount();\n\t\t\t});\n\t\t},\n\t},\n\twritable: false,\n});\n\nSpicetify.Topbar = (() => {\n\tlet leftGeneratedClassName;\n\tlet rightGeneratedClassName;\n\tlet leftContainer;\n\tlet rightContainer;\n\tconst leftButtonsStash = new Set();\n\tconst rightButtonsStash = new Set();\n\n\tclass Button {\n\t\tconstructor(label, icon, onClick, disabled = false, isRight = false) {\n\t\t\tthis.element = document.createElement(\"div\");\n\t\t\tthis.button = document.createElement(\"button\");\n\t\t\tthis.icon = icon;\n\t\t\tthis.onClick = onClick;\n\t\t\tthis.disabled = disabled;\n\t\t\tthis.tippy = Spicetify.Tippy?.(this.element, {\n\t\t\t\tcontent: label,\n\t\t\t\t...Spicetify.TippyProps,\n\t\t\t});\n\t\t\tthis.label = label;\n\n\t\t\tthis.element.appendChild(this.button);\n\t\t\tif (isRight) {\n\t\t\t\tthis.button.className = rightGeneratedClassName;\n\t\t\t\trightButtonsStash.add(this.element);\n\t\t\t\trightContainer?.prepend(this.element);\n\t\t\t} else {\n\t\t\t\tthis.button.className = leftGeneratedClassName;\n\t\t\t\tleftButtonsStash.add(this.element);\n\t\t\t\tleftContainer?.append(this.element);\n\t\t\t}\n\t\t}\n\t\tget label() {\n\t\t\treturn this._label;\n\t\t}\n\t\tset label(text) {\n\t\t\tthis._label = text;\n\t\t\tthis.button.setAttribute(\"aria-label\", text);\n\t\t\tif (!this.tippy) this.button.setAttribute(\"title\", text);\n\t\t\telse this.tippy.setContent(text);\n\t\t}\n\t\tget icon() {\n\t\t\treturn this._icon;\n\t\t}\n\t\tset icon(input) {\n\t\t\tlet newInput = input;\n\t\t\tif (newInput && Spicetify.SVGIcons[newInput]) {\n\t\t\t\tnewInput = `<svg height=\"16\" width=\"16\" viewBox=\"0 0 16 16\" fill=\"currentColor\">${Spicetify.SVGIcons[newInput]}</svg>`;\n\t\t\t}\n\t\t\tthis._icon = newInput;\n\t\t\tthis.button.innerHTML = newInput;\n\t\t}\n\t\tget onClick() {\n\t\t\treturn this._onClick;\n\t\t}\n\t\tset onClick(func) {\n\t\t\tthis._onClick = func;\n\t\t\tthis.button.onclick = () => this._onClick(this);\n\t\t}\n\t\tget disabled() {\n\t\t\treturn this._disabled;\n\t\t}\n\t\tset disabled(bool) {\n\t\t\tthis._disabled = bool;\n\t\t\tthis.button.disabled = bool;\n\t\t\tthis.button.classList.toggle(\"disabled\", bool);\n\t\t}\n\t}\n\n\tfunction waitForTopbarMounted() {\n\t\tconst globalHistoryButtons = document.querySelector(\".main-globalNav-historyButtons\");\n\t\tleftGeneratedClassName = document.querySelector(\n\t\t\t\".main-topBar-historyButtons .main-topBar-button, .main-globalNav-historyButtons .main-globalNav-icon, .main-globalNav-historyButtons [data-encore-id='buttonTertiary']\"\n\t\t)?.className;\n\t\trightGeneratedClassName = document.querySelector(\n\t\t\t\".main-topBar-container .main-topBar-buddyFeed, .main-actionButtons .main-topBar-buddyFeed, .main-actionButtons .main-globalNav-buddyFeed\"\n\t\t)?.className;\n\t\tleftContainer = document.querySelector(\".main-topBar-historyButtons\") ?? globalHistoryButtons;\n\t\trightContainer = document.querySelector(\".main-actionButtons\");\n\t\tif (!leftContainer || !rightContainer || !leftGeneratedClassName || !rightGeneratedClassName) {\n\t\t\tsetTimeout(waitForTopbarMounted, 100);\n\t\t\treturn;\n\t\t}\n\n\t\tif (globalHistoryButtons) globalHistoryButtons.style = \"gap: 4px; padding-inline: 4px 4px\";\n\t\tfor (const button of leftButtonsStash) {\n\t\t\tif (button.parentNode) button.parentNode.removeChild(button);\n\n\t\t\tconst buttonElement = button.querySelector(\"button\");\n\t\t\tbuttonElement.className = leftGeneratedClassName;\n\t\t}\n\t\tleftContainer.append(...leftButtonsStash);\n\t\tfor (const button of rightButtonsStash) {\n\t\t\tif (button.parentNode) button.parentNode.removeChild(button);\n\n\t\t\tconst buttonElement = button.querySelector(\"button\");\n\t\t\tbuttonElement.className = rightGeneratedClassName;\n\t\t}\n\t\trightContainer.prepend(...rightButtonsStash);\n\t}\n\n\twaitForTopbarMounted();\n\t(function waitForPlatform() {\n\t\tif (!Spicetify.Platform?.History) {\n\t\t\tsetTimeout(waitForPlatform, 100);\n\t\t\treturn;\n\t\t}\n\n\t\tSpicetify.Platform.History.listen(() => waitForTopbarMounted());\n\t})();\n\n\treturn { Button };\n})();\n\nSpicetify.Playbar = (() => {\n\tlet rightContainer;\n\tlet sibling;\n\tconst buttonsStash = new Set();\n\n\tclass Button {\n\t\tconstructor(label, icon, onClick = () => {}, disabled = false, active = false, registerOnCreate = true) {\n\t\t\tthis.element = document.createElement(\"button\");\n\t\t\tthis.element.classList.add(\"main-genericButton-button\");\n\t\t\tthis.iconElement = document.createElement(\"span\");\n\t\t\tthis.iconElement.classList.add(\"Wrapper-sm-only\", \"Wrapper-small-only\");\n\t\t\tthis.element.appendChild(this.iconElement);\n\t\t\tthis.icon = icon;\n\t\t\tthis.onClick = onClick;\n\t\t\tthis.disabled = disabled;\n\t\t\tthis.active = active;\n\t\t\taddClassname(this.element);\n\t\t\tthis.tippy = Spicetify.Tippy?.(this.element, {\n\t\t\t\tcontent: label,\n\t\t\t\t...Spicetify.TippyProps,\n\t\t\t});\n\t\t\tthis.label = label;\n\t\t\tregisterOnCreate && this.register();\n\t\t}\n\t\tget label() {\n\t\t\treturn this._label;\n\t\t}\n\t\tset label(text) {\n\t\t\tthis._label = text;\n\t\t\tif (!this.tippy) this.element.setAttribute(\"title\", text);\n\t\t\telse this.tippy.setContent(text);\n\t\t}\n\t\tget icon() {\n\t\t\treturn this._icon;\n\t\t}\n\t\tset icon(input) {\n\t\t\tlet newInput = input;\n\t\t\tif (newInput && Spicetify.SVGIcons[newInput]) {\n\t\t\t\tnewInput = `<svg height=\"16\" width=\"16\" viewBox=\"0 0 16 16\" fill=\"currentColor\" stroke=\"currentColor\">${Spicetify.SVGIcons[newInput]}</svg>`;\n\t\t\t}\n\t\t\tthis._icon = newInput;\n\t\t\tthis.iconElement.innerHTML = newInput;\n\t\t}\n\t\tget onClick() {\n\t\t\treturn this._onClick;\n\t\t}\n\t\tset onClick(func) {\n\t\t\tthis._onClick = func;\n\t\t\tthis.element.onclick = () => this._onClick(this);\n\t\t}\n\t\tget disabled() {\n\t\t\treturn this._disabled;\n\t\t}\n\t\tset disabled(bool) {\n\t\t\tthis._disabled = bool;\n\t\t\tthis.element.disabled = bool;\n\t\t\tthis.element.classList.toggle(\"disabled\", bool);\n\t\t}\n\t\tset active(bool) {\n\t\t\tthis._active = bool;\n\t\t\tthis.element.classList.toggle(\"main-genericButton-buttonActive\", bool);\n\t\t\tthis.element.classList.toggle(\"main-genericButton-buttonActiveDot\", bool);\n\t\t}\n\t\tget active() {\n\t\t\treturn this._active;\n\t\t}\n\t\tregister() {\n\t\t\tbuttonsStash.add(this.element);\n\t\t\trightContainer?.prepend(this.element);\n\t\t}\n\t\tderegister() {\n\t\t\tbuttonsStash.delete(this.element);\n\t\t\tthis.element.remove();\n\t\t}\n\t}\n\n\t(function waitForPlaybarMounted() {\n\t\trightContainer = document.querySelector(\".main-nowPlayingBar-right > div\");\n\t\tif (!rightContainer) {\n\t\t\tsetTimeout(waitForPlaybarMounted, 300);\n\t\t\treturn;\n\t\t}\n\t\tfor (const button of buttonsStash) {\n\t\t\taddClassname(button);\n\t\t}\n\t\trightContainer.prepend(...buttonsStash);\n\t})();\n\n\tfunction addClassname(element) {\n\t\tsibling = document.querySelector(\".main-nowPlayingBar-right .main-genericButton-button\");\n\t\tif (!sibling) {\n\t\t\tsetTimeout(addClassname, 300, element);\n\t\t\treturn;\n\t\t}\n\t\tfor (const className of Array.from(sibling.classList)) {\n\t\t\tif (!className.startsWith(\"main-genericButton\")) element.classList.add(className);\n\t\t}\n\t}\n\n\tconst widgetStash = new Set();\n\tlet nowPlayingWidget;\n\n\tclass Widget {\n\t\tconstructor(label, icon, onClick = () => {}, disabled = false, active = false, registerOnCreate = true) {\n\t\t\tthis.element = document.createElement(\"button\");\n\t\t\tthis.element.className = \"main-addButton-button control-button control-button-heart\";\n\t\t\tthis.icon = icon;\n\t\t\tthis.onClick = onClick;\n\t\t\tthis.disabled = disabled;\n\t\t\tthis.active = active;\n\t\t\tthis.tippy = Spicetify.Tippy?.(this.element, {\n\t\t\t\tcontent: label,\n\t\t\t\t...Spicetify.TippyProps,\n\t\t\t});\n\t\t\tthis.label = label;\n\t\t\tregisterOnCreate && this.register();\n\t\t}\n\t\tget label() {\n\t\t\treturn this._label;\n\t\t}\n\t\tset label(text) {\n\t\t\tthis._label = text;\n\t\t\tif (!this.tippy) this.element.setAttribute(\"title\", text);\n\t\t\telse this.tippy.setContent(text);\n\t\t}\n\t\tget icon() {\n\t\t\treturn this._icon;\n\t\t}\n\t\tset icon(input) {\n\t\t\tlet newInput = input;\n\t\t\tif (newInput && Spicetify.SVGIcons[newInput]) {\n\t\t\t\tnewInput = `<svg height=\"16\" width=\"16\" viewBox=\"0 0 16 16\" fill=\"currentColor\">${Spicetify.SVGIcons[newInput]}</svg>`;\n\t\t\t}\n\t\t\tthis._icon = newInput;\n\t\t\tthis.element.innerHTML = newInput;\n\t\t}\n\t\tget onClick() {\n\t\t\treturn this._onClick;\n\t\t}\n\t\tset onClick(func) {\n\t\t\tthis._onClick = func;\n\t\t\tthis.element.onclick = () => this._onClick(this);\n\t\t}\n\t\tget disabled() {\n\t\t\treturn this._disabled;\n\t\t}\n\t\tset disabled(bool) {\n\t\t\tthis._disabled = bool;\n\t\t\tthis.element.disabled = bool;\n\t\t\tthis.element.classList.toggle(\"main-addButton-disabled\", bool);\n\t\t\tthis.element.ariaDisabled = bool;\n\t\t}\n\t\tset active(bool) {\n\t\t\tthis._active = bool;\n\t\t\tthis.element.classList.toggle(\"main-addButton-active\", bool);\n\t\t\tthis.element.ariaChecked = bool;\n\t\t}\n\t\tget active() {\n\t\t\treturn this._active;\n\t\t}\n\t\tregister() {\n\t\t\twidgetStash.add(this.element);\n\t\t\tnowPlayingWidget?.append(this.element);\n\t\t}\n\t\tderegister() {\n\t\t\twidgetStash.delete(this.element);\n\t\t\tthis.element.remove();\n\t\t}\n\t}\n\n\tfunction waitForWidgetMounted() {\n\t\tnowPlayingWidget = document.querySelector(\".main-nowPlayingWidget-nowPlaying\");\n\t\tif (!nowPlayingWidget) {\n\t\t\tsetTimeout(waitForWidgetMounted, 300);\n\t\t\treturn;\n\t\t}\n\t\tnowPlayingWidget.append(...widgetStash);\n\t}\n\n\t(function attachObserver() {\n\t\tconst leftPlayer = document.querySelector(\".main-nowPlayingBar-left\");\n\t\tif (!leftPlayer) {\n\t\t\tsetTimeout(attachObserver, 300);\n\t\t\treturn;\n\t\t}\n\t\twaitForWidgetMounted();\n\t\tconst observer = new MutationObserver((mutations) => {\n\t\t\tfor (const mutation of mutations) {\n\t\t\t\tif (mutation.removedNodes.length > 0) {\n\t\t\t\t\tnowPlayingWidget = null;\n\t\t\t\t\twaitForWidgetMounted();\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tobserver.observe(leftPlayer, { childList: true });\n\t})();\n\n\treturn { Button, Widget };\n})();\n\n(async function checkForUpdate() {\n\tif (!Spicetify.Config) {\n\t\tsetTimeout(checkForUpdate, 300);\n\t\treturn;\n\t}\n\tconst { check_spicetify_update, version } = Spicetify.Config;\n\t// Skip checking if upgrade check is disabled, or version is Dev/version is not set\n\tif (!check_spicetify_update || !version || version === \"Dev\") return;\n\t// Fetch latest version from GitHub\n\ttry {\n\t\tlet changelog;\n\t\tconst res = await fetch(\"https://api.github.com/repos/spicetify/cli/releases/latest\");\n\t\tconst { tag_name, html_url, body } = await res.json();\n\t\tconst semver = tag_name.slice(1);\n\t\tconst changelogRawDataOld = body.match(/## What's Changed([\\s\\S]*?)\\r\\n\\r/)?.[1];\n\t\tif (changelogRawDataOld) {\n\t\t\tchangelog = [...changelogRawDataOld.matchAll(/\\r\\n\\*\\s(.+?)\\sin\\shttps/g)]\n\t\t\t\t.map((match) => {\n\t\t\t\t\tconst featureData = match[1].split(\"@\");\n\t\t\t\t\tconst feature = featureData[0];\n\t\t\t\t\tconst committerID = featureData[1];\n\t\t\t\t\treturn `<li>${feature}<a href=\"https://github.com/${committerID}\">${committerID}</a></li>`;\n\t\t\t\t})\n\t\t\t\t.join(\"\\n\");\n\t\t} else {\n\t\t\tconst sections = body.split(\"\\n## \");\n\t\t\tconst filteredSections = sections.filter((section) => !section.startsWith(\"Compatibility\"));\n\t\t\tconst filteredText = filteredSections.join(\"\\n## \");\n\t\t\tchangelog = [...filteredText.matchAll(/- (?:\\*\\*(.+?)\\*\\*:? )?(.+?) \\(\\[(.+?)\\]\\((.+?)\\)\\)/g)]\n\t\t\t\t.map((match) => {\n\t\t\t\t\tconst feature = match[1];\n\t\t\t\t\tconst description = match[2];\n\t\t\t\t\tconst prNumber = match[3];\n\t\t\t\t\tconst prLink = match[4];\n\t\t\t\t\tlet text = \"<li>\";\n\t\t\t\t\tif (feature) text += `<strong>${feature}</strong>${!feature.endsWith(\":\") ? \": \" : \" \"}`;\n\t\t\t\t\ttext += `${description} (<a href=\"${prLink}\">${prNumber}</a>)</li>`;\n\t\t\t\t\treturn text;\n\t\t\t\t})\n\t\t\t\t.join(\"\\n\");\n\t\t}\n\n\t\tif (semver !== version) {\n\t\t\tconst content = document.createElement(\"div\");\n\t\t\tcontent.id = \"spicetify-update\";\n\t\t\tcontent.innerHTML = `\n\t\t\t\t<style>\n\t\t\t\t\t#spicetify-update a {\n\t\t\t\t\t\ttext-decoration: underline;\n\t\t\t\t\t}\n\t\t\t\t\t#spicetify-update pre {\n\t\t\t\t\t\tcursor: pointer;\n\t\t\t\t\t\tfont-size: 1rem;\n\t\t\t\t\t\tpadding: 0.5rem;\n\t\t\t\t\t\tbackground-color: var(--spice-highlight-elevated);\n\t\t\t\t\t\tborder-radius: 0.25rem;\n\t\t\t\t\t}\n\t\t\t\t\t#spicetify-update hr {\n\t\t\t\t\t\tborder-color: var(--spice-subtext);\n\t\t\t\t\t\tmargin-top: 1rem;\n\t\t\t\t\t\tmargin-bottom: 1rem;\n\t\t\t\t\t}\n\t\t\t\t\t#spicetify-update ul,\n\t\t\t\t\t#spicetify-update ol {\n\t\t\t\t\t\tpadding-left: 1.5rem;\n\t\t\t\t\t}\n\t\t\t\t\t#spicetify-update li {\n\t\t\t\t\t\tmargin-top: 0.5rem;\n\t\t\t\t\t\tmargin-bottom: 0.5rem;\n\t\t\t\t\t\tlist-style-type: disc;\n\t\t\t\t\t}\n\t\t\t\t\t#spicetify-update ol > li {\n\t\t\t\t\t\tlist-style-type: decimal;\n\t\t\t\t\t}\n\t\t\t\t\t.spicetify-update-space {\n\t\t\t\t\t\tmargin-bottom: 25px;\n\t\t\t\t\t}\n\t\t\t\t\t.spicetify-update-little-space {\n\t\t\t\t\t\tmargin-bottom: 8px;\n\t\t\t\t\t}\n\t\t\t\t</style>\n\t\t\t\t<p class=\"spicetify-update-space\">Update Spicetify to receive new features and bug fixes.</p>\n\t\t\t\t<p> Current version: ${version} </p>\n\t\t\t\t<p> Latest version:\n\t\t\t\t\t<a href=\"${html_url}\" target=\"_blank\" rel=\"noopener noreferrer\">\n\t\t\t\t\t\t${semver}\n\t\t\t\t\t</a>\n\t\t\t\t</p>\n\t\t\t\t<hr>\n\t\t\t\t<h3>What's Changed</h3>\n\t\t\t\t<details>\n\t\t\t\t\t<summary>\n\t\t\t\t\t\tSee changelog\n\t\t\t\t\t</summary>\n\t\t\t\t\t<ul>\n\t\t\t\t\t\t${changelog}\n\t\t\t\t\t</ul>\n\t\t\t\t</details>\n\t\t\t\t<hr>\n\t\t\t\t<h3>Guide</h3>\n\t\t\t\t<p>Run these commands in the terminal:</p>\n\t\t\t\t<ol>\n\t\t\t\t\t<li>Update Spicetify CLI</li>\n\t\t\t\t\t<pre class=\"spicetify-update-little-space\">spicetify update</pre>\n\t\t\t\t\t<p>Spicetify will automatically apply changes to Spotify after upgrading to the latest version.</p>\n\t\t\t\t\t<p>If you installed Spicetify via a package manager, update using said package manager.</p>\n\t\t\t\t</ol>\n\t\t\t`;\n\n\t\t\t(function waitForTippy() {\n\t\t\t\tif (!Spicetify.Tippy) {\n\t\t\t\t\tsetTimeout(waitForTippy, 300);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst tippy = Spicetify.Tippy(content.querySelectorAll(\"pre\"), {\n\t\t\t\t\tcontent: \"Click to copy\",\n\t\t\t\t\thideOnClick: false,\n\t\t\t\t\t...Spicetify.TippyProps,\n\t\t\t\t});\n\n\t\t\t\tfor (const instance of tippy) {\n\t\t\t\t\tinstance.reference.addEventListener(\"click\", () => {\n\t\t\t\t\t\tSpicetify.Platform.ClipboardAPI.copy(instance.reference.textContent);\n\t\t\t\t\t\tinstance.setContent(\"Copied!\");\n\t\t\t\t\t\tsetTimeout(() => instance.setContent(\"Click to copy\"), 1000);\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t})();\n\n\t\t\tconst updateModal = {\n\t\t\t\ttitle: \"Update Spicetify\",\n\t\t\t\tcontent,\n\t\t\t\tisLarge: true,\n\t\t\t};\n\n\t\t\tnew Spicetify.Topbar.Button(\n\t\t\t\t\"Update Spicetify\",\n\t\t\t\t`<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.0\" width=\"22px\" height=\"22px\" viewBox=\"0 0 320.000000 400.000000\"><g transform=\"translate(0.000000,400.000000) scale(0.100000,-0.100000)\" fill=\"currentColor\"><path d=\"M2213 3833 c3 -10 18 -52 34 -93 25 -67 28 -88 28 -200 0 -113 -3 -131 -27 -188 -87 -207 -222 -340 -613 -602 -206 -139 -308 -223 -442 -364 -117 -124 -133 -129 -146 -51 -28 173 -52 229 -130 307 -69 69 -133 101 -214 106 -80 5 -113 -3 -113 -28 0 -13 14 -25 43 -38 63 -28 113 -76 144 -140 25 -51 28 -68 28 -152 -1 -141 -27 -221 -193 -600 -133 -305 -164 -459 -138 -685 20 -168 46 -268 101 -382 127 -262 351 -451 642 -540 81 -24 102 -27 268 -27 159 -1 190 2 265 22 172 47 315 129 447 255 164 157 251 322 304 572 26 124 31 308 15 585 -7 130 -6 168 8 240 42 211 148 335 316 371 38 8 50 15 50 29 0 23 -27 30 -120 30 -101 0 -183 -22 -250 -68 -52 -36 -71 -58 -163 -203 -46 -73 -90 -96 -141 -75 -41 17 -51 43 -44 118 4 39 29 97 106 248 198 388 264 606 264 880 0 200 -37 347 -123 492 -53 91 -156 198 -188 198 -18 0 -22 -4 -18 -17z m-591 -2208 c277 -37 576 -148 608 -226 25 -59 -20 -129 -82 -129 -15 0 -61 16 -101 36 -133 67 -288 111 -480 135 -131 16 -447 7 -542 -16 -38 -10 -95 -19 -125 -22 -46 -4 -59 -1 -77 16 -41 38 -42 102 -4 140 33 33 270 78 441 84 109 4 249 -4 362 -18z m-40 -354 c142 -25 276 -68 397 -129 76 -38 97 -53 107 -79 23 -53 -8 -103 -63 -103 -19 0 -67 17 -111 39 -92 46 -203 84 -315 108 -128 28 -450 25 -573 -5 -68 -17 -97 -20 -117 -13 -47 18 -62 80 -29 120 55 69 457 104 704 62z m-48 -326 c183 -28 418 -126 432 -181 7 -29 -16 -69 -45 -77 -12 -3 -62 15 -123 43 -175 82 -240 95 -468 95 -149 0 -214 -4 -274 -18 -43 -9 -87 -17 -97 -17 -27 0 -59 35 -59 64 0 50 47 67 280 100 67 9 266 4 354 -9z\"/></g></svg>`,\n\t\t\t\t() => Spicetify.PopupModal.display(updateModal)\n\t\t\t);\n\t\t}\n\t} catch (err) {\n\t\tconsole.error(err);\n\t}\n})();\n"
  },
  {
    "path": "manifest.json",
    "content": "[\n\t{\n\t\t\"name\": \"Auto Skip Videos\",\n\t\t\"description\": \"Videos are unable to play in some regions because of Spotify's policy. Instead of jumping to next song in playlist, it just stops playing. And it's kinda annoying to open up the client to manually click next every times it happens. Use this extension to skip them automatically.\",\n\t\t\"preview\": null,\n\t\t\"main\": \"Extensions/autoSkipVideo.js\"\n\t},\n\t{\n\t\t\"name\": \"Bookmark\",\n\t\t\"description\": \"Easily store and browse pages, play tracks or tracks in specific time. Useful for who wants to check out an artist, album later without following them or writing their name down.\",\n\t\t\"preview\": \"https://i.imgur.com/isgU4TS.png\",\n\t\t\"main\": \"Extensions/bookmark.js\"\n\t},\n\t{\n\t\t\"name\": \"Christian Spotify\",\n\t\t\"description\": \"Auto skip explicit tracks. Toggle option is in Profile menu (top right button).\",\n\t\t\"preview\": \"https://i.imgur.com/5reGrBb.png\",\n\t\t\"main\": \"Extensions/autoSkipExplicit.js\"\n\t},\n\t{\n\t\t\"name\": \"Keyboard Shortcut\",\n\t\t\"description\": \"Register some useful keybinds to support keyboard-driven navigation in Spotify client. Less time touching the mouse.\",\n\t\t\"preview\": \"https://i.imgur.com/evkGv9q.png\",\n\t\t\"main\": \"Extensions/keyboardShortcut.js\"\n\t},\n\t{\n\t\t\"name\": \"Loopy Loop\",\n\t\t\"description\": \"Provide ability to mark start and end points on progress bar and automatically loop over that track portion.\",\n\t\t\"preview\": \"https://i.imgur.com/YEkbjLC.png\",\n\t\t\"main\": \"Extensions/loopyLoop.js\"\n\t},\n\t{\n\t\t\"name\": \"Shuffle+\",\n\t\t\"description\": \"Shuffles using Fisher–Yates algorithm with zero bias. After installing extensions, right click album/playlist/artist item, there will be an option \\\"Play with Shuffle+\\\". You can also multiple select tracks and choose to \\\"Play with Shuffle+\\\". Moreover, enable option \\\"Auto Shuffle+\\\" in Profile menu to inject Shuffle+ into every play buttons, no need to right click anymore.\",\n\t\t\"preview\": \"https://i.imgur.com/gxbnqSN.png\",\n\t\t\"main\": \"Extensions/shuffle+.js\"\n\t},\n\t{\n\t\t\"name\": \"Trash Bin\",\n\t\t\"description\": \"Throw songs/artists to trash bin and never hear them again (automatically skip). This extension will append a Throw to Trashbin option in tracks and artists link right click menu.\",\n\t\t\"preview\": \"https://i.imgur.com/ZFTy5Rm.png\",\n\t\t\"main\": \"Extensions/trashbin.js\"\n\t}\n]\n"
  },
  {
    "path": "spicetify.go",
    "content": "package main\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\n\tcolorable \"github.com/mattn/go-colorable\"\n\t\"github.com/pterm/pterm\"\n\t\"github.com/spicetify/cli/src/cmd\"\n\tspotifystatus \"github.com/spicetify/cli/src/status/spotify\"\n\t\"github.com/spicetify/cli/src/utils\"\n\t\"github.com/spicetify/cli/src/utils/isAdmin\"\n)\n\nvar (\n\tversion string\n)\n\nvar (\n\tflags            = []string{}\n\tcommands         = []string{}\n\tquiet            = false\n\textensionFocus   = false\n\tappFocus         = false\n\tstyleFocus       = false\n\tnoRestart        = false\n\tliveRefresh      = false\n\tbypassAdminCheck = false\n)\n\nfunc init() {\n\tif runtime.GOOS != \"windows\" &&\n\t\truntime.GOOS != \"darwin\" &&\n\t\truntime.GOOS != \"linux\" {\n\t\tutils.PrintError(\"Unsupported OS.\")\n\t\tos.Exit(1)\n\t}\n\tif version == \"\" {\n\t\tversion = \"Dev\"\n\t}\n\n\tlog.SetFlags(0)\n\t// Supports print color output for Windows\n\tlog.SetOutput(colorable.NewColorableStdout())\n\n\t// Separates flags and commands\n\tfor _, v := range os.Args[1:] {\n\t\tif len(v) > 0 && v[0] == '-' {\n\t\t\tif len(v) > 2 && v[1] != '-' {\n\t\t\t\tfor _, char := range v[1:] {\n\t\t\t\t\tflags = append(flags, \"-\"+string(char))\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tflags = append(flags, v)\n\t\t\t}\n\t\t} else {\n\t\t\tcommands = append(commands, v)\n\t\t}\n\t}\n\n\tfor _, v := range flags {\n\t\tswitch v {\n\t\tcase \"--bypass-admin\":\n\t\t\tbypassAdminCheck = true\n\t\tcase \"-c\", \"--config\":\n\t\t\tlog.Println(cmd.GetConfigPath())\n\t\t\tos.Exit(0)\n\t\tcase \"-h\", \"--help\":\n\t\t\tkind := \"\"\n\t\t\tif len(commands) > 0 {\n\t\t\t\tkind = commands[0]\n\t\t\t}\n\t\t\tif kind == \"config\" {\n\t\t\t\thelpConfig()\n\t\t\t} else {\n\t\t\t\thelp()\n\t\t\t}\n\n\t\t\tos.Exit(0)\n\t\tcase \"-v\", \"--version\":\n\t\t\tlog.Println(version)\n\t\t\tos.Exit(0)\n\t\tcase \"-e\", \"--extension\":\n\t\t\textensionFocus = true\n\t\t\tliveRefresh = true\n\t\tcase \"-a\", \"--app\":\n\t\t\tappFocus = true\n\t\t\tliveRefresh = true\n\t\tcase \"-q\", \"--quiet\":\n\t\t\tquiet = true\n\t\tcase \"-n\", \"--no-restart\":\n\t\t\tnoRestart = true\n\t\tcase \"-s\", \"--style\":\n\t\t\tstyleFocus = true\n\t\t\tliveRefresh = true\n\t\tcase \"-l\", \"--live-refresh\":\n\t\t\textensionFocus = true\n\t\t\tappFocus = true\n\t\t\tstyleFocus = true\n\t\t\tliveRefresh = true\n\t\t}\n\t}\n\n\tif quiet {\n\t\tlog.SetOutput(io.Discard)\n\t\tos.Stdout = nil\n\t\tpterm.DisableOutput()\n\t}\n\n\tif isAdmin.Check(bypassAdminCheck) {\n\t\tutils.PrintError(\"Spicetify should NOT be run with administrator or root privileges\")\n\t\tutils.PrintError(\"Doing so can cause Spotify to show a black/blank window after applying!\")\n\t\tutils.PrintError(\"This happens because Spotify (running as a normal user) can't access files modified with admin privileges\")\n\t\tutils.PrintInfo(\"If you understand the risks and need to continue, you can use the '--bypass-admin' flag.\")\n\t\tos.Exit(1)\n\t}\n\n\tfor i, flag := range flags {\n\t\tif flag == \"--bypass-admin\" {\n\t\t\tflags = append(flags[:i], flags[i+1:]...)\n\t\t\tbreak\n\t\t}\n\t}\n\n\tutils.MigrateConfigFolder()\n\tutils.MigrateFolders()\n\tcmd.InitConfig(quiet)\n\n\tif len(commands) < 1 {\n\t\thelp()\n\t\tcmd.CheckUpdate(version)\n\t\tos.Exit(0)\n\t}\n}\n\nfunc main() {\n\tif slices.Contains(commands, \"config-dir\") {\n\t\tcmd.ShowConfigDirectory()\n\t\treturn\n\t}\n\n\t// Unchainable commands\n\tswitch commands[0] {\n\tcase \"config\":\n\t\tcommands = commands[1:]\n\t\tif len(commands) == 0 {\n\t\t\tcmd.DisplayAllConfig()\n\t\t} else if len(commands) == 1 {\n\t\t\tcmd.DisplayConfig(commands[0])\n\t\t} else {\n\t\t\tcmd.EditConfig(commands)\n\t\t}\n\t\treturn\n\n\tcase \"color\":\n\t\tcommands = commands[1:]\n\t\tif len(commands) == 0 {\n\t\t\tcmd.DisplayColors()\n\t\t} else {\n\t\t\tcmd.EditColor(commands)\n\t\t}\n\t\treturn\n\n\tcase \"spotify-updates\":\n\t\tcmd.InitPaths()\n\t\tcommands = commands[1:]\n\t\tif len(commands) == 0 {\n\t\t\tutils.PrintError(\"No parameter given. It has to be \\\"block\\\" or \\\"unblock\\\".\")\n\t\t\treturn\n\t\t}\n\t\tparam := commands[0]\n\t\tswitch param {\n\t\tcase \"block\":\n\t\t\tcmd.BlockSpotifyUpdates(true)\n\t\tcase \"unblock\":\n\t\t\tcmd.BlockSpotifyUpdates(false)\n\t\tdefault:\n\t\t\tutils.PrintError(\"Invalid parameter. It has to be \\\"block\\\" or \\\"unblock\\\".\")\n\t\t}\n\t\treturn\n\n\tcase \"path\":\n\t\tcmd.InitPaths()\n\t\tcommands = commands[1:]\n\t\tpath, err := (func() (string, error) {\n\t\t\tif styleFocus {\n\t\t\t\tif len(commands) == 0 {\n\t\t\t\t\treturn cmd.ThemeAllAssetsPath()\n\t\t\t\t}\n\t\t\t\treturn cmd.ThemeAssetPath(commands[0])\n\t\t\t} else if extensionFocus {\n\t\t\t\tif len(commands) == 0 {\n\t\t\t\t\treturn cmd.ExtensionAllPath()\n\t\t\t\t}\n\t\t\t\treturn cmd.ExtensionPath(commands[0])\n\t\t\t} else if appFocus {\n\t\t\t\tif len(commands) == 0 {\n\t\t\t\t\treturn cmd.AppAllPath()\n\t\t\t\t}\n\t\t\t\treturn cmd.AppPath(commands[0])\n\t\t\t} else {\n\t\t\t\tfor _, v := range flags {\n\t\t\t\t\tif v != \"-e\" && v != \"-c\" && v != \"-a\" && v != \"-s\" {\n\t\t\t\t\t\treturn \"\", errors.New(\"invalid option\\navailable options: -e, -c, -a, -s\")\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif len(commands) == 0 && len(flags) == 0 {\n\t\t\t\t\treturn utils.GetExecutableDir(), nil\n\t\t\t\t} else if commands[0] == \"all\" {\n\t\t\t\t\treturn cmd.AllPaths()\n\t\t\t\t} else if commands[0] == \"userdata\" {\n\t\t\t\t\treturn utils.GetSpicetifyFolder(), nil\n\t\t\t\t}\n\t\t\t\treturn \"\", errors.New(\"invalid option\\navailable options: all, userdata\")\n\t\t\t}\n\t\t})()\n\n\t\tif err != nil {\n\t\t\tutils.Fatal(err)\n\t\t}\n\n\t\tlog.Println(path)\n\t\treturn\n\n\tcase \"watch\":\n\t\tcmd.InitPaths()\n\n\t\tvar name []string\n\t\tif len(commands) > 1 {\n\t\t\tname = commands[1:]\n\t\t}\n\n\t\tvar watchGroup sync.WaitGroup\n\n\t\tif extensionFocus {\n\t\t\twatchGroup.Add(1)\n\t\t\tgo func(name []string, liveUpdate bool) {\n\t\t\t\tdefer watchGroup.Done()\n\t\t\t\tcmd.WatchExtensions(name, liveUpdate)\n\t\t\t}(name, liveRefresh)\n\t\t}\n\n\t\tif appFocus {\n\t\t\twatchGroup.Add(1)\n\t\t\tgo func(name []string, liveUpdate bool) {\n\t\t\t\tdefer watchGroup.Done()\n\t\t\t\tcmd.WatchCustomApp(name, liveUpdate)\n\t\t\t}(name, liveRefresh)\n\t\t}\n\n\t\tif styleFocus {\n\t\t\twatchGroup.Add(1)\n\t\t\tgo func(liveUpdate bool) {\n\t\t\t\tdefer watchGroup.Done()\n\t\t\t\tcmd.Watch(liveUpdate)\n\t\t\t}(liveRefresh)\n\t\t}\n\n\t\twatchGroup.Wait()\n\t\treturn\n\t}\n\n\tcmd.InitPaths()\n\n\tutils.PrintBold(\"spicetify v\" + version)\n\tif slices.Contains(commands, \"upgrade\") || slices.Contains(commands, \"update\") {\n\t\tupdateStatus := cmd.Update(version)\n\t\tspotifyPath := filepath.Join(cmd.GetSpotifyPath(), \"Apps\")\n\t\tex, err := os.Executable()\n\t\tif err != nil {\n\t\t\tex = \"spicetify\"\n\t\t}\n\n\t\tif updateStatus {\n\t\t\tspotStat := spotifystatus.Get(spotifyPath)\n\t\t\tcmds := []string{\"backup\", \"apply\"}\n\t\t\tif !spotStat.IsBackupable() {\n\t\t\t\tcmds = append([]string{\"restore\"}, cmds...)\n\t\t\t}\n\n\t\t\tcmd := exec.Command(ex, cmds...)\n\t\t\tutils.CmdScanner(cmd)\n\n\t\t\tcmd = exec.Command(ex, strings.Join(commands[:], \" \"))\n\t\t\tutils.CmdScanner(cmd)\n\t\t}\n\n\t\tspotStat := spotifystatus.Get(spotifyPath)\n\t\tif spotStat.IsBackupable() {\n\t\t\tutils.PrintNote(\"spicetify is up-to-date! If you ran this because spicetify disappeared after Spotify updated, we'll attempt to fix it for you right now.\")\n\t\t\tcmd.Backup(version, true)\n\t\t\tcmd.CheckStates()\n\t\t\tcmd.InitSetting()\n\t\t\tcmd.Apply(version)\n\t\t\tif !noRestart {\n\t\t\t\tcmd.SpotifyRestart()\n\t\t\t}\n\t\t}\n\n\t\treturn\n\t} else {\n\t\tcmd.CheckUpdate(version)\n\t}\n\n\tvar shouldRestart bool = false\n\t// Chainable commands\n\tfor _, v := range commands {\n\t\tswitch v {\n\t\tcase \"backup\":\n\t\t\tcmd.Backup(version, slices.Contains(commands, \"apply\"))\n\n\t\tcase \"clear\":\n\t\t\tcmd.Clear()\n\n\t\tcase \"apply\":\n\t\t\tcmd.CheckStates()\n\t\t\tcmd.InitSetting()\n\t\t\tcmd.Apply(version)\n\t\t\tshouldRestart = true\n\n\t\tcase \"refresh\":\n\t\t\tcmd.CheckStates()\n\t\t\tcmd.InitSetting()\n\t\t\tif extensionFocus {\n\t\t\t\tcmd.RefreshExtensions()\n\t\t\t} else if appFocus {\n\t\t\t\tcmd.RefreshApps()\n\t\t\t} else {\n\t\t\t\tcmd.RefreshTheme()\n\t\t\t}\n\n\t\tcase \"restore\":\n\t\t\tcmd.Restore()\n\t\t\tshouldRestart = true\n\n\t\tcase \"enable-devtools\":\n\t\t\tcmd.EnableDevTools()\n\t\t\tshouldRestart = true\n\n\t\tcase \"restart\":\n\t\t\tcmd.SpotifyRestart()\n\n\t\tcase \"auto\":\n\t\t\tcmd.Auto(version)\n\t\t\tshouldRestart = true\n\n\t\tdefault:\n\t\t\tutils.Fatal(errors.New(`Command \"` + v + `\" not found.\nRun \"spicetify -h\" for a list of valid commands.`))\n\t\t}\n\t}\n\n\tif !noRestart && !slices.Contains(commands, \"restart\") && shouldRestart {\n\t\tcmd.SpotifyRestart()\n\t}\n}\n\nfunc help() {\n\tutils.PrintBold(\"spicetify v\" + version)\n\tlog.Println(utils.Bold(\"USAGE\") + \"\\n\" +\n\t\t\"spicetify [-q] [-e] [-a] \\x1B[4mcommand\\033[0m...\\n\" +\n\t\t\"spicetify {-c | --config} | {-v | --version} | {-h | --help}\\n\\n\" +\n\t\tutils.Bold(\"DESCRIPTION\") + \"\\n\" +\n\t\t\"Customize Spotify client UI and functionality\\n\\n\" +\n\t\tutils.Bold(\"CHAINABLE COMMANDS\") + `\nbackup              Start backup and preprocessing of app files.\n\napply               Apply customization.\n\nrefresh             Refresh the theme's CSS, JS, colors, and assets.\n                    Use with flag \"-e\" to update extensions or with flag \"-a\" to update custom apps.\n\nrestore             Restore Spotify to original state.\n\nclear               Clear current backup files.\n\nenable-devtools     Enable Spotify's developer tools.\n                    Press Ctrl + Shift + I (Windows/Linux) or Cmd + Option + I (macOS) in the Spotify client to open.\n\nwatch               Enter watch mode.\n                    To update on change, use with any combination of the following flags:\n                        \"-e\" (for extensions),\n                        \"-a\" (for custom apps),\n                        \"-s\" (for the active theme; color.ini, user.css, theme.js, and assets)\n                        \"-l\" (for all of the above)\n\n\nrestart             Restart Spotify client.\n\n` + utils.Bold(\"NON-CHAINABLE COMMANDS\") + `\nspotify-updates     Block Spotify updates by patching spotify executable.\n                    Accepts \"block\" or \"unblock\" as the parameter.\n\npath                Print path of Spotify's executable, userdata, and more.\n                    1. Print executable path:\n                    spicetify path\n\n                    2. Print userdata path:\n                    spicetify path userdata\n\n                    3. Print all paths:\n                    spicetify path all\n\n                    4. Toggle focus with flags:\n                    spicetify path <flag> <option>\n\n                    Available flags and options:\n                    \"-e\" (for extensions),\n                    options: root, extension name, blank for all.\n\n                    \"-a\" (for custom apps),\n                    options: root, <app-name>, blank for all.\n\n                    \"-s\" (for the active theme)\n                    options: root, folder, color, css, js, assets, blank for all.\n\n                    \"-c\" (for config.ini)\n                    options: N/A.\n\nconfig              1. Print all config fields and values:\n                    spicetify config\n\n                    2. Print one config field's value:\n                    spicetify config <field>\n\n                    Example usage:\n                    spicetify config color_scheme\n                    spicetify config custom_apps\n\n                    3. Change value of one or multiple config fields.\n                    spicetify config <field> <value> [<field> <value> ...]\n\n                    \"extensions\" and \"custom_apps\" fields are arrays of values,\n                    so <value> will be appended to those fields' current value.\n                    To remove one of array's values, postfix \"-\" to <value>.\n\n                    Example usage:\n                    - Enable \"disable_sentry\" preprocess:\n                    spicetify config disable_sentry 1\n                    - Add extension \"myFakeExt.js\" to current extensions list:\n                    spicetify config extensions myFakeExt.js\n                    - Remove extension \"wrongname.js\" from extensions list:\n                    spicetify config extensions wrongname.js-\n                    - Disable \"inject_css\" and enable \"song_page\"\n                    spicetify config inject_css 0 song_page 1\n\ncolor               1. Print all color fields and values.\n                    spicetify color\n\n                    Color boxes require 24-bit color (True color) supported\n                    terminal to show colors correctly.\n\n                    2. Change theme's one or multiple color values.\n                    spicetify color <field> <value> [<field> <value> ...]\n\n                    <value> can be in hex or decimal (rrr,ggg,bbb) format.\n\n                    Example usage:\n                    - Change main to ff0000\n                    spicetify color main ff0000\n                    - Change sidebar to 00ff00 and button to 0000ff\n                    spicetify color sidebar 00ff00 button 0000ff\n\nconfig-dir          Show config directory in file viewer\n\nupgrade|update      Update spicetify to the latest version if an update is available\n\n` + utils.Bold(\"FLAGS\") + `\n-q, --quiet         Quiet mode (no output).\n\n-s, --style         Use with \"watch\" or \"path\" to focus on the active theme.\n                    Use with \"watch\" to auto-reload Spotify when changes are made to the active theme.\n\n-e, --extension     Use with \"refresh\", \"watch\" or \"path\" to focus on extensions.\n                    Use with \"watch\" to auto-reload Spotify when changes are made to extensions.\n\n-a, --app           Use with \"refresh\", \"watch\" or \"path\" to focus on custom apps.\n                    Use with \"watch\" to auto-reload Spotify when changes are made to apps.\n\n-l, --live-refresh  Use with \"watch\" command to auto-reload Spotify when changes\n                    are made to any custom component.\n\n-n, --no-restart    Do not restart Spotify after running command(s),\n                    except for the \"restart\" command.\n\n--bypass-admin      Bypass admin or root (sudo) check. NOT RECOMMENDED\n\n-c, --config        Print config file path and quit\n\n-h, --help          Print this help text and quit\n\n-v, --version       Print version number and quit\n\nFor config information, run \"spicetify -h config\".\nFor more information and reporting bugs: https://github.com/spicetify/cli/`)\n}\n\nfunc helpConfig() {\n\tutils.PrintBold(\"CONFIG MEANING\")\n\tlog.Println(utils.Bold(\"[Setting]\") + `\nspotify_path\n    Path to Spotify directory\n\nprefs_path\n    Path to Spotify's \"prefs\" file\n\ncurrent_theme\n    Name of folder of your theme\n\ncolor_scheme\n    Color config section name in color.ini file.\n    If color_scheme is blank, first section in color.ini file would be used.\n\ninject_css <0 | 1>\n    Whether custom css from user.css in theme folder is applied\n\ninject_theme_js <0 | 1>\n    Whether custom js from theme.js in theme folder is applied\n\nreplace_colors <0 | 1>\n    Whether custom colors is applied\n\nspotify_launch_flags <string>\n    Command-line flags used when launching/restarting Spotify.\n    Separate each flag with \"|\".\n    List of valid flags: https://spicetify.app/docs/development/spotify-cli-flags\n\nalways_enable_devtools <0 | 1>\n    Whether Chrome DevTools is enabled when launching/restarting Spotify.\n\ncheck_spicetify_update <0 | 1>\n    Whether to always check for updates when running Spicetify.\n\n` + utils.Bold(\"[Preprocesses]\") + `\ndisable_sentry <0 | 1>\n    Prevents Sentry and Amazon Qualaroo to send console log/error/warning to Spotify developers.\n    Enable if you don't want to catch their attention when developing extension or app.\n\ndisable_ui_logging <0 | 1>\n    Various elements logs every user clicks, scrolls.\n    Enable to stop logging and improve user experience.\n\nremove_rtl_rule <0 | 1>\n    To support Arabic and other Right-To-Left language, Spotify added a lot of\n    CSS rules that are obsoleted to Left-To-Right users.\n    Enable to remove all of them and improve render speed.\n\nexpose_apis <0 | 1>\n    Leaks some Spotify's API, functions, objects to Spicetify global object that\n    are useful for making extensions to extend Spotify functionality.\n\n` + utils.Bold(\"[AdditionalOptions]\") + `\ncustom_apps <string>\n    List of custom apps. Separate each app with \"|\".\n\nextensions <string>\n    List of Javascript files to be executed along with Spotify main script.\n    Separate each extension with \"|\".\n\nexperimental_features <0 | 1>\n    Enable ability to activate unfinished or work-in-progress features that would eventually be released in future Spotify updates.\n    Open \"Experimental features\" popup in Profile menu.\n\nhome_config <0 | 1>\n    Enable ability to re-arrange sections in Home page.\n    Navigate to Home page, turn \"Home config\" mode on in Profile menu and hover on sections to show customization buttons.\n\nsidebar_config <0 | 1>\n    Enable ability to stick, hide, re-arrange sidebar items.\n    Turn \"Sidebar config\" mode on in Profile menu and hover on sidebar items to show customization buttons.\n\n` + utils.Bold(\"[Patch]\") + `\nAllows you to apply custom patches to Spotify.`)\n}\n"
  },
  {
    "path": "src/apply/apply.go",
    "content": "package apply\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/spicetify/cli/src/utils\"\n)\n\n// Flag enables/disables additional feature\ntype Flag struct {\n\tCurrentTheme         string\n\tColorScheme          string\n\tInjectThemeJS        bool\n\tCheckSpicetifyUpdate bool\n\tExtension            []string\n\tCustomApp            []string\n\tSidebarConfig        bool\n\tHomeConfig           bool\n\tExpFeatures          bool\n\tSpicetifyVer         string\n\tSpotifyVer           string\n}\n\n// AdditionalOptions .\nfunc AdditionalOptions(appsFolderPath string, flags Flag) {\n\tjsModifiers := []func(path string, flags Flag){\n\t\tinsertExpFeatures,\n\t\tinsertSidebarConfig,\n\t\tinsertHomeConfig,\n\t}\n\tfilesToModified := map[string][]func(path string, flags Flag){\n\t\tfilepath.Join(appsFolderPath, \"xpui\", \"index.html\"): {\n\t\t\thtmlMod,\n\t\t},\n\t\tfilepath.Join(appsFolderPath, \"xpui\", \"xpui.js\"):         jsModifiers,\n\t\tfilepath.Join(appsFolderPath, \"xpui\", \"xpui-modules.js\"): jsModifiers,\n\t\tfilepath.Join(appsFolderPath, \"xpui\", \"xpui-snapshot.js\"): {\n\t\t\tinsertCustomApp,\n\t\t},\n\t\tfilepath.Join(appsFolderPath, \"xpui\", \"home-v2.js\"): {\n\t\t\tinsertHomeConfig,\n\t\t},\n\t\tfilepath.Join(appsFolderPath, \"xpui\", \"xpui-desktop-modals.js\"): {\n\t\t\tinsertVersionInfo,\n\t\t},\n\t}\n\n\tverParts := strings.Split(flags.SpotifyVer, \".\")\n\tspotifyMajor, spotifyMinor, spotifyPatch := 0, 0, 0\n\tif len(verParts) > 0 {\n\t\tspotifyMajor, _ = strconv.Atoi(verParts[0])\n\t}\n\tif len(verParts) > 1 {\n\t\tspotifyMinor, _ = strconv.Atoi(verParts[1])\n\t}\n\tif len(verParts) > 2 {\n\t\tspotifyPatch, _ = strconv.Atoi(verParts[2])\n\t}\n\n\tfilesToModified[filepath.Join(appsFolderPath, \"xpui\", \"xpui.js\")] = append(filesToModified[filepath.Join(appsFolderPath, \"xpui\", \"xpui.js\")], insertCustomApp)\n\tif spotifyMajor >= 1 && spotifyMinor >= 2 && spotifyPatch >= 57 {\n\t\tfilesToModified[filepath.Join(appsFolderPath, \"xpui\", \"xpui.js\")] = append(filesToModified[filepath.Join(appsFolderPath, \"xpui\", \"xpui.js\")], insertExpFeatures)\n\t} else {\n\t\tfilesToModified[filepath.Join(appsFolderPath, \"xpui\", \"vendor~xpui.js\")] = []func(string, Flag){insertExpFeatures}\n\t}\n\n\tfor file, calls := range filesToModified {\n\t\tif _, err := os.Stat(file); os.IsNotExist(err) {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, call := range calls {\n\t\t\tcall(file, flags)\n\t\t}\n\t}\n\n\tif flags.SidebarConfig {\n\t\tutils.CopyFile(\n\t\t\tfilepath.Join(utils.GetJsHelperDir(), \"sidebarConfig.js\"),\n\t\t\tfilepath.Join(appsFolderPath, \"xpui\", \"helper\"))\n\t}\n\n\tif flags.HomeConfig {\n\t\tutils.CopyFile(\n\t\t\tfilepath.Join(utils.GetJsHelperDir(), \"homeConfig.js\"),\n\t\t\tfilepath.Join(appsFolderPath, \"xpui\", \"helper\"))\n\t}\n\n\tif flags.ExpFeatures {\n\t\tutils.CopyFile(\n\t\t\tfilepath.Join(utils.GetJsHelperDir(), \"expFeatures.js\"),\n\t\t\tfilepath.Join(appsFolderPath, \"xpui\", \"helper\"))\n\t}\n}\n\n// UserCSS creates colors.css user.css files in \"xpui\".\n// To not use custom css, set `themeFolder` to blank string\n// To use default color scheme, set `scheme` to `nil`\nfunc UserCSS(appsFolderPath, themeFolder string, scheme map[string]string) {\n\tcolorsDest := filepath.Join(appsFolderPath, \"xpui\", \"colors.css\")\n\tif err := os.WriteFile(colorsDest, []byte(getColorCSS(scheme)), 0700); err != nil {\n\t\tutils.Fatal(err)\n\t}\n\tcssDest := filepath.Join(appsFolderPath, \"xpui\", \"user.css\")\n\tif err := os.WriteFile(cssDest, []byte(getUserCSS(themeFolder)), 0700); err != nil {\n\t\tutils.Fatal(err)\n\t}\n}\n\n// UserAsset .\nfunc UserAsset(appsFolderPath, themeFolder string) {\n\tvar assetsPath = getAssetsPath(themeFolder)\n\tvar xpuiPath = filepath.Join(appsFolderPath, \"xpui\")\n\tif err := utils.Copy(assetsPath, xpuiPath, true, nil); err != nil {\n\t\tutils.Fatal(err)\n\t}\n}\n\nfunc htmlMod(htmlPath string, flags Flag) {\n\tif len(flags.Extension) == 0 &&\n\t\t!flags.HomeConfig &&\n\t\t!flags.SidebarConfig &&\n\t\t!flags.ExpFeatures {\n\t\treturn\n\t}\n\n\textensionsHTML := \"\\n\"\n\thelperHTML := \"\\n\"\n\n\tif flags.InjectThemeJS {\n\t\textensionsHTML += \"<script defer src='extensions/theme.js'></script>\\n\"\n\t}\n\n\tif flags.SidebarConfig {\n\t\thelperHTML += \"<script defer src='helper/sidebarConfig.js'></script>\\n\"\n\t}\n\n\tif flags.HomeConfig {\n\t\thelperHTML += \"<script defer src='helper/homeConfig.js'></script>\\n\"\n\t}\n\n\tif flags.ExpFeatures {\n\t\thelperHTML += \"<script defer src='helper/expFeatures.js'></script>\\n\"\n\t}\n\n\tif flags.SpicetifyVer != \"\" {\n\t\tvar extList string\n\t\tfor _, ext := range flags.Extension {\n\t\t\textList += fmt.Sprintf(`\"%s\",`, ext)\n\t\t}\n\n\t\tvar customAppList string\n\t\tfor _, app := range flags.CustomApp {\n\t\t\tcustomAppList += fmt.Sprintf(`\"%s\",`, app)\n\t\t}\n\n\t\thelperHTML += fmt.Sprintf(`<script>\n\t\t\tSpicetify.Config={};\n\t\t\tSpicetify.Config[\"version\"]=\"%s\";\n\t\t\tSpicetify.Config[\"current_theme\"]=\"%s\";\n\t\t\tSpicetify.Config[\"color_scheme\"]=\"%s\";\n\t\t\tSpicetify.Config[\"extensions\"] = [%s];\n\t\t\tSpicetify.Config[\"custom_apps\"] = [%s];\n\t\t\tSpicetify.Config[\"check_spicetify_update\"]=%v;\n\t\t</script>\n\t\t`, flags.SpicetifyVer, flags.CurrentTheme, flags.ColorScheme, extList, customAppList, flags.CheckSpicetifyUpdate)\n\t}\n\n\tfor _, v := range flags.Extension {\n\t\tif strings.HasSuffix(v, \".mjs\") {\n\t\t\textensionsHTML += fmt.Sprintf(\"<script defer type='module' src='extensions/%s'></script>\\n\", v)\n\t\t} else {\n\t\t\textensionsHTML += fmt.Sprintf(\"<script defer src='extensions/%s'></script>\\n\", v)\n\t\t}\n\t}\n\n\tfor _, v := range flags.CustomApp {\n\t\tmanifest, _, err := utils.GetAppManifest(v)\n\t\tif err == nil {\n\t\t\tfor _, extensionFile := range manifest.ExtensionFiles {\n\t\t\t\tif strings.HasSuffix(extensionFile, \".mjs\") {\n\t\t\t\t\textensionsHTML += fmt.Sprintf(\"<script defer type='module' src='extensions/%s/%s'></script>\\n\", v, extensionFile)\n\t\t\t\t} else {\n\t\t\t\t\textensionsHTML += fmt.Sprintf(\"<script defer src='extensions/%s/%s'></script>\\n\", v, extensionFile)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tutils.ModifyFile(htmlPath, func(content string) string {\n\t\tutils.Replace(\n\t\t\t&content,\n\t\t\t`<script defer=\"defer\" src=\"/xpui-snapshot\\.js\"></script>`,\n\t\t\tfunc(submatches ...string) string {\n\t\t\t\treturn `<script defer=\"defer\" src=\"/xpui-modules.js\"></script><script defer=\"defer\" src=\"/xpui-snapshot.js\"></script>`\n\t\t\t})\n\t\tutils.Replace(\n\t\t\t&content,\n\t\t\t`<\\!-- spicetify helpers -->`,\n\t\t\tfunc(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%s%s\", submatches[0], helperHTML)\n\t\t\t})\n\t\tutils.Replace(\n\t\t\t&content,\n\t\t\t`</body>`,\n\t\t\tfunc(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%s%s\", extensionsHTML, submatches[0])\n\t\t\t})\n\t\treturn content\n\t})\n}\n\nfunc getUserCSS(themeFolder string) string {\n\tif len(themeFolder) == 0 {\n\t\treturn \"\"\n\t}\n\n\tcssFilePath := filepath.Join(themeFolder, \"user.css\")\n\t_, err := os.Stat(cssFilePath)\n\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tcontent, err := os.ReadFile(cssFilePath)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn string(content)\n}\n\nfunc getColorCSS(scheme map[string]string) string {\n\tvar variableList string\n\tvar variableRGBList string\n\tmergedScheme := make(map[string]string)\n\n\tfor k, v := range scheme {\n\t\tmergedScheme[k] = v\n\t}\n\n\tfor k, v := range utils.BaseColorList {\n\t\tif len(mergedScheme[k]) == 0 {\n\t\t\tmergedScheme[k] = v\n\t\t}\n\t}\n\n\tfor k, v := range mergedScheme {\n\t\tparsed := utils.ParseColor(v)\n\t\tvariableList += fmt.Sprintf(\"    --spice-%s: #%s;\\n\", k, parsed.Hex())\n\t\tvariableRGBList += fmt.Sprintf(\"    --spice-rgb-%s: %s;\\n\", k, parsed.RGB())\n\t}\n\n\treturn fmt.Sprintf(\":root {\\n%s\\n%s\\n}\\n\", variableList, variableRGBList)\n}\n\nfunc insertCustomApp(jsPath string, flags Flag) {\n\tutils.ModifyFile(jsPath, func(content string) string {\n\t\t// React lazy loading patterns for dynamic imports\n\t\treactPatterns := []string{\n\t\t\t// Sync pattern: X.lazy((() => Y.Z(123).then(W.bind(W, 456))))\n\t\t\t`([\\w_\\$][\\w_\\$\\d]*(?:\\(\\))?)\\.lazy\\(\\((?:\\(\\)=>|function\\(\\)\\{return )(\\w+)\\.(\\w+)\\(\\d+\\)\\.then\\(\\w+\\.bind\\(\\w+,\\d+\\)\\)\\}?\\)\\)`,\n\t\t\t// Async pattern (1.2.78+): m.lazy(async()=>{...await o.e(123).then(...)})\n\t\t\t`([\\w_\\$][\\w_\\$\\d]*)\\.lazy\\(async\\(\\)=>\\{(?:[^{}]|\\{[^{}]*\\})*await\\s+(\\w+)\\.(\\w+)\\(\\d+\\)\\.then\\(\\w+\\.bind\\(\\w+,\\d+\\)\\)`,\n\t\t\t// Async Promise.all pattern (1.2.78+): m.lazy(async()=>await Promise.all([...]).then(...))\n\t\t\t`([\\w_\\$][\\w_\\$\\d]*(?:\\(\\))?)\\.lazy\\(async\\(\\)=>await\\s+Promise\\.all\\(\\[[^\\]]+\\]\\)\\.then\\((\\w+)\\.bind\\((\\w+),\\d+\\)\\)`,\n\t\t}\n\n\t\t// React element/route patterns for path matching\n\t\telementPatterns := []string{\n\t\t\t// JSX pattern (1.2.78+): (0,S.jsx)(se.qh,{path:\"/collection/*\",element:...})\n\t\t\t// Settings page should be more consistent with having no conditional renders\n\t\t\t`(\\([\\w$\\.,]+\\))\\(([\\w\\.]+),\\{path:\"/settings(?:/[\\w\\*]+)?\",?(element|children)?`,\n\t\t\t// createElement pattern: X.createElement(Y,{path:\"/collection\"...})\n\t\t\t`([\\w_\\$][\\w_\\$\\d]*(?:\\(\\))?\\.createElement|\\([\\w$\\.,]+\\))\\(([\\w\\.]+),\\{path:\"\\/collection\"(?:,(element|children)?[:.\\w,{}()$/*\"]+)?\\}`,\n\t\t}\n\n\t\treactSymbs, matchedReactPattern := utils.FindSymbolWithPattern(\n\t\t\t\"Custom app React symbols\",\n\t\t\tcontent,\n\t\t\treactPatterns)\n\t\teleSymbs, matchedElementPattern := utils.FindSymbolWithPattern(\n\t\t\t\"Custom app React Element\",\n\t\t\tcontent,\n\t\t\telementPatterns)\n\n\t\tif (len(reactSymbs) < 2) || (len(eleSymbs) == 0) {\n\t\t\tutils.PrintError(\"Spotify version mismatch with Spicetify. Please report it on our github repository.\")\n\t\t\tutils.PrintInfo(\"Spicetify might have been updated for this version already. Please run `spicetify update` to check for a new version.\")\n\t\t\tutils.PrintInfo(\"If one isn't available yet, please wait for an update to be released or downgrade Spotify to a supported version.\")\n\t\t\treturn content\n\t\t}\n\n\t\tappMap := \"\"\n\t\tappReactMap := \"\"\n\t\tappEleMap := \"\"\n\t\tcssEnableMap := \"\"\n\t\tappNameArray := \"\"\n\n\t\t// Spotify's new route system\n\t\twildcard := \"\"\n\t\tif eleSymbs[2] == \"\" {\n\t\t\teleSymbs[2] = \"children\"\n\t\t} else if eleSymbs[2] == \"element\" {\n\t\t\twildcard = \"*\"\n\t\t}\n\n\t\tfor index, app := range flags.CustomApp {\n\t\t\tappName := `spicetify-routes-` + app\n\t\t\tappMap += fmt.Sprintf(`\"%s\":\"%s\",`, appName, appName)\n\t\t\tappNameArray += fmt.Sprintf(`\"%s\",`, app)\n\n\t\t\tappReactMap += fmt.Sprintf(\n\t\t\t\t`,spicetifyApp%d=%s.lazy((()=>%s.%s(\"%s\").then(%s.bind(%s,\"%s\"))))`,\n\t\t\t\tindex, reactSymbs[0], reactSymbs[1], reactSymbs[2],\n\t\t\t\tappName, reactSymbs[1], reactSymbs[1], appName)\n\n\t\t\tappEleMap += fmt.Sprintf(\n\t\t\t\t`%s(%s,{path:\"/%s/%s\",pathV6:\"/%s/*\",%s:%s(spicetifyApp%d,{})}),`,\n\t\t\t\teleSymbs[0], eleSymbs[1], app, wildcard, app, eleSymbs[2], eleSymbs[0], index)\n\n\t\t\tcssEnableMap += fmt.Sprintf(`,\"%s\":1`, appName)\n\t\t}\n\n\t\tutils.Replace(\n\t\t\t&content,\n\t\t\t`\\{(\\d+:\"xpui)`,\n\t\t\tfunc(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"{%s%s\", appMap, submatches[1])\n\t\t\t})\n\n\t\t// Seek to the full matched React.lazy pattern\n\t\tmatchedReactPattern = utils.SeekToCloseParen(\n\t\t\tcontent,\n\t\t\tmatchedReactPattern,\n\t\t\t'(',\n\t\t\t')',\n\t\t)\n\n\t\tcontent = strings.Replace(\n\t\t\tcontent,\n\t\t\tmatchedReactPattern,\n\t\t\tfmt.Sprintf(\"%s%s\", matchedReactPattern, appReactMap),\n\t\t\t1,\n\t\t)\n\n\t\tutils.ReplaceOnce(\n\t\t\t&content,\n\t\t\tmatchedElementPattern,\n\t\t\tfunc(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%s%s\", appEleMap, submatches[0])\n\t\t\t})\n\n\t\tcontent = insertNavLink(content, appNameArray)\n\n\t\tutils.ReplaceOnce(\n\t\t\t&content,\n\t\t\t`\\d+:1,\\d+:1,\\d+:1`,\n\t\t\tfunc(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%s%s\", submatches[0], cssEnableMap)\n\t\t\t})\n\n\t\treturn content\n\t})\n}\n\nfunc insertNavLink(str string, appNameArray string) string {\n\t// Library X\n\tlibraryXItemMatch := utils.SeekToCloseParen(\n\t\tstr,\n\t\t`\\(\"li\",\\{[^\\{]*\\{[^\\{]*\\{to:\"\\/search`,\n\t\t'(', ')')\n\n\tif libraryXItemMatch != \"\" {\n\t\tstr = strings.Replace(\n\t\t\tstr,\n\t\t\tlibraryXItemMatch,\n\t\t\tfmt.Sprintf(\"%s,Spicetify._renderNavLinks([%s], false)\", libraryXItemMatch, appNameArray),\n\t\t\t1)\n\t}\n\n\tutils.ReplaceOnceWithPriority(&str,\n\t\t[]string{\n\t\t\t// Global Navbar <= 1.2.45\n\t\t\t`(,[a-zA-Z_\\$][\\w\\$]*===(?:[a-zA-Z_\\$][\\w\\$]*\\.){2}HOME_NEXT_TO_NAVIGATION&&.+?)\\]`,\n\t\t\t// Global Navbar >= 1.2.60, greedy matching with enclosing brackets\n\t\t\t`(\"global-nav-bar\".*[[\\w\\$&|]*\\(0,[a-zA-Z_\\$][\\w\\$]*\\.jsx\\)\\(\\s*\\w+,\\s*\\{\\s*className:\\w*\\s*\\}\\s*\\))\\]`,\n\t\t\t// Global Navbar >= 1.2.46, lazy matching\n\t\t\t`(\"global-nav-bar\".*?)(\\(0,\\s*[a-zA-Z_\\$][\\w\\$]*\\.jsx\\))(\\(\\s*\\w+,\\s*\\{\\s*className:\\w*\\s*\\}\\s*\\))`,\n\t\t},\n\t\tfunc(index int, submatches ...string) string {\n\t\t\tswitch index {\n\t\t\tcase 0, 1:\n\t\t\t\treturn fmt.Sprintf(\"%s,Spicetify._renderNavLinks([%s], true)]\", submatches[1], appNameArray)\n\t\t\tcase 2:\n\t\t\t\treturn fmt.Sprintf(\"%s[%s%s,Spicetify._renderNavLinks([%s], true)].flat()\", submatches[1], submatches[2], submatches[3], appNameArray)\n\t\t\t}\n\t\t\treturn \"\"\n\t\t},\n\t)\n\n\treturn str\n}\n\nfunc insertHomeConfig(jsPath string, flags Flag) {\n\tif !flags.HomeConfig {\n\t\treturn\n\t}\n\n\tutils.ModifyFile(jsPath, func(content string) string {\n\t\tutils.ReplaceOnce(\n\t\t\t&content,\n\t\t\t`(createDesktopHomeFeatureActivationShelfEventFactory.*?)([\\w\\.]+)(\\.map)`,\n\t\t\tfunc(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sSpicetifyHomeConfig.arrange(%s)%s\", submatches[1], submatches[2], submatches[3])\n\t\t\t})\n\n\t\t// >= 1.2.40\n\t\tutils.ReplaceOnce(\n\t\t\t&content,\n\t\t\t`(&&\"HomeShortsSectionData\".*?[\\],}])([a-zA-Z])(\\}\\)?\\()`,\n\t\t\tfunc(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sSpicetifyHomeConfig.arrange(%s)%s\", submatches[1], submatches[2], submatches[3])\n\t\t\t})\n\n\t\treturn content\n\t})\n}\n\nfunc getAssetsPath(themeFolder string) string {\n\tdir := filepath.Join(themeFolder, \"assets\")\n\n\tif _, err := os.Stat(dir); err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn dir\n}\n\nfunc insertSidebarConfig(jsPath string, flags Flag) {\n\tif !flags.SidebarConfig {\n\t\treturn\n\t}\n\n\tutils.ModifyFile(jsPath, func(content string) string {\n\t\tutils.ReplaceOnce(\n\t\t\t&content,\n\t\t\t`return null!=\\w+&&\\w+\\.totalLength(\\?\\w+\\(\\)\\.createElement\\(\\w+,\\{contextUri:)(\\w+)\\.uri`,\n\t\t\tfunc(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(`return true%s%s?.uri||\"\"`, submatches[1], submatches[2])\n\t\t\t})\n\n\t\treturn content\n\t})\n}\n\nfunc insertExpFeatures(jsPath string, flags Flag) {\n\tif !flags.ExpFeatures {\n\t\treturn\n\t}\n\n\tutils.ModifyFile(jsPath, func(content string) string {\n\t\tutils.ReplaceOnce(\n\t\t\t&content,\n\t\t\t`(function \\w+\\((\\w+)\\)\\{)(\\w+ \\w+=\\w\\.name;if\\(\"internal\")`,\n\t\t\tfunc(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%s%s=Spicetify.expFeatureOverride(%s);%s\", submatches[1], submatches[2], submatches[2], submatches[3])\n\t\t\t})\n\n\t\t// utils.ReplaceOnce(\n\t\t// \t&content,\n\t\t// \t`(\\w+\\.fromJSON)(\\s*=\\s*function\\b[^{]*{[^}]*})`,\n\t\t// \tfunc(submatches ...string) string {\n\t\t// \t\treturn fmt.Sprintf(\"%s=Spicetify.createInternalMap%s\", submatches[1], submatches[2])\n\t\t// \t})\n\n\t\tutils.ReplaceOnce(\n\t\t\t&content,\n\t\t\t`(([\\w$.]+\\.fromJSON)\\(\\w+\\)+;)(return ?[\\w{}().,]+[\\w$]+\\.Provider,)(\\{value:\\{localConfiguration)`,\n\t\t\tfunc(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sSpicetify.createInternalMap=%s;%sSpicetify.RemoteConfigResolver=%s\", submatches[1], submatches[2], submatches[3], submatches[4])\n\t\t\t})\n\n\t\treturn content\n\t})\n}\n\nfunc insertVersionInfo(jsPath string, flags Flag) {\n\tutils.ModifyFile(jsPath, func(content string) string {\n\t\tutils.ReplaceOnce(\n\t\t\t&content,\n\t\t\t`(\\w+(?:\\(\\))?\\.createElement|\\([\\w$\\.,]+\\))\\([\\w\\.\"]+,[\\w{}():,]+\\.containerVersion\\}?\\),`,\n\t\t\tfunc(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(`%s%s(\"details\",{children: [\n\t\t\t\t\t%s(\"summary\",{children: \"Spicetify v\" + Spicetify.Config.version}),\n\t\t\t\t\t%s(\"li\",{children: \"Theme: \" + Spicetify.Config.current_theme + (Spicetify.Config.color_scheme && \" / \") + Spicetify.Config.color_scheme}),\n\t\t\t\t\t%s(\"li\",{children: \"Extensions: \" + Spicetify.Config.extensions.join(\", \")}),\n\t\t\t\t\t%s(\"li\",{children: \"Custom apps: \" + Spicetify.Config.custom_apps.join(\", \")}),\n\t\t\t\t\t]}),`, submatches[0], submatches[1], submatches[1], submatches[1], submatches[1], submatches[1])\n\t\t\t})\n\t\treturn content\n\t})\n}\n"
  },
  {
    "path": "src/backup/backup.go",
    "content": "package backup\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/spicetify/cli/src/utils\"\n)\n\n// Start backing up Spotify Apps folder to backupPath\nfunc Start(appPath, backupPath string) error {\n\treturn utils.Copy(appPath, backupPath, false, []string{\".spa\"})\n}\n\n// Extract all SPA files from backupPath to extractPath\nfunc Extract(backupPath, extractPath string) {\n\tspinner, _ := utils.Spinner.Start(\"Extracting backup\")\n\tfor _, app := range []string{\"xpui\", \"login\"} {\n\t\tappPath := filepath.Join(backupPath, app+\".spa\")\n\t\tappExtractToFolder := filepath.Join(extractPath, app)\n\n\t\t_, err := os.Stat(appPath)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\terr = utils.Unzip(appPath, appExtractToFolder)\n\t\tif err != nil {\n\t\t\tspinner.Fail(\"Failed to extract backup\")\n\t\t\tutils.Fatal(err)\n\t\t}\n\t}\n\tspinner.Success(\"Extracted backup\")\n}\n"
  },
  {
    "path": "src/cmd/apply.go",
    "content": "package cmd\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/spicetify/cli/src/apply\"\n\tbackupstatus \"github.com/spicetify/cli/src/status/backup\"\n\tspotifystatus \"github.com/spicetify/cli/src/status/spotify\"\n\t\"github.com/spicetify/cli/src/utils\"\n)\n\n// Apply .\nfunc Apply(spicetifyVersion string) {\n\tutils.MigrateConfigFolder()\n\n\tbackupSpicetifyVersion := backupSection.Key(\"with\").MustString(\"\")\n\tif spicetifyVersion != backupSpicetifyVersion {\n\t\tutils.PrintInfo(`Preprocessed Spotify data is outdated. Please run \"spicetify restore backup apply\" to receive new features and bug fixes`)\n\t\tos.Exit(1)\n\t}\n\n\t// Copy raw assets to Spotify Apps folder if Spotify is never applied\n\t// before.\n\t// extractedStock is for preventing copy raw assets 2 times when\n\t// replaceColors is false.\n\textractedStock := false\n\tif !spotifystatus.Get(appDestPath).IsApplied() {\n\t\tspinner, _ := utils.Spinner.Start(\"Copying raw assets\")\n\t\tif err := os.RemoveAll(appDestPath); err != nil {\n\t\t\tspinner.Fail(\"Failed to copy raw assets\")\n\t\t\tutils.Fatal(err)\n\t\t}\n\t\tif err := utils.Copy(rawFolder, appDestPath, true, nil); err != nil {\n\t\t\tspinner.Fail(\"Failed to copy raw assets\")\n\t\t\tutils.Fatal(err)\n\t\t}\n\t\tspinner.Success(\"Copied raw assets\")\n\t\textractedStock = true\n\t}\n\n\tif replaceColors {\n\t\tspinner, _ := utils.Spinner.Start(\"Overwriting themed assets\")\n\t\tif err := utils.Copy(themedFolder, appDestPath, true, nil); err != nil {\n\t\t\tspinner.Fail(\"Failed to overwrite themed assets\")\n\t\t\tutils.Fatal(err)\n\t\t}\n\t\tspinner.Success(\"Overwrote themed assets\")\n\t} else if !extractedStock {\n\t\tspinner, _ := utils.Spinner.Start(\"Overwriting raw assets\")\n\t\tif err := utils.Copy(rawFolder, appDestPath, true, nil); err != nil {\n\t\t\tspinner.Fail(\"Failed to overwrite raw assets\")\n\t\t\tutils.Fatal(err)\n\t\t}\n\t\tspinner.Success(\"Overwrote raw assets\")\n\t}\n\n\tRefreshTheme()\n\n\tif preprocSection.Key(\"expose_apis\").MustBool(false) {\n\t\tutils.CopyFile(\n\t\t\tfilepath.Join(utils.GetJsHelperDir(), \"spicetifyWrapper.js\"),\n\t\t\tfilepath.Join(appDestPath, \"xpui\", \"helper\"))\n\t}\n\n\textensionList := featureSection.Key(\"extensions\").Strings(\"|\")\n\tcustomAppsList := featureSection.Key(\"custom_apps\").Strings(\"|\")\n\n\tspinner, _ := utils.Spinner.Start(\"Applying additional modifications\")\n\tapply.AdditionalOptions(appDestPath, apply.Flag{\n\t\tCurrentTheme:         settingSection.Key(\"current_theme\").MustString(\"\"),\n\t\tColorScheme:          settingSection.Key(\"color_scheme\").MustString(\"\"),\n\t\tInjectThemeJS:        injectJS,\n\t\tCheckSpicetifyUpdate: settingSection.Key(\"check_spicetify_update\").MustBool(false),\n\t\tExtension:            extensionList,\n\t\tCustomApp:            customAppsList,\n\t\tSidebarConfig:        featureSection.Key(\"sidebar_config\").MustBool(false),\n\t\tHomeConfig:           featureSection.Key(\"home_config\").MustBool(false),\n\t\tExpFeatures:          featureSection.Key(\"experimental_features\").MustBool(false),\n\t\tSpicetifyVer:         backupSection.Key(\"with\").MustString(\"\"),\n\t})\n\tspinner.Success(\"Applied additional modifications\")\n\n\tif len(extensionList) > 0 {\n\t\tRefreshExtensions(extensionList...)\n\t\tnodeModuleSymlink()\n\t}\n\n\tif len(customAppsList) > 0 {\n\t\tRefreshApps(customAppsList...)\n\t}\n\n\tif len(patchSection.Keys()) > 0 {\n\t\tPatch()\n\t}\n}\n\n// RefreshTheme updates user.css + theme.js and overwrites custom assets\nfunc RefreshTheme() {\n\trefreshThemeCSS()\n\n\tif injectJS {\n\t\trefreshThemeJS()\n\t} else {\n\t\tutils.CheckExistAndDelete(filepath.Join(appDestPath, \"xpui\", \"extensions/theme.js\"))\n\t}\n\n\tif overwriteAssets {\n\t\trefreshThemeAssets()\n\t}\n}\n\ntype spicetifyConfigJson struct {\n\tThemeName  string                       `json:\"theme_name\"`\n\tSchemeName string                       `json:\"scheme_name\"`\n\tSchemes    map[string]map[string]string `json:\"schemes\"`\n}\n\nfunc refreshThemeCSS() {\n\tspinner, _ := utils.Spinner.Start(\"Updating theme's styles\")\n\tvar scheme map[string]string = nil\n\tif colorSection != nil {\n\t\tscheme = colorSection.KeysHash()\n\t}\n\ttheme := themeFolder\n\tif !injectCSS {\n\t\ttheme = \"\"\n\t}\n\tapply.UserCSS(appDestPath, theme, scheme)\n\n\tvar configJson spicetifyConfigJson\n\tconfigJson.ThemeName = settingSection.Key(\"current_theme\").MustString(\"\")\n\tconfigJson.SchemeName = settingSection.Key(\"color_scheme\").MustString(\"\")\n\n\tif colorCfg != nil {\n\t\tcolorsJson := make(map[string]map[string]string)\n\t\tfor _, section := range colorCfg.Sections() {\n\t\t\tname := section.Name()\n\t\t\tcolorsJson[name] = make(map[string]string)\n\n\t\t\tfor _, key := range section.Keys() {\n\t\t\t\tcolorsJson[name][key.Name()] = key.MustString(\"\")\n\t\t\t}\n\t\t}\n\t\tconfigJson.Schemes = colorsJson\n\t}\n\n\tconfigJsonBytes, err := json.MarshalIndent(configJson, \"\", \"    \")\n\tif err != nil {\n\t\tspinner.Fail(\"Failed to update theme's styles\")\n\t\tutils.PrintError(\"Cannot convert colors.ini to JSON\")\n\t} else {\n\t\tif err := os.WriteFile(\n\t\t\tfilepath.Join(appDestPath, \"xpui\", \"spicetify-config.json\"),\n\t\t\tconfigJsonBytes, 0700); err != nil {\n\t\t\tspinner.Fail(\"Failed to update theme's styles\")\n\t\t\tutils.PrintError(err.Error())\n\t\t} else {\n\t\t\tspinner.Success(\"Updated theme's styles\")\n\t\t}\n\t}\n}\n\nfunc refreshThemeAssets() {\n\tspinner, _ := utils.Spinner.Start(\"Updating custom assets\")\n\tapply.UserAsset(appDestPath, themeFolder)\n\tspinner.Success(\"Updated custom assets\")\n}\n\n// RefreshExtensions pushes all extensions to Spotify\nfunc RefreshExtensions(list ...string) {\n\tspinner, _ := utils.Spinner.Start(\"Refreshing extensions\")\n\tif len(list) == 0 {\n\t\tlist = featureSection.Key(\"extensions\").Strings(\"|\")\n\t}\n\n\tif len(list) > 0 {\n\t\tpushExtensions(\"\", list...)\n\t\tspinner.Success(\"Refreshed extensions\")\n\t} else {\n\t\tspinner.Info(\"No extensions to update\")\n\t}\n}\n\n// CheckStates examines both Backup and Spotify states to prompt informative\n// instruction for users\nfunc CheckStates() {\n\tbackupVersion := backupSection.Key(\"version\").MustString(\"\")\n\tbackStat := backupstatus.Get(prefsPath, backupFolder, backupVersion)\n\tspotStat := spotifystatus.Get(appPath)\n\n\tif backStat.IsEmpty() {\n\t\tif spotStat.IsBackupable() {\n\t\t\tutils.PrintError(`You haven't backed up. Run \"spicetify backup apply\"`)\n\t\t} else {\n\t\t\tutils.PrintError(`You haven't backed up and Spotify cannot be backed up at this state. Please re-install Spotify then run \"spicetify backup apply\"`)\n\t\t}\n\t\tos.Exit(1)\n\n\t} else if backStat.IsOutdated() {\n\t\tutils.PrintWarning(\"Spotify version and backup version are mismatched.\")\n\n\t\tif spotStat.IsMixed() {\n\t\t\tutils.PrintInfo(`Spotify client possibly just had a new update`)\n\t\t\tutils.PrintInfo(`Please run \"spicetify backup apply\"`)\n\t\t} else if spotStat.IsStock() {\n\t\t\tutils.PrintInfo(`Spotify client is in stock state`)\n\t\t\tutils.PrintInfo(`Please run \"spicetify backup apply\"`)\n\t\t} else {\n\t\t\tutils.PrintInfo(`Spotify cannot be backed up at this state. Please re-install Spotify then run \"spicetify backup apply\"`)\n\t\t}\n\n\t\tos.Exit(1)\n\t}\n}\n\nfunc refreshThemeJS() {\n\tspinner, _ := utils.Spinner.Start(\"Updating theme's script\")\n\tif err := utils.CopyFile(\n\t\tfilepath.Join(themeFolder, \"theme.js\"),\n\t\tfilepath.Join(appDestPath, \"xpui\", \"extensions\")); err != nil {\n\t\tspinner.Fail(\"Failed to update theme's script\")\n\t\tutils.PrintError(err.Error())\n\t} else {\n\t\tspinner.Success(\"Updated theme's script\")\n\t}\n}\n\nfunc pushExtensions(destExt string, list ...string) {\n\tvar err error\n\tvar dest string\n\tif len(destExt) > 0 {\n\t\tdest = filepath.Join(appDestPath, \"xpui\", \"extensions\", destExt)\n\t} else {\n\t\tdest = filepath.Join(appDestPath, \"xpui\", \"extensions\")\n\t}\n\n\tfor _, v := range list {\n\t\tvar extName, extPath string\n\n\t\tif filepath.IsAbs(v) {\n\t\t\textName = filepath.Base(v)\n\t\t\textPath = v\n\t\t} else {\n\t\t\textName = v\n\t\t\tif !strings.Contains(extName, \".js\") && !strings.Contains(extName, \".mjs\") {\n\t\t\t\textName += \".js\"\n\t\t\t}\n\t\t\textPath, err = utils.GetExtensionPath(extName)\n\t\t\tif err != nil {\n\t\t\t\tutils.PrintError(`Extension \"` + extName + `\" not found`)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif err = utils.CopyFile(extPath, dest); err != nil {\n\t\t\tutils.PrintError(err.Error())\n\t\t\tcontinue\n\t\t}\n\n\t\tif strings.HasSuffix(extName, \".mjs\") {\n\t\t\tutils.ModifyFile(filepath.Join(dest, extName), func(content string) string {\n\t\t\t\tlines := strings.Split(content, \"\\n\")\n\t\t\t\tfor i := 0; i < len(lines); i++ {\n\t\t\t\t\tmapping := utils.FindSymbol(\"\", lines[i], []string{\n\t\t\t\t\t\t`//\\s*spicetify_map\\{(.+?)\\}\\{(.+?)\\}`,\n\t\t\t\t\t})\n\t\t\t\t\tif len(mapping) > 0 {\n\t\t\t\t\t\tlines[i+1] = strings.Replace(lines[i+1], mapping[0], mapping[1], 1)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn strings.Join(lines, \"\\n\")\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc RefreshApps(list ...string) {\n\tspinner, _ := utils.Spinner.Start(\"Refreshing custom apps\")\n\tif len(list) == 0 {\n\t\tlist = featureSection.Key(\"custom_apps\").Strings(\"|\")\n\t}\n\n\tfor _, app := range list {\n\t\tappName := `spicetify-routes-` + app\n\n\t\tcustomAppPath, err := utils.GetCustomAppPath(app)\n\t\tif err != nil {\n\t\t\tutils.PrintError(`Custom app \"` + app + `\" not found`)\n\t\t\tcontinue\n\t\t}\n\n\t\tjsFile := filepath.Join(customAppPath, \"index.js\")\n\t\tjsFileContent, err := os.ReadFile(jsFile)\n\t\tif err != nil {\n\t\t\tutils.PrintError(`Custom app \"` + app + `\" does not have index.js`)\n\t\t\tcontinue\n\t\t}\n\n\t\tmanifestFile := filepath.Join(customAppPath, \"manifest.json\")\n\t\tmanifestFileContent, err := os.ReadFile(manifestFile)\n\t\tif err != nil {\n\t\t\tmanifestFileContent = []byte{'{', '}'}\n\t\t}\n\t\tos.WriteFile(\n\t\t\tfilepath.Join(appDestPath, \"xpui\", appName+\".json\"),\n\t\t\tmanifestFileContent,\n\t\t\t0700)\n\n\t\tvar manifestJson utils.AppManifest\n\t\tif err = json.Unmarshal(manifestFileContent, &manifestJson); err == nil {\n\t\t\tfor _, subfile := range manifestJson.Files {\n\t\t\t\tsubfilePath := filepath.Join(customAppPath, subfile)\n\t\t\t\tsubfileContent, err := os.ReadFile(subfilePath)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tjsFileContent = append(jsFileContent, '\\n')\n\t\t\t\tjsFileContent = append(jsFileContent, subfileContent...)\n\t\t\t}\n\t\t\tfor _, extensionFile := range manifestJson.ExtensionFiles {\n\t\t\t\tsubfilePath, err := filepath.Abs(filepath.Join(customAppPath, extensionFile))\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tpushExtensions(app, subfilePath)\n\t\t\t}\n\t\t\tfor _, assetExpr := range manifestJson.Assets {\n\t\t\t\tassetsList, err := filepath.Glob(filepath.Join(customAppPath, assetExpr))\n\t\t\t\tif err != nil {\n\t\t\t\t\tutils.PrintError(err.Error())\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif len(assetsList) == 0 {\n\t\t\t\t\tmessage := fmt.Sprintf(\"Custom App '%s': no assets found for expression '%s'\", app, assetExpr)\n\t\t\t\t\tutils.PrintWarning(message)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tfor _, assetPath := range assetsList {\n\t\t\t\t\tassetName, err := filepath.Rel(customAppPath, assetPath)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tutils.PrintError(err.Error())\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tstat, err := os.Stat(assetPath)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tutils.PrintError(err.Error())\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif stat.IsDir() {\n\t\t\t\t\t\tdest := filepath.Join(appDestPath, \"xpui\", \"assets\", app, assetName)\n\t\t\t\t\t\terr = utils.Copy(assetPath, dest, true, []string{})\n\t\t\t\t\t} else {\n\t\t\t\t\t\tdest := filepath.Join(appDestPath, \"xpui\", \"assets\", app, filepath.Dir(assetName))\n\t\t\t\t\t\terr = utils.CopyFile(assetPath, dest)\n\t\t\t\t\t}\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tutils.PrintError(err.Error())\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tjsTemplate := fmt.Sprintf(\n\t\t\t`((\"undefined\"!=typeof self?self:global).webpackChunkclient_web=(\"undefined\"!=typeof self?self:global).webpackChunkclient_web||[])\n.push([[\"%s\"],{\"%s\":(e,t,n)=>{\n\"use strict\";n.r(t),n.d(t,{default:()=>render});\n%s\n}}]);`,\n\t\t\tappName, appName, jsFileContent)\n\n\t\tos.WriteFile(\n\t\t\tfilepath.Join(appDestPath, \"xpui\", appName+\".js\"),\n\t\t\t[]byte(jsTemplate),\n\t\t\t0700)\n\n\t\tcssFile := filepath.Join(customAppPath, \"style.css\")\n\t\tcssFileContent, err := os.ReadFile(cssFile)\n\t\tif err != nil {\n\t\t\tcssFileContent = []byte{}\n\t\t}\n\t\tos.WriteFile(\n\t\t\tfilepath.Join(appDestPath, \"xpui\", appName+\".css\"),\n\t\t\t[]byte(cssFileContent),\n\t\t\t0700)\n\t}\n\n\tspinner.Success(\"Refreshed custom apps\")\n}\n\nfunc nodeModuleSymlink() {\n\tnodeModulePath, err := utils.GetExtensionPath(\"node_modules\")\n\tif err != nil {\n\t\treturn\n\t}\n\n\tspinner, _ := utils.Spinner.Start(\"Creating node_modules symlink\")\n\n\tnodeModuleDest := filepath.Join(appDestPath, \"xpui\", \"extensions\", \"node_modules\")\n\tif err = utils.CreateJunction(nodeModulePath, nodeModuleDest); err != nil {\n\t\tspinner.Fail(\"Failed to create node_modules symlink\")\n\t\tutils.PrintError(err.Error())\n\t\treturn\n\t}\n\n\tspinner.Success(\"Created node_modules symlink\")\n}\n"
  },
  {
    "path": "src/cmd/auto.go",
    "content": "package cmd\n\nimport (\n\t\"os\"\n\n\tbackupstatus \"github.com/spicetify/cli/src/status/backup\"\n\tspotifystatus \"github.com/spicetify/cli/src/status/spotify\"\n)\n\n// Auto checks Spotify state, re-backup and apply if needed, then launch\n// Spotify client normally.\nfunc Auto(spicetifyVersion string) {\n\tbackupVersion := backupSection.Key(\"version\").MustString(\"\")\n\tspotStat := spotifystatus.Get(appPath)\n\tbackStat := backupstatus.Get(prefsPath, backupFolder, backupVersion)\n\n\tif spotStat.IsBackupable() && (backStat.IsEmpty() || backStat.IsOutdated()) {\n\t\tBackup(spicetifyVersion, true)\n\t\tbackupVersion := backupSection.Key(\"version\").MustString(\"\")\n\t\tbackStat = backupstatus.Get(prefsPath, backupFolder, backupVersion)\n\t}\n\n\tif !backStat.IsBackuped() {\n\t\tos.Exit(1)\n\t}\n\n\tif isAppX {\n\t\tspotStat = spotifystatus.Get(appDestPath)\n\t}\n\n\tif !spotStat.IsApplied() && backStat.IsBackuped() {\n\t\tCheckStates()\n\t\tInitSetting()\n\t\tApply(spicetifyVersion)\n\t}\n}\n"
  },
  {
    "path": "src/cmd/backup.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\tspotifystatus \"github.com/spicetify/cli/src/status/spotify\"\n\n\t\"github.com/spicetify/cli/src/backup\"\n\t\"github.com/spicetify/cli/src/preprocess\"\n\tbackupstatus \"github.com/spicetify/cli/src/status/backup\"\n\t\"github.com/spicetify/cli/src/utils\"\n)\n\n// Backup stores original apps packages, extracts them and preprocesses extracted apps' assets\n// If silent is true, the final readiness message is suppressed (useful when chaining with \"apply\")\nfunc Backup(spicetifyVersion string, silent bool) {\n\tif isAppX {\n\t\tutils.PrintInfo(`You are using the Microsoft Store version of Spotify, which is only partly supported.\nDon't use the Microsoft Store version with Spicetify unless you absolutely CANNOT install Spotify from its installer.\nModded Spotify cannot be launched using original Shortcut/Start menu tile. To correctly launch modified Spotify, make a desktop shortcut that executes \"spicetify auto\". After that, you can change its icon, pin it to the start menu or put it in the startup folder.`)\n\t\tif !ReadAnswer(\"Continue backing up anyway?\", false, true) {\n\t\t\tos.Exit(1)\n\t\t}\n\t}\n\tbackupVersion := backupSection.Key(\"version\").MustString(\"\")\n\tbackStat := backupstatus.Get(prefsPath, backupFolder, backupVersion)\n\tif !backStat.IsEmpty() {\n\t\tutils.PrintInfo(\"A backup is available\")\n\n\t\tspotStat := spotifystatus.Get(appPath)\n\t\tif spotStat.IsBackupable() {\n\t\t\tclearBackup()\n\t\t} else {\n\t\t\tutils.PrintWarning(`After clearing backup, Spotify cannot be backed up again`)\n\t\t\tutils.PrintInfo(`Please restore first then backup, run \"spicetify restore backup\" or re-install Spotify then run \"spicetify backup\"`)\n\t\t\tos.Exit(1)\n\t\t}\n\t}\n\n\tspinner, _ := utils.Spinner.Start(\"Backing up app files\")\n\n\tif err := backup.Start(appPath, backupFolder); err != nil {\n\t\tspinner.Fail(\"Failed to backup app files\")\n\t\tutils.Fatal(err)\n\t}\n\n\tappList, err := os.ReadDir(backupFolder)\n\tif err != nil {\n\t\tspinner.Fail(\"Failed to backup app files\")\n\t\tutils.Fatal(err)\n\t}\n\n\ttotalApp := len(appList)\n\tif totalApp > 0 {\n\t\tspinner.Success(\"Backed up app files\")\n\t} else {\n\t\tspinner.Fail(\"Failed to backup app files\")\n\t\tutils.PrintInfo(\"Reinstall Spotify and try again\")\n\t\tos.Exit(1)\n\t}\n\n\tbackup.Extract(backupFolder, rawFolder)\n\n\tutils.PrintBold(\"Preprocessing\")\n\n\tspotifyBasePath := spotifyPath\n\tif spotifyBasePath == \"\" {\n\t\tutils.PrintError(\"Spotify installation path not found. Cannot preprocess V8 snapshots\")\n\t} else {\n\t\tpreprocess.Start(\n\t\t\tspicetifyVersion,\n\t\t\tspotifyBasePath,\n\t\t\trawFolder,\n\t\t\tpreprocess.Flag{\n\t\t\t\tDisableSentry:  preprocSection.Key(\"disable_sentry\").MustBool(false),\n\t\t\t\tDisableLogging: preprocSection.Key(\"disable_ui_logging\").MustBool(false),\n\t\t\t\tRemoveRTL:      preprocSection.Key(\"remove_rtl_rule\").MustBool(false),\n\t\t\t\tExposeAPIs:     preprocSection.Key(\"expose_apis\").MustBool(false),\n\t\t\t\tSpotifyVer:     utils.GetSpotifyVersion(prefsPath)},\n\t\t)\n\t\tutils.PrintSuccess(\"Preprocessing completed\")\n\t}\n\n\terr = utils.Copy(rawFolder, themedFolder, true, []string{\".html\", \".js\", \".css\"})\n\tif err != nil {\n\t\tutils.Fatal(err)\n\t}\n\n\tpreprocess.StartCSS(themedFolder)\n\n\tbackupSection.Key(\"version\").SetValue(utils.GetSpotifyVersion(prefsPath))\n\tbackupSection.Key(\"with\").SetValue(spicetifyVersion)\n\tif err := cfg.Write(); err != nil {\n\t\tutils.PrintWarning(fmt.Sprintf(\"Failed to save config: %s\", err.Error()))\n\t}\n\tif !silent {\n\t\tutils.PrintSuccess(\"Everything is ready, you can start applying!\")\n\t}\n}\n\n// Clear clears current backup. Before clearing, it checks whether Spotify is in\n// valid state to backup again.\nfunc Clear() {\n\tspotStat := spotifystatus.Get(appPath)\n\n\tif !spotStat.IsBackupable() {\n\t\tutils.PrintWarning(\"Before clearing backup, please restore or re-install Spotify to stock state\")\n\t\tos.Exit(1)\n\t}\n\n\tclearBackup()\n}\n\nfunc clearBackup() {\n\tspinner, _ := utils.Spinner.Start(\"Clearing current backup\")\n\tif err := os.RemoveAll(backupFolder); err != nil {\n\t\tspinner.Fail(\"Failed to clear current backup\")\n\t\tutils.Fatal(err)\n\t}\n\n\tif err := os.Mkdir(backupFolder, 0700); err != nil {\n\t\tspinner.Fail(\"Failed to clear current backup\")\n\t\tutils.Fatal(err)\n\t}\n\n\tif err := os.RemoveAll(rawFolder); err != nil {\n\t\tspinner.Fail(\"Failed to clear current backup\")\n\t\tutils.Fatal(err)\n\t}\n\n\tif err := os.Mkdir(rawFolder, 0700); err != nil {\n\t\tspinner.Fail(\"Failed to clear current backup\")\n\t\tutils.Fatal(err)\n\t}\n\n\tif err := os.RemoveAll(themedFolder); err != nil {\n\t\tspinner.Fail(\"Failed to clear current backup\")\n\t\tutils.Fatal(err)\n\t}\n\n\tif err := os.Mkdir(themedFolder, 0700); err != nil {\n\t\tspinner.Fail(\"Failed to clear current backup\")\n\t\tutils.Fatal(err)\n\t}\n\n\tbackupSection.Key(\"version\").SetValue(\"\")\n\tbackupSection.Key(\"with\").SetValue(\"\")\n\tif err := cfg.Write(); err != nil {\n\t\tutils.PrintWarning(fmt.Sprintf(\"Failed to save config: %s\", err.Error()))\n\t}\n\tspinner.Success(\"Cleared current backup\")\n}\n\n// Restore uses backup to revert every changes made by Spicetify.\nfunc Restore() {\n\tCheckStates()\n\tspinner, _ := utils.Spinner.Start(\"Restoring Spotify\")\n\tif err := os.RemoveAll(appDestPath); err != nil {\n\t\tspinner.Fail(\"Failed to restore Spotify\")\n\t\tutils.Fatal(err)\n\t}\n\n\tif err := utils.Copy(backupFolder, appDestPath, false, []string{\".spa\"}); err != nil {\n\t\tspinner.Fail(\"Failed to restore Spotify\")\n\t\tutils.Fatal(err)\n\t}\n\n\tspinner.Success(\"Restored Spotify\")\n}\n"
  },
  {
    "path": "src/cmd/block-updates.go",
    "content": "package cmd\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/spicetify/cli/src/utils\"\n)\n\n// Block spotify updates. Taken from https://github.com/Delusoire/bespoke-cli/blob/main/cmd/spotify/update.go\nfunc BlockSpotifyUpdates(disabled bool) {\n\tif runtime.GOOS == \"linux\" {\n\t\tutils.PrintError(\"Auto-updates on linux should be disabled in package manager you installed spotify with.\")\n\t\treturn\n\t}\n\tspotifyExecPath := GetSpotifyPath()\n\tswitch runtime.GOOS {\n\tcase \"windows\":\n\t\tspotifyExecPath = filepath.Join(spotifyExecPath, \"Spotify.exe\")\n\tcase \"darwin\":\n\t\tspotifyExecPath = filepath.Join(spotifyExecPath, \"..\", \"MacOS\", \"Spotify\")\n\t}\n\n\tvar str, msg string\n\tif runtime.GOOS == \"darwin\" {\n\t\thomeDir, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\tutils.PrintError(\"Cannot get user home directory\")\n\t\t\treturn\n\t\t}\n\t\tupdateDir := homeDir + \"/Library/Application Support/Spotify/PersistentCache/Update\"\n\t\tif disabled {\n\t\t\texec.Command(\"pkill\", \"Spotify\").Run()\n\t\t\texec.Command(\"mkdir\", \"-p\", updateDir).Run()\n\t\t\texec.Command(\"chflags\", \"uchg\", updateDir).Run()\n\t\t\tmsg = \"Disabled\"\n\t\t} else {\n\t\t\texec.Command(\"pkill\", \"Spotify\").Run()\n\t\t\texec.Command(\"mkdir\", \"-p\", updateDir).Run()\n\t\t\texec.Command(\"chflags\", \"nouchg\", updateDir).Run()\n\t\t\tmsg = \"Enabled\"\n\t\t}\n\n\t\tutils.PrintSuccess(msg + \" Spotify updates!\")\n\t\treturn\n\t}\n\n\tfile, err := os.OpenFile(spotifyExecPath, os.O_RDWR, 0644)\n\tif err != nil {\n\t\tutils.Fatal(err)\n\t\treturn\n\t}\n\tdefer file.Close()\n\n\tbuf := new(bytes.Buffer)\n\tbuf.ReadFrom(file)\n\tcontent := buf.String()\n\n\ti := strings.Index(content, \"desktop-update/\")\n\tif i == -1 {\n\t\tutils.PrintError(\"Can't find update endpoint in executable\")\n\t\treturn\n\t}\n\tif disabled {\n\t\tstr = \"no/thanks\"\n\t\tmsg = \"Disabled\"\n\t} else {\n\t\tstr = \"v2/update\"\n\t\tmsg = \"Enabled\"\n\t}\n\tfile.WriteAt([]byte(str), int64(i+15))\n\tutils.PrintSuccess(msg + \" Spotify updates!\")\n}\n"
  },
  {
    "path": "src/cmd/cmd.go",
    "content": "package cmd\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/go-ini/ini\"\n\t\"github.com/spicetify/cli/src/utils\"\n)\n\nvar (\n\tspicetifyFolder         = utils.GetSpicetifyFolder()\n\trawFolder, themedFolder = getExtractFolder()\n\tbackupFolder            = utils.GetStateFolder(\"Backup\")\n\tuserThemesFolder        = utils.GetSubFolder(spicetifyFolder, \"Themes\")\n\tquiet                   bool\n\tisAppX                  = false\n\tspotifyPath             string\n\tprefsPath               string\n\tappPath                 string\n\tappDestPath             string\n\tcfg                     utils.Config\n\tsettingSection          *ini.Section\n\tbackupSection           *ini.Section\n\tpreprocSection          *ini.Section\n\tfeatureSection          *ini.Section\n\tpatchSection            *ini.Section\n\tthemeFolder             string\n\tcolorCfg                *ini.File\n\tcolorSection            *ini.Section\n\tinjectCSS               bool\n\tinjectJS                bool\n\treplaceColors           bool\n\toverwriteAssets         bool\n)\n\n// InitConfig gets and parses config file.\nfunc InitConfig(isQuiet bool) {\n\tquiet = isQuiet\n\n\tcfg = utils.ParseConfig(GetConfigPath())\n\tsettingSection = cfg.GetSection(\"Setting\")\n\tbackupSection = cfg.GetSection(\"Backup\")\n\tpreprocSection = cfg.GetSection(\"Preprocesses\")\n\tfeatureSection = cfg.GetSection(\"AdditionalOptions\")\n\tpatchSection = cfg.GetSection(\"Patch\")\n}\n\n// InitPaths checks various essential paths' availabilities,\n// tries to auto-detect them and stops spicetify when any one\n// of them is invalid.\nfunc InitPaths() {\n\tspotifyPath = settingSection.Key(\"spotify_path\").String()\n\tprefsPath = settingSection.Key(\"prefs_path\").String()\n\n\tspotifyPath = utils.ReplaceEnvVarsInString(spotifyPath)\n\tprefsPath = utils.ReplaceEnvVarsInString(prefsPath)\n\ttestPath := filepath.Join(spotifyPath, \"Apps\")\n\n\tif _, err := os.Stat(testPath); err != nil {\n\t\tactualSpotifyPath := utils.FindAppPath()\n\n\t\tif len(actualSpotifyPath) == 0 {\n\t\t\tif len(spotifyPath) != 0 {\n\t\t\t\tutils.PrintError(spotifyPath + ` is not a valid path. Please manually set \"spotify_path\" in config-xpui.ini to correct directory of Spotify.`)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tutils.PrintError(`Cannot detect Spotify location. Please manually set \"spotify_path\" in config-xpui.ini`)\n\t\t\tif runtime.GOOS == \"windows\" {\n\t\t\t\tutils.PrintInfo(\"Please make sure Spotify is not installed via Microsoft Store. If it is, please uninstall it and install Spotify with their web installer.\")\n\t\t\t}\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\tspotifyPath = actualSpotifyPath\n\t\tsettingSection.Key(\"spotify_path\").SetValue(spotifyPath)\n\t\tif err := cfg.Write(); err != nil {\n\t\t\tutils.PrintWarning(fmt.Sprintf(\"Failed to save config: %s\", err.Error()))\n\t\t}\n\t}\n\n\tif _, err := os.Stat(prefsPath); err != nil {\n\t\tactualPrefsPath := utils.FindPrefFilePath()\n\n\t\tif len(actualPrefsPath) == 0 {\n\t\t\tif len(prefsPath) != 0 {\n\t\t\t\tutils.PrintError(prefsPath + ` does not exist or is not a valid path. Please manually set \"prefs_path\" in config-xpui.ini to correct path of \"prefs\" file.`)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t\tutils.PrintError(`Cannot detect Spotify \"prefs\" file location. Please manually set \"prefs_path\" in config-xpui.ini`)\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\tprefsPath = actualPrefsPath\n\t\tsettingSection.Key(\"prefs_path\").SetValue(prefsPath)\n\t\tif err := cfg.Write(); err != nil {\n\t\t\tutils.PrintWarning(fmt.Sprintf(\"Failed to save config: %s\", err.Error()))\n\t\t}\n\t}\n\n\tif runtime.GOOS == \"windows\" {\n\t\tif strings.Contains(spotifyPath, \"SpotifyAB.SpotifyMusic\") || strings.Contains(prefsPath, \"SpotifyAB.SpotifyMusic\") {\n\t\t\tisAppX = true\n\t\t}\n\t}\n\n\tappPath = filepath.Join(spotifyPath, \"Apps\")\n\n\tif isAppX {\n\t\tappDestPath = filepath.Join(spicetifyFolder, \"AppX\")\n\t} else {\n\t\tappDestPath = appPath\n\t}\n\n\tutils.CheckExistAndCreate(appDestPath)\n}\n\n// InitSetting parses theme settings and gets color section.\nfunc InitSetting() {\n\treplaceColors = settingSection.Key(\"replace_colors\").MustBool(false)\n\tinjectCSS = settingSection.Key(\"inject_css\").MustBool(false)\n\tinjectJS = settingSection.Key(\"inject_theme_js\").MustBool(false)\n\toverwriteAssets = settingSection.Key(\"overwrite_assets\").MustBool(false)\n\n\tthemeName := settingSection.Key(\"current_theme\").String()\n\n\tif len(themeName) == 0 {\n\t\tinjectCSS = false\n\t\tinjectJS = false\n\t\treplaceColors = false\n\t\toverwriteAssets = false\n\t\treturn\n\t}\n\n\tthemeFolder = getThemeFolder(themeName)\n\n\tcolorPath := filepath.Join(themeFolder, \"color.ini\")\n\tcssPath := filepath.Join(themeFolder, \"user.css\")\n\tassetsPath := filepath.Join(themeFolder, \"assets\")\n\tjsPath := filepath.Join(themeFolder, \"theme.js\")\n\n\tif replaceColors {\n\t\t_, err := os.Stat(colorPath)\n\t\treplaceColors = err == nil\n\t}\n\n\tif injectCSS {\n\t\t_, err := os.Stat(cssPath)\n\t\tinjectCSS = err == nil\n\t}\n\n\tif injectJS {\n\t\t_, err := os.Stat(jsPath)\n\t\tinjectJS = err == nil\n\t\tif err != nil {\n\t\t\tutils.CheckExistAndDelete(filepath.Join(appDestPath, \"xpui\", \"extensions/theme.js\"))\n\t\t}\n\t}\n\n\tif overwriteAssets {\n\t\t_, err := os.Stat(assetsPath)\n\t\toverwriteAssets = err == nil\n\t}\n\n\tvar err error\n\tcolorCfg, err = ini.InsensitiveLoad(colorPath)\n\tif err != nil {\n\t\tutils.PrintError(\"Cannot open file \" + colorPath)\n\t\treplaceColors = false\n\t}\n\n\tif !replaceColors {\n\t\treturn\n\t}\n\n\tsections := colorCfg.Sections()\n\n\tif len(sections) < 2 {\n\t\tutils.PrintError(\"No section found in \" + colorPath)\n\t\treplaceColors = false\n\t\treturn\n\t}\n\n\tschemeName := settingSection.Key(\"color_scheme\").String()\n\tif len(schemeName) == 0 {\n\t\tcolorSection = sections[1]\n\t\treturn\n\t}\n\n\tschemeSection, err := colorCfg.GetSection(schemeName)\n\tif err != nil {\n\t\tutils.PrintWarning(\"Color scheme '\" + schemeName + \"' not found; using first scheme\")\n\t\tcolorSection = sections[1]\n\t\treturn\n\t}\n\n\tcolorSection = schemeSection\n}\n\n// GetConfigPath returns location of config file\nfunc GetConfigPath() string {\n\treturn filepath.Join(spicetifyFolder, \"config-xpui.ini\")\n}\n\n// GetSpotifyPath returns location of Spotify client\nfunc GetSpotifyPath() string {\n\treturn spotifyPath\n}\n\nfunc getExtractFolder() (string, string) {\n\tdir := utils.GetStateFolder(\"Extracted\")\n\n\traw := filepath.Join(dir, \"Raw\")\n\tutils.CheckExistAndCreate(raw)\n\n\tthemed := filepath.Join(dir, \"Themed\")\n\tutils.CheckExistAndCreate(themed)\n\n\treturn raw, themed\n}\n\nfunc getThemeFolder(themeName string) string {\n\tfolder := filepath.Join(userThemesFolder, themeName)\n\t_, err := os.Stat(folder)\n\tif err == nil {\n\t\treturn folder\n\t}\n\n\tfolder = filepath.Join(utils.GetExecutableDir(), \"Themes\", themeName)\n\t_, err = os.Stat(folder)\n\tif err == nil {\n\t\treturn folder\n\t}\n\n\tutils.PrintError(`Theme \"` + themeName + `\" not found`)\n\tos.Exit(1)\n\treturn \"\"\n}\n\n// ReadAnswer prints out a yes/no form with string from `info`\n// and returns boolean value based on user input (y/Y or n/N) or\n// return `defaultAnswer` if input is omitted.\n// If input is neither of them, print form again.\n// If app is in quiet mode, returns quietModeAnswer without prompting.\nfunc ReadAnswer(info string, defaultAnswer bool, quietModeAnswer bool) bool {\n\tif quiet {\n\t\treturn quietModeAnswer\n\t}\n\n\tprompt := info\n\tif defaultAnswer {\n\t\tprompt += \" [Y/n]: \"\n\t} else {\n\t\tprompt += \" [y/N]: \"\n\t}\n\n\treader := bufio.NewReader(os.Stdin)\n\tfmt.Print(prompt)\n\ttext, _ := reader.ReadString('\\n')\n\ttext = strings.Replace(text, \"\\r\", \"\", 1)\n\ttext = strings.Replace(text, \"\\n\", \"\", 1)\n\tif len(text) == 0 {\n\t\treturn defaultAnswer\n\t} else if text == \"y\" || text == \"Y\" {\n\t\treturn true\n\t} else if text == \"n\" || text == \"N\" {\n\t\treturn false\n\t}\n\treturn ReadAnswer(info, defaultAnswer, quietModeAnswer)\n}\n\n// CheckUpdate fetches latest package version from Github API and inform user if there is new release\nfunc CheckUpdate(version string) {\n\tif !settingSection.Key(\"check_spicetify_update\").MustBool() || version == \"Dev\" {\n\t\treturn\n\t}\n\n\tlatestTag, err := utils.FetchLatestTag()\n\n\tif err != nil {\n\t\tutils.PrintError(\"Cannot fetch latest release info\")\n\t\tutils.PrintError(err.Error())\n\t\treturn\n\t}\n\n\tif latestTag != version {\n\t\tutils.PrintInfo(\"New version available: v\" + latestTag + \" (currently on: v\" + version + \")\")\n\t\tutils.PrintInfo(`Run \"spicetify update\" or use a package manager to update spicetify`)\n\t}\n}\n"
  },
  {
    "path": "src/cmd/color.go",
    "content": "package cmd\n\nimport (\n\t\"path/filepath\"\n\n\t\"github.com/go-ini/ini\"\n\t\"github.com/pterm/pterm\"\n\t\"github.com/spicetify/cli/src/utils\"\n)\n\n// EditColor changes one or multiple colors' values\nfunc EditColor(args []string) {\n\tif !initCmdColor() {\n\t\treturn\n\t}\n\n\tfor len(args) >= 2 {\n\t\tfield := args[0]\n\t\tvalue := args[1]\n\t\targs = args[2:]\n\n\t\tcolor := utils.ParseColor(value).Hex()\n\n\t\tif key, err := colorSection.GetKey(field); err == nil {\n\t\t\tkey.SetValue(color)\n\t\t\tcolorChangeSuccess(field, color)\n\t\t\tcontinue\n\t\t}\n\n\t\tif len(utils.BaseColorList[field]) > 0 {\n\t\t\tcolorSection.NewKey(field, color)\n\t\t\tcolorChangeSuccess(field, color)\n\t\t\tcontinue\n\t\t}\n\n\t\tutils.PrintWarning(`Color \"` + field + `\" unchanged: Not found.`)\n\t}\n\n\tcolorCfg.SaveTo(filepath.Join(themeFolder, \"color.ini\"))\n}\n\nfunc initCmdColor() bool {\n\tvar err error\n\n\tthemeName := settingSection.Key(\"current_theme\").String()\n\n\tif len(themeName) == 0 {\n\t\tutils.PrintError(`Config \"current_theme\" is blank.`)\n\t\treturn false\n\t}\n\n\tthemeFolder = getThemeFolder(themeName)\n\n\tcolorPath := filepath.Join(themeFolder, \"color.ini\")\n\n\tcolorCfg, err = ini.InsensitiveLoad(colorPath)\n\tif err != nil {\n\t\tutils.PrintError(\"Cannot open file \" + colorPath)\n\t\treturn false\n\t}\n\n\tsections := colorCfg.Sections()\n\n\tif len(sections) < 2 {\n\t\tutils.PrintError(\"No section found in \" + colorPath)\n\t\treturn false\n\t}\n\n\tschemeName := settingSection.Key(\"color_scheme\").String()\n\tif len(schemeName) == 0 {\n\t\tcolorSection = sections[1]\n\t} else {\n\t\tschemeSection, err := colorCfg.GetSection(schemeName)\n\t\tif err != nil {\n\t\t\tcolorSection = sections[1]\n\t\t} else {\n\t\t\tcolorSection = schemeSection\n\t\t}\n\t}\n\n\treturn true\n}\n\n// DisplayColors prints out every color name, hex and rgb value.\nfunc DisplayColors() {\n\tif !initCmdColor() {\n\t\treturn\n\t}\n\tdata := pterm.TableData{\n\t\t{\"Name\", \"Preview\", \"Hex\", \"RGB\"},\n\t}\n\tfor _, k := range utils.BaseColorOrder {\n\t\tcolorString := colorSection.Key(k).String()\n\n\t\tif len(colorString) == 0 {\n\t\t\tcolorString = utils.BaseColorList[k]\n\t\t\tk += \" (*)\"\n\t\t}\n\n\t\tcolor := utils.ParseColor(colorString)\n\t\tdata = append(data, []string{utils.Bold(k), colorPreview(color), color.Hex(), color.RGB()})\n\t}\n\n\tfor _, v := range colorSection.Keys() {\n\t\tk := v.Name()\n\n\t\tif len(utils.BaseColorList[k]) != 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcolor := utils.ParseColor(v.String())\n\t\tdata = append(data, []string{utils.Bold(k), colorPreview(color), color.Hex(), color.RGB()})\n\t}\n\n\tpterm.DefaultTable.WithHasHeader().WithData(data).Render()\n\n\tutils.PrintNote(\"(*): Default color is used\")\n}\n\nfunc colorChangeSuccess(field, value string) {\n\tutils.PrintSuccess(`Color changed: ` + field + ` = ` + value)\n\tutils.PrintInfo(`Run \"spicetify refresh\" to apply new color(s)`)\n}\n\nfunc colorPreview(color utils.Color) string {\n\treturn \"\\x1B[48;2;\" + color.TerminalRGB() + \"m       \\033[0m\"\n}\n"
  },
  {
    "path": "src/cmd/config-dir.go",
    "content": "package cmd\n\nimport (\n\t\"github.com/spicetify/cli/src/utils\"\n)\n\n// ShowConfigDirectory shows config directory in user's default file manager application\nfunc ShowConfigDirectory() {\n\tconfigDir := utils.GetSpicetifyFolder()\n\terr := utils.ShowDirectory(configDir)\n\tif err != nil {\n\t\tutils.PrintError(\"Error opening config directory:\")\n\t\tutils.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "src/cmd/config.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/go-ini/ini\"\n\t\"github.com/spicetify/cli/src/utils\"\n)\n\n// EditConfig changes one or multiple config value\nfunc EditConfig(args []string) {\n\tfor len(args) >= 2 {\n\t\tfield := args[0]\n\t\tvalue := args[1]\n\n\t\tswitch field {\n\t\tcase \"extensions\", \"custom_apps\":\n\t\t\tarrayType(featureSection, field, value)\n\t\tcase \"spotify_launch_flags\":\n\t\t\tcontinue\n\t\tcase \"prefs_path\", \"spotify_path\", \"current_theme\", \"color_scheme\":\n\t\t\tstringType(settingSection, field, value)\n\n\t\tdefault:\n\t\t\ttoggleType(field, value)\n\t\t}\n\n\t\targs = args[2:]\n\t}\n\n\tif err := cfg.Write(); err != nil {\n\t\tutils.PrintWarning(fmt.Sprintf(\"Failed to save config: %s\", err.Error()))\n\t}\n}\n\n// DisplayAllConfig displays all configs in all sections\nfunc DisplayAllConfig() {\n\tmaxLen := 30\n\tutils.PrintBold(\"Settings\")\n\tfor _, key := range settingSection.Keys() {\n\t\tname := key.Name()\n\t\tlog.Println(name + strings.Repeat(\" \", maxLen-len(name)) + key.Value())\n\t}\n\n\tlog.Println()\n\tutils.PrintBold(\"Preprocesses\")\n\tfor _, key := range preprocSection.Keys() {\n\t\tname := key.Name()\n\t\tlog.Println(name + strings.Repeat(\" \", maxLen-len(name)) + key.Value())\n\t}\n\n\tlog.Println()\n\tutils.PrintBold(\"AdditionalFeatures\")\n\tfor _, key := range featureSection.Keys() {\n\t\tname := key.Name()\n\t\tif name == \"extensions\" || name == \"custom_apps\" || name == \"spotify_launch_flags\" {\n\t\t\tlist := key.Strings(\"|\")\n\t\t\tlistLen := len(list)\n\t\t\tif listLen == 0 {\n\t\t\t\tlog.Println(name)\n\t\t\t} else {\n\t\t\t\tlog.Println(name + strings.Repeat(\" \", maxLen-len(name)) + strings.Join(list, \" | \"))\n\t\t\t}\n\t\t} else {\n\t\t\tlog.Println(name + strings.Repeat(\" \", maxLen-len(name)) + key.Value())\n\t\t}\n\t}\n\n\tlog.Println()\n\tutils.PrintBold(\"Backup\")\n\tfor _, key := range backupSection.Keys() {\n\t\tname := key.Name()\n\t\tlog.Println(name + strings.Repeat(\" \", maxLen-len(name)) + key.Value())\n\t}\n}\n\n// DisplayConfig displays value of requested config field\nfunc DisplayConfig(field string) {\n\tkey := searchField(field)\n\n\tname := key.Name()\n\tif name == \"extensions\" || name == \"custom_apps\" {\n\t\tlist := key.Strings(\"|\")\n\t\tfor _, ext := range list {\n\t\t\tlog.Println(ext)\n\t\t}\n\t\treturn\n\t}\n\n\tlog.Println(key.Value())\n}\n\n// searchField finds requested field in all three config sections\nfunc searchField(field string) *ini.Key {\n\tkey, err := settingSection.GetKey(field)\n\tif err != nil {\n\t\tkey, err = preprocSection.GetKey(field)\n\t\tif err != nil {\n\t\t\tkey, err = featureSection.GetKey(field)\n\t\t\tif err != nil {\n\t\t\t\tunchangeWarning(field, `Not a valid field.`)\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\t\t}\n\t}\n\treturn key\n}\n\nfunc changeSuccess(key, value string) {\n\tutils.PrintSuccess(`Config changed: ` + key + ` = ` + value)\n\tutils.PrintInfo(`Run \"spicetify apply\" to apply new config`)\n}\n\nfunc unchangeWarning(field, reason string) {\n\tutils.PrintWarning(`Config \"` + field + `\" unchanged: ` + reason)\n}\n\nfunc arrayType(section *ini.Section, field, value string) {\n\tkey, err := section.GetKey(field)\n\tif err != nil {\n\t\tutils.Fatal(err)\n\t}\n\n\tif strings.TrimSpace(value) == \"\" {\n\t\tkey.SetValue(\"\")\n\t\tchangeSuccess(field, \"\")\n\t\treturn\n\t}\n\n\tallExts := make(map[string]bool)\n\tfor _, v := range key.Strings(\"|\") {\n\t\tallExts[v] = true\n\t}\n\n\tvalues := strings.Split(value, \"|\")\n\tduplicates := []string{}\n\tinputValues := make(map[string]bool)\n\tmodifiedValues := 0\n\n\tfor _, value := range values {\n\t\tisSubstract := strings.HasSuffix(value, \"-\")\n\t\tif isSubstract {\n\t\t\tvalue = value[:len(value)-1]\n\t\t}\n\n\t\tif isSubstract {\n\t\t\tif _, found := allExts[value]; !found {\n\t\t\t\tunchangeWarning(field, fmt.Sprintf(\"%s is not on the list.\", value))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tmodifiedValues++\n\t\t\tdelete(allExts, value)\n\t\t} else {\n\t\t\tif _, found := allExts[value]; found && !inputValues[value] {\n\t\t\t\tduplicates = append(duplicates, value)\n\t\t\t} else if _, found := allExts[value]; !found {\n\t\t\t\tallExts[value] = true\n\t\t\t\tmodifiedValues++\n\t\t\t}\n\n\t\t\tinputValues[value] = true\n\t\t}\n\t}\n\n\tif len(duplicates) > 0 {\n\t\tunchangeWarning(field, fmt.Sprintf(\"%s %s already in the list.\", strings.Join(duplicates, \", \"), pluralize(len(duplicates), \"is\", \"are\")))\n\t}\n\n\tif modifiedValues == 0 {\n\t\treturn\n\t}\n\n\tnewList := make([]string, 0, len(allExts))\n\tfor k := range allExts {\n\t\tnewList = append(newList, k)\n\t}\n\n\tkey.SetValue(strings.Join(newList, \"|\"))\n\tchangeSuccess(field, strings.Join(newList, \"|\"))\n}\n\nfunc pluralize(count int, singular, plural string) string {\n\tif count == 1 {\n\t\treturn singular\n\t}\n\treturn plural\n}\n\nfunc stringType(section *ini.Section, field, value string) {\n\tkey, err := section.GetKey(field)\n\tif err != nil {\n\t\tutils.Fatal(err)\n\t}\n\tif len(strings.TrimSpace(value)) == 0 || value[len(value)-1] == '-' {\n\t\tvalue = \"\"\n\t}\n\tkey.SetValue(value)\n\n\tchangeSuccess(field, value)\n}\n\nfunc toggleType(field, value string) {\n\tkey := searchField(field)\n\n\tif value != \"0\" && value != \"1\" {\n\t\tunchangeWarning(field, `\"`+value+`\" is not valid value. Only \"0\" or \"1\" are valid.`)\n\t\treturn\n\t}\n\n\tkey.SetValue(value)\n\tchangeSuccess(field, value)\n}\n"
  },
  {
    "path": "src/cmd/devtools.go",
    "content": "package cmd\n\nimport (\n\t\"bytes\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/spicetify/cli/src/utils\"\n)\n\n// EnableDevTools enables the developer tools in the Spotify client\nfunc EnableDevTools() {\n\tvar filePath string\n\n\tswitch runtime.GOOS {\n\tcase \"windows\":\n\t\tappFilePath := os.Getenv(\"LOCALAPPDATA\") + \"\\\\Spotify\\\\offline.bnk\"\n\t\tif _, err := os.Stat(appFilePath); err == nil {\n\t\t\tfilePath = appFilePath\n\t\t} else if len(utils.WinXApp()) != 0 && len(utils.WinXPrefs()) != 0 {\n\t\t\tdir, _ := filepath.Split(utils.WinXPrefs())\n\t\t\tfilePath = filepath.Join(dir, \"offline.bnk\")\n\t\t}\n\tcase \"linux\":\n\t\t{\n\t\t\thomePath := os.Getenv(\"HOME\")\n\t\t\tsnapSpotifyHome := homePath + \"/snap/spotify/common\"\n\t\t\tif _, err := os.Stat(snapSpotifyHome); err == nil {\n\t\t\t\thomePath = snapSpotifyHome\n\t\t\t}\n\n\t\t\tflatpakHome := homePath + \"/.var/app/com.spotify.Client\"\n\t\t\tif _, err := os.Stat(flatpakHome); err == nil {\n\t\t\t\thomePath = flatpakHome\n\t\t\t\tfilePath = homePath + \"/cache/spotify/offline.bnk\"\n\t\t\t} else {\n\t\t\t\tfilePath = homePath + \"/.cache/spotify/offline.bnk\"\n\t\t\t}\n\n\t\t}\n\tcase \"darwin\":\n\t\tfilePath = os.Getenv(\"HOME\") + \"/Library/Application Support/Spotify/PersistentCache/offline.bnk\"\n\t}\n\n\tif _, err := os.Stat(filePath); os.IsNotExist(err) {\n\t\tutils.PrintError(\"Can't find \\\"offline.bnk\\\". Try running spotify first.\")\n\t\tos.Exit(1)\n\t}\n\n\tfile, err := os.OpenFile(filePath, os.O_RDWR, 0644)\n\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tdefer file.Close()\n\n\tbuf := new(bytes.Buffer)\n\tbuf.ReadFrom(file)\n\tcontent := buf.String()\n\tfirstLocation := strings.Index(content, \"app-developer\")\n\tfirstPatchLocation := int64(firstLocation + 14)\n\n\tsecondLocation := strings.LastIndex(content, \"app-developer\")\n\tsecondPatchLocation := int64(secondLocation + 15)\n\n\tfile.WriteAt([]byte{50}, firstPatchLocation)\n\tfile.WriteAt([]byte{50}, secondPatchLocation)\n\tutils.PrintSuccess(\"Enabled DevTools!\")\n}\n"
  },
  {
    "path": "src/cmd/patch.go",
    "content": "package cmd\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/spicetify/cli/src/utils\"\n)\n\nfunc Patch() {\n\tutils.PrintBold(\"Applying custom patches:\")\n\tkeys := patchSection.Keys()\n\n\tre := regexp.MustCompile(`^([\\w\\d\\-~\\.]+)_find_(\\d+)$`)\n\tfor _, key := range keys {\n\t\tkeyName := key.Name()\n\t\tmatches := re.FindStringSubmatch(keyName)\n\t\tif len(matches) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tname := matches[1]\n\t\tassetPath := filepath.Join(appPath, \"xpui\", name)\n\t\tindex := matches[2]\n\n\t\tif _, err := os.Stat(assetPath); err != nil {\n\t\t\tutils.PrintError(\"File name \\\"\" + name + \"\\\" is not found.\")\n\t\t\tcontinue\n\t\t}\n\n\t\treplName := name + \"_repl_all_\" + index\n\t\treplOnceName := name + \"_repl_\" + index\n\t\treplKey, errAll := patchSection.GetKey(replName)\n\t\treplOnceKey, errOnce := patchSection.GetKey(replOnceName)\n\n\t\tif errAll != nil && errOnce != nil {\n\t\t\tutils.PrintError(\"Cannot find replace string for patch \\\"\" + keyName + \"\\\"\")\n\t\t\tutils.PrintInfo(\"Correct key name for replace string are:\")\n\t\t\tutils.PrintInfo(\"    \\\"\" + replOnceName + \"\\\"\")\n\t\t\tutils.PrintInfo(\"    \\\"\" + replName + \"\\\"\")\n\t\t\tcontinue\n\t\t}\n\n\t\tpatchRegexp, errReg := regexp.Compile(key.String())\n\t\tif errReg != nil {\n\t\t\tutils.PrintError(\"Cannot compile find RegExp for patch \\\"\" + keyName + \"\\\"\")\n\t\t\tcontinue\n\t\t}\n\n\t\tutils.ModifyFile(assetPath, func(content string) string {\n\t\t\t// Prioritize replace all\n\t\t\tif errAll == nil {\n\t\t\t\treturn patchRegexp.ReplaceAllString(content, replKey.MustString(\"\"))\n\t\t\t} else {\n\t\t\t\tmatch := patchRegexp.FindString(content)\n\t\t\t\tif len(match) > 0 {\n\t\t\t\t\ttoReplace := patchRegexp.ReplaceAllString(match, replOnceKey.MustString(\"\"))\n\t\t\t\t\tcontent = strings.Replace(content, match, toReplace, 1)\n\t\t\t\t}\n\t\t\t\treturn content\n\t\t\t}\n\t\t})\n\n\t\tutils.PrintSuccess(\"\\\"\" + keyName + \"\\\" is patched\")\n\t}\n\n\tutils.PrintSuccess(\"Applied custom patches\")\n}\n"
  },
  {
    "path": "src/cmd/path.go",
    "content": "package cmd\n\nimport (\n\t\"errors\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/spicetify/cli/src/utils\"\n)\n\n// ThemeAssetPath returns path of theme; assets, color.ini, theme.js and user.css\nfunc ThemeAssetPath(kind string) (string, error) {\n\tInitSetting()\n\n\tif kind == \"root\" {\n\t\treturn filepath.Join(utils.GetExecutableDir(), \"Themes\"), nil\n\t} else if len(themeFolder) == 0 {\n\t\treturn \"\", errors.New(`config \"current_theme\" is blank`)\n\t}\n\n\tif kind == \"folder\" {\n\t\treturn themeFolder, nil\n\t} else if kind == \"color\" {\n\t\tcolor := filepath.Join(themeFolder, \"color.ini\")\n\t\treturn color, nil\n\t} else if kind == \"css\" {\n\t\tcss := filepath.Join(themeFolder, \"user.css\")\n\t\treturn css, nil\n\t} else if kind == \"js\" {\n\t\tjs := filepath.Join(themeFolder, \"theme.js\")\n\t\treturn js, nil\n\t} else if kind == \"assets\" {\n\t\tassets := filepath.Join(themeFolder, \"assets\")\n\t\treturn assets, nil\n\t}\n\n\treturn \"\", errors.New(`unrecognized theme assets kind. only \"root\", \"folder\", \"color\", \"css\", \"js\" or \"assets\" is valid`)\n}\n\n// ThemeAllAssetsPath returns paths of all theme's assets\nfunc ThemeAllAssetsPath() (string, error) {\n\tInitSetting()\n\n\tif len(themeFolder) == 0 {\n\t\treturn \"\", errors.New(`config \"current_theme\" is blank`)\n\t}\n\n\tresults := []string{\n\t\tthemeFolder,\n\t\tfilepath.Join(themeFolder, \"color.ini\"),\n\t\tfilepath.Join(themeFolder, \"user.css\"),\n\t\tfilepath.Join(themeFolder, \"theme.js\"),\n\t\tfilepath.Join(themeFolder, \"assets\")}\n\n\treturn strings.Join(results, \"\\n\"), nil\n}\n\n// ExtensionPath return path of extension file\nfunc ExtensionPath(name string) (string, error) {\n\tif name == \"root\" {\n\t\treturn filepath.Join(utils.GetExecutableDir(), \"Extensions\"), nil\n\t}\n\treturn utils.GetExtensionPath(name)\n}\n\n// ExtensionAllPath returns paths of all extension files\nfunc ExtensionAllPath() (string, error) {\n\texts := featureSection.Key(\"extensions\").Strings(\"|\")\n\tresults := []string{}\n\tfor _, v := range exts {\n\t\tpath, err := utils.GetExtensionPath(v)\n\t\tif err != nil {\n\t\t\tpath = utils.Red(\"Extension \" + v + \" not found\")\n\t\t}\n\t\tresults = append(results, path)\n\t}\n\n\treturn strings.Join(results, \"\\n\"), nil\n}\n\n// AppPath return path of app directory\nfunc AppPath(name string) (string, error) {\n\tif name == \"root\" {\n\t\treturn filepath.Join(utils.GetExecutableDir(), \"CustomApps\"), nil\n\t}\n\treturn utils.GetCustomAppPath(name)\n}\n\n// AppAllPath returns paths of all apps\nfunc AppAllPath() (string, error) {\n\texts := featureSection.Key(\"custom_apps\").Strings(\"|\")\n\tresults := []string{}\n\tfor _, v := range exts {\n\t\tpath, err := utils.GetCustomAppPath(v)\n\t\tif err != nil {\n\t\t\tpath = utils.Red(\"App \" + v + \" not found\")\n\t\t}\n\t\tresults = append(results, path)\n\t}\n\n\treturn strings.Join(results, \"\\n\"), nil\n}\n\nfunc AllPaths() (string, error) {\n\ttheme, _ := ThemeAllAssetsPath()\n\text, _ := ExtensionAllPath()\n\tapp, _ := AppAllPath()\n\n\treturn strings.Join([]string{theme, ext, app}, \"\\n\"), nil\n}\n"
  },
  {
    "path": "src/cmd/restart.go",
    "content": "package cmd\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n)\n\nfunc SpotifyKill() {\n\tswitch runtime.GOOS {\n\tcase \"windows\":\n\t\tisRunning := exec.Command(\"tasklist\", \"/FI\", \"ImageName eq spotify.exe\")\n\t\tresult, _ := isRunning.Output()\n\t\tif !bytes.Contains(result, []byte(\"No tasks are running\")) {\n\t\t\texec.Command(\"taskkill\", \"/F\", \"/IM\", \"spotify.exe\").Run()\n\t\t}\n\tcase \"linux\":\n\t\tisRunning := exec.Command(\"pgrep\", \"-x\", \"spotify\")\n\t\t_, err := isRunning.Output()\n\t\tif err == nil {\n\t\t\texec.Command(\"pkill\", \"-x\", \"spotify\").Run()\n\t\t}\n\tcase \"darwin\":\n\t\tisRunning := exec.Command(\"sh\", \"-c\", \"ps aux | grep 'Spotify' | grep -v grep\")\n\t\t_, err := isRunning.CombinedOutput()\n\t\tif err == nil {\n\t\t\texec.Command(\"pkill\", \"-x\", \"Spotify\").Run()\n\t\t}\n\t}\n}\n\nfunc SpotifyStart(flags ...string) {\n\tenableDevtools := settingSection.Key(\"always_enable_devtools\").MustBool(false)\n\tif enableDevtools {\n\t\tEnableDevTools()\n\t}\n\n\tlaunchFlag := settingSection.Key(\"spotify_launch_flags\").Strings(\"|\")\n\tif len(launchFlag) > 0 {\n\t\tflags = append(flags, launchFlag...)\n\t}\n\n\tswitch runtime.GOOS {\n\tcase \"windows\":\n\t\tif isAppX {\n\t\t\tps, _ := exec.LookPath(\"powershell.exe\")\n\t\t\texe := filepath.Join(os.Getenv(\"LOCALAPPDATA\"), \"Microsoft\", \"WindowsApps\", \"Spotify.exe\")\n\t\t\tcmd := `& \"` + exe + `\" --app-directory=\"` + appDestPath + `\"`\n\t\t\tif len(flags) > 0 {\n\t\t\t\tcmd += \" \" + strings.Join(flags, \" \")\n\t\t\t}\n\t\t\texec.Command(ps, \"-NoProfile\", \"-NonInteractive\", \"-Command\", cmd).Start()\n\t\t} else {\n\t\t\texec.Command(filepath.Join(spotifyPath, \"spotify.exe\"), flags...).Start()\n\t\t}\n\tcase \"linux\":\n\t\texec.Command(filepath.Join(spotifyPath, \"spotify\"), flags...).Start()\n\tcase \"darwin\":\n\t\tflags = append([]string{\"-a\", \"/Applications/Spotify.app\", \"--args\"}, flags...)\n\t\texec.Command(\"open\", flags...).Start()\n\t}\n}\n\nfunc SpotifyRestart(flags ...string) {\n\tSpotifyKill()\n\tSpotifyStart(flags...)\n}\n"
  },
  {
    "path": "src/cmd/update.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\n\t\"github.com/spicetify/cli/src/utils\"\n)\n\nfunc Update(currentVersion string) bool {\n\ttagName, err := utils.FetchLatestTag()\n\tif err != nil {\n\t\tutils.PrintError(\"Cannot fetch latest release info\")\n\t\tutils.PrintError(err.Error())\n\t\treturn false\n\t}\n\tif currentVersion == tagName {\n\t\tutils.PrintSuccess(\"Spicetify is up-to-date.\")\n\t\treturn false\n\t}\n\n\tutils.PrintInfo(\"Latest release: \" + tagName)\n\tvar assetURL string = \"https://github.com/spicetify/cli/releases/download/v\" + tagName + \"/spicetify-\" + tagName + \"-\" + runtime.GOOS + \"-\"\n\tvar location string = os.TempDir() + \"/spicetify-\" + tagName\n\n\tif runtime.GOARCH == \"386\" && runtime.GOOS == \"windows\" {\n\t\tassetURL += \"x32\"\n\t} else if runtime.GOARCH == \"arm64\" {\n\t\tassetURL += \"arm64\"\n\t} else if runtime.GOOS == \"windows\" {\n\t\tassetURL += \"x64\"\n\t} else {\n\t\tassetURL += \"amd64\"\n\t}\n\n\tif runtime.GOOS == \"windows\" {\n\t\tassetURL += \".zip\"\n\t\tlocation += \".zip\"\n\t} else {\n\t\tassetURL += \".tar.gz\"\n\t\tlocation += \".tar.gz\"\n\t}\n\n\tspinner, _ := utils.Spinner.Start(\"Downloading Spicetify\")\n\n\tout, err := os.Create(location)\n\tif err != nil {\n\t\tspinner.Fail(\"Failed to download Spicetify\")\n\t\tutils.Fatal(err)\n\t}\n\tdefer out.Close()\n\n\tresp2, err := http.Get(assetURL)\n\tif err != nil {\n\t\tspinner.Fail(\"Failed to download Spicetify\")\n\t\tutils.Fatal(err)\n\t}\n\tdefer resp2.Body.Close()\n\n\tif resp2.StatusCode != http.StatusOK {\n\t\tspinner.Fail(\"Failed to download Spicetify\")\n\t\tutils.Fatal(fmt.Errorf(\"unexpected HTTP status: %s for %s\", resp2.Status, assetURL))\n\t}\n\n\t_, err = io.Copy(out, resp2.Body)\n\tif err != nil {\n\t\tspinner.Fail(\"Failed to download Spicetify\")\n\t\tutils.Fatal(err)\n\t}\n\tspinner.Success(\"Downloaded Spicetify\")\n\n\texe, err := os.Executable()\n\tif err != nil {\n\t\tutils.Fatal(err)\n\t}\n\tif exe, err = filepath.EvalSymlinks(exe); err != nil {\n\t\tutils.Fatal(err)\n\t}\n\n\texeOld := exe + \".old\"\n\tutils.CheckExistAndDelete(exeOld)\n\n\tif err = os.Rename(exe, exeOld); err != nil {\n\t\tpermissionError(err)\n\t}\n\n\tswitch runtime.GOOS {\n\tcase \"windows\":\n\t\terr = utils.Unzip(location, utils.GetExecutableDir())\n\n\tcase \"linux\", \"darwin\":\n\t\terr = exec.Command(\"tar\", \"-xzf\", location, \"-C\", utils.GetExecutableDir()).Run()\n\t}\n\tif err != nil {\n\t\tos.Rename(exeOld, exe)\n\t\tpermissionError(err)\n\t}\n\n\tutils.CheckExistAndDelete(exeOld)\n\tutils.PrintSuccess(\"Successfully updated Spicetify to v\" + tagName)\n\treturn true\n}\n\nfunc permissionError(err error) {\n\tutils.PrintInfo(\"If fatal error is \\\"Permission denied\\\", please check read/write permission of spicetify executable directory.\")\n\tutils.PrintInfo(\"However, if you used a package manager to install spicetify, please upgrade by using the same package manager.\")\n\tutils.Fatal(err)\n}\n"
  },
  {
    "path": "src/cmd/watch.go",
    "content": "package cmd\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"time\"\n\n\tspotifystatus \"github.com/spicetify/cli/src/status/spotify\"\n\t\"github.com/spicetify/cli/src/utils\"\n)\n\nvar (\n\tdebuggerURL    string\n\tautoReloadFunc func()\n\twatchQueue     chan func()\n\twatchQueueOnce sync.Once\n)\n\n// Watch .\nfunc Watch(liveUpdate bool) {\n\tif !isValidForWatching() {\n\t\tos.Exit(1)\n\t}\n\n\tInitSetting()\n\n\tif liveUpdate {\n\t\tstartDebugger()\n\t}\n\n\tif len(themeFolder) == 0 {\n\t\tutils.PrintError(`Config \"current_theme\" is blank. No theme asset to watch`)\n\t\tos.Exit(1)\n\t}\n\n\tcolorPath := filepath.Join(themeFolder, \"color.ini\")\n\tcssPath := filepath.Join(themeFolder, \"user.css\")\n\n\tfileList := []string{}\n\tif replaceColors {\n\t\tfileList = append(fileList, colorPath)\n\t}\n\n\tif injectCSS {\n\t\tfileList = append(fileList, cssPath)\n\t}\n\n\tif injectJS {\n\t\tjsPath := filepath.Join(themeFolder, \"theme.js\")\n\t\tpathArr := []string{jsPath}\n\n\t\tif _, err := os.Stat(jsPath); err == nil {\n\t\t\tgo utils.Watch(pathArr, func(_ string, err error) {\n\t\t\t\tif err != nil {\n\t\t\t\t\tutils.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\tenqueueWatchJob(func() {\n\t\t\t\t\trefreshThemeJS()\n\t\t\t\t\tif autoReloadFunc != nil {\n\t\t\t\t\t\tautoReloadFunc()\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}, nil)\n\t\t}\n\t}\n\n\tif overwriteAssets {\n\t\tassetPath := filepath.Join(themeFolder, \"assets\")\n\n\t\tif _, err := os.Stat(assetPath); err == nil {\n\t\t\tgo utils.WatchRecursive(assetPath, func(_ string, err error) {\n\t\t\t\tif err != nil {\n\t\t\t\t\tutils.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\tenqueueWatchJob(func() {\n\t\t\t\t\trefreshThemeAssets()\n\t\t\t\t\tif autoReloadFunc != nil {\n\t\t\t\t\t\tautoReloadFunc()\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}, nil)\n\t\t}\n\t}\n\n\tutils.Watch(fileList, func(_ string, err error) {\n\t\tif err != nil {\n\t\t\tutils.Fatal(err)\n\t\t}\n\n\t\tenqueueWatchJob(func() {\n\t\t\tInitSetting()\n\t\t\trefreshThemeCSS()\n\t\t\tif autoReloadFunc != nil {\n\t\t\t\tautoReloadFunc()\n\t\t\t}\n\t\t})\n\t}, nil)\n}\n\n// WatchExtensions .\nfunc WatchExtensions(extName []string, liveUpdate bool) {\n\tif !isValidForWatching() {\n\t\tos.Exit(1)\n\t}\n\n\tif liveUpdate {\n\t\tstartDebugger()\n\t}\n\n\tvar extNameList []string\n\tif len(extName) > 0 {\n\t\textNameList = extName\n\t} else {\n\t\textNameList = featureSection.Key(\"extensions\").Strings(\"|\")\n\t}\n\n\tvar extPathList []string\n\n\tfor _, v := range extNameList {\n\t\textPath, err := utils.GetExtensionPath(v)\n\t\tif err != nil {\n\t\t\tutils.PrintError(`Extension \"` + v + `\" not found.`)\n\t\t\tcontinue\n\t\t}\n\t\textPathList = append(extPathList, extPath)\n\t}\n\n\tif len(extPathList) == 0 {\n\t\tutils.PrintError(\"No extension to watch\")\n\t\tos.Exit(1)\n\t}\n\n\tutils.Watch(extPathList, func(filePath string, err error) {\n\t\tif err != nil {\n\t\t\tutils.PrintError(err.Error())\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\tpushExtensions(\"\", filePath)\n\n\t\tutils.PrintSuccess(utils.PrependTime(`Extension \"` + filePath + `\" is updated`))\n\t}, autoReloadFunc)\n}\n\n// WatchCustomApp .\nfunc WatchCustomApp(appName []string, liveUpdate bool) {\n\tif !isValidForWatching() {\n\t\tos.Exit(1)\n\t}\n\n\tif liveUpdate {\n\t\tstartDebugger()\n\t}\n\n\tvar appNameList []string\n\tif len(appName) > 0 {\n\t\tappNameList = appName\n\t} else {\n\t\tappNameList = featureSection.Key(\"custom_apps\").Strings(\"|\")\n\t}\n\n\tthreadCount := 0\n\tfor _, v := range appNameList {\n\t\tappPath, err := utils.GetCustomAppPath(v)\n\t\tif err != nil {\n\t\t\tutils.PrintError(`Custom app \"` + v + `\" not found`)\n\t\t\tcontinue\n\t\t}\n\n\t\tvar appFileList []string\n\t\tjsFilePath := filepath.Join(appPath, \"index.js\")\n\t\tif _, err := os.Stat(jsFilePath); err != nil {\n\t\t\tutils.PrintError(`Custom app \"` + v + `\" does not contain index.js`)\n\t\t\tcontinue\n\t\t}\n\t\tappFileList = append(appFileList, jsFilePath)\n\t\tcssFilePath := filepath.Join(appPath, \"style.css\")\n\t\tif _, err := os.Stat(cssFilePath); err == nil {\n\t\t\tappFileList = append(appFileList, cssFilePath)\n\t\t}\n\n\t\tmanifestPath := filepath.Join(appPath, \"manifest.json\")\n\t\tmanifestFileContent, err := os.ReadFile(manifestPath)\n\t\tif err == nil {\n\t\t\tvar manifestJson utils.AppManifest\n\t\t\tif err = json.Unmarshal(manifestFileContent, &manifestJson); err == nil {\n\t\t\t\tfor _, subfile := range manifestJson.Files {\n\t\t\t\t\tsubfilePath := filepath.Join(appPath, subfile)\n\t\t\t\t\tappFileList = append(appFileList, subfilePath)\n\t\t\t\t}\n\t\t\t\tfor _, subfile := range manifestJson.ExtensionFiles {\n\t\t\t\t\tsubfilePath := filepath.Join(appPath, subfile)\n\t\t\t\t\tappFileList = append(appFileList, subfilePath)\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\n\t\tthreadCount += 1\n\t\tvar appName = v\n\t\tgo utils.Watch(appFileList, func(filePath string, err error) {\n\t\t\tif err != nil {\n\t\t\t\tutils.PrintError(err.Error())\n\t\t\t\tos.Exit(1)\n\t\t\t}\n\n\t\t\tRefreshApps(appName)\n\n\t\t\tutils.PrintSuccess(utils.PrependTime(`Custom app \"` + appName + `\" is updated`))\n\t\t}, autoReloadFunc)\n\t}\n\n\tif threadCount > 0 {\n\t\tfor {\n\t\t\ttime.Sleep(utils.INTERVAL)\n\t\t}\n\t}\n}\n\nfunc isValidForWatching() bool {\n\tstatus := spotifystatus.Get(appDestPath)\n\n\tif !status.IsModdable() {\n\t\tutils.PrintError(`You haven't applied. Run \"spicetify apply\" once before entering watch mode`)\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc startDebugger() {\n\tif len(utils.GetDebuggerPath()) == 0 {\n\t\tEnableDevTools()\n\t\tSpotifyRestart(\"--remote-debugging-port=9222\", \"--remote-allow-origins=*\")\n\t\tutils.PrintInfo(\"Restarted Spotify with debugger on. Waiting...\")\n\t\tfor len(utils.GetDebuggerPath()) == 0 {\n\t\t\t// Wait until debugger is up\n\t\t}\n\t}\n\tautoReloadFunc = func() {\n\t\tif utils.SendReload(&debuggerURL) != nil {\n\t\t\tutils.PrintError(\"Could not reload Spotify\")\n\t\t\tutils.PrintInfo(`Close Spotify and run watch command again`)\n\t\t\tos.Exit(1)\n\t\t} else {\n\t\t\tutils.PrintSuccess(\"Reloaded Spotify\")\n\t\t}\n\t}\n}\n\nfunc enqueueWatchJob(job func()) {\n\twatchQueueOnce.Do(func() {\n\t\twatchQueue = make(chan func(), 64)\n\t\tgo func() {\n\t\t\tfor fn := range watchQueue {\n\t\t\t\tfn()\n\t\t\t}\n\t\t}()\n\t})\n\twatchQueue <- job\n}\n"
  },
  {
    "path": "src/preprocess/preprocess.go",
    "content": "package preprocess\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/pterm/pterm\"\n\t\"github.com/spicetify/cli/src/utils\"\n)\n\n// Flag enables/disables preprocesses to be applied\ntype Flag struct {\n\t// DisableSentry prevents Sentry to send console log/error/warning to Spotify developers.\n\tDisableSentry bool\n\t// DisableLogging stops various elements to log user interaction.\n\tDisableLogging bool\n\t// RemoveRTL removes all Right-To-Left CSS rules to simplify CSS files.\n\tRemoveRTL bool\n\t// ExposeAPIs leaks Spotify's API, functions, objects to Spicetify global object.\n\tExposeAPIs bool\n\tSpotifyVer string\n}\n\ntype Patch struct {\n\tName        string\n\tRegex       string\n\tReplacement func(submatches ...string) string\n\tOnce        bool\n}\n\nfunc applyPatches(input string, patches []Patch) string {\n\tfor _, patch := range patches {\n\t\tif patch.Once {\n\t\t\tutils.ReplaceOnce(&input, patch.Regex, patch.Replacement)\n\t\t} else {\n\t\t\tutils.Replace(&input, patch.Regex, patch.Replacement)\n\t\t}\n\t}\n\treturn input\n}\n\nfunc readRemoteCssMap(tag string, cssTranslationMap *map[string]string) error {\n\tvar cssMapURL string = \"https://raw.githubusercontent.com/spicetify/cli/\" + tag + \"/css-map.json\"\n\tcssMapResp, err := http.Get(cssMapURL)\n\tif err != nil {\n\t\treturn err\n\t} else {\n\t\terr := json.NewDecoder(cssMapResp.Body).Decode(cssTranslationMap)\n\t\tif err != nil {\n\t\t\tutils.PrintWarning(\"Remote CSS map JSON malformed.\")\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc readLocalCssMap(cssTranslationMap *map[string]string) error {\n\tcssMapLocalPath := path.Join(utils.GetExecutableDir(), \"css-map.json\")\n\tcssMapContent, err := os.ReadFile(cssMapLocalPath)\n\tif err != nil {\n\t\tutils.PrintWarning(\"Cannot read local CSS map.\")\n\t\treturn err\n\t} else {\n\t\terr = json.Unmarshal(cssMapContent, cssTranslationMap)\n\t\tif err != nil {\n\t\t\tutils.PrintWarning(\"Local CSS map JSON malformed.\")\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc Start(version string, spotifyBasePath string, extractedAppsPath string, flags Flag) {\n\tappPath := filepath.Join(extractedAppsPath, \"xpui\")\n\tvar cssTranslationMap = make(map[string]string)\n\n\tif version != \"Dev\" {\n\t\tfetchSpinner, _ := utils.Spinner.Start(\"Fetching remote CSS map\")\n\t\ttag, err := FetchLatestTagMatchingOrMain(version)\n\t\tif err != nil {\n\t\t\tfetchSpinner.Warning(\"Failed to fetch remote CSS map\")\n\t\t\tutils.PrintWarning(err.Error())\n\t\t\ttag = version\n\t\t}\n\t\tif readRemoteCssMap(tag, &cssTranslationMap) != nil {\n\t\t\tfetchSpinner.Warning(\"Failed to fetch remote CSS map\")\n\t\t\tutils.PrintInfo(\"Using local CSS map instead\")\n\t\t\treadLocalCssMap(&cssTranslationMap)\n\t\t} else {\n\t\t\tfetchSpinner.Success(\"Fetched remote CSS map\")\n\t\t}\n\t} else {\n\t\tutils.PrintInfo(\"Using local CSS map; in development environment\")\n\t\treadLocalCssMap(&cssTranslationMap)\n\t}\n\n\tverParts := strings.Split(flags.SpotifyVer, \".\")\n\tspotifyMajor, spotifyMinor, spotifyPatch := 0, 0, 0\n\tif len(verParts) > 0 {\n\t\tspotifyMajor, _ = strconv.Atoi(verParts[0])\n\t}\n\tif len(verParts) > 1 {\n\t\tspotifyMinor, _ = strconv.Atoi(verParts[1])\n\t}\n\tif len(verParts) > 2 {\n\t\tspotifyPatch, _ = strconv.Atoi(verParts[2])\n\t}\n\n\tvar spotifyBinaryPath string\n\tswitch runtime.GOOS {\n\tcase \"windows\":\n\t\tdllPath := filepath.Join(spotifyBasePath, \"spotify.dll\")\n\t\texePath := filepath.Join(spotifyBasePath, \"spotify.exe\")\n\n\t\tif _, err := os.Stat(dllPath); err == nil {\n\t\t\tspotifyBinaryPath = dllPath\n\t\t} else if _, err := os.Stat(exePath); err == nil {\n\t\t\tspotifyBinaryPath = exePath\n\t\t} else {\n\t\t\tutils.PrintError(\"Could not find spotify.dll or spotify.exe in Spotify installation directory\")\n\t\t\tutils.Fatal(errors.New(\"aborting the patching process due to missing Spotify binaries\"))\n\t\t}\n\tcase \"darwin\":\n\t\tspotifyBinaryPath = filepath.Join(spotifyBasePath, \"..\", \"MacOS\", \"Spotify\")\n\t}\n\n\tif spotifyBinaryPath != \"\" {\n\t\tif err := validateReleaseBuild(spotifyBinaryPath); err != nil {\n\t\t\tutils.PrintError(err.Error())\n\t\t\tutils.Fatal(errors.New(\"aborting the patching process due to unsupported build\"))\n\t\t}\n\t}\n\n\tframeworkResourcesPath := \"\"\n\tswitch runtime.GOOS {\n\tcase \"darwin\":\n\t\tframeworkResourcesPath = filepath.Join(spotifyBasePath, \"..\", \"Frameworks\", \"Chromium Embedded Framework.framework\", \"Resources\")\n\tcase \"windows\", \"linux\":\n\t\tframeworkResourcesPath = spotifyBasePath\n\tdefault:\n\t\tutils.PrintError(\"Unsupported OS for V8 snapshot finding: \" + runtime.GOOS)\n\t}\n\n\tif frameworkResourcesPath != \"\" {\n\t\tfiles, err := os.ReadDir(frameworkResourcesPath)\n\t\tif err != nil {\n\t\t\tutils.PrintWarning(fmt.Sprintf(\"Could not read directory %s for V8 snapshots: %v\", frameworkResourcesPath, err))\n\t\t} else {\n\t\t\tfor _, file := range files {\n\t\t\t\tif !file.IsDir() && strings.HasPrefix(file.Name(), \"v8_context_snapshot\") && strings.HasSuffix(file.Name(), \".bin\") {\n\t\t\t\t\tbinFilePath := filepath.Join(frameworkResourcesPath, file.Name())\n\n\t\t\t\t\tstartMarker := []byte(\"var __webpack_modules__={\")\n\t\t\t\t\tendMarker := []byte(\"xpui-modules.js.map\")\n\n\t\t\t\t\tembeddedString, _, _, err := utils.ReadStringFromUTF16Binary(binFilePath, startMarker, endMarker)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tutils.PrintWarning(fmt.Sprintf(\"Could not process %s: %v\", binFilePath, err))\n\t\t\t\t\t\tutils.PrintInfo(\"If above warning says 'could not find start marker', you can safely ignore that error if you're on Spotify 1.2.63 or lower and you're not on macOS Intel.\")\n\t\t\t\t\t\tutils.PrintInfo(\"However, if you're on 1.2.64 or higher, please report this issue\")\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\terr = utils.CreateFile(filepath.Join(appPath, \"xpui-modules.js\"), embeddedString)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tutils.PrintWarning(fmt.Sprintf(\"Could not create xpui-modules.js: %v\", err))\n\t\t\t\t\t\tbreak\n\t\t\t\t\t} else {\n\t\t\t\t\t\tutils.PrintSuccess(\"Finished extracting V8 snapshot blob to local file\")\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tvar filesToPatch []string\n\tfilepath.Walk(appPath, func(path string, info os.FileInfo, err error) error {\n\t\tif err != nil || info.IsDir() {\n\t\t\treturn nil\n\t\t}\n\t\text := filepath.Ext(info.Name())\n\t\tif ext == \".js\" || ext == \".css\" || ext == \".html\" {\n\t\t\tfilesToPatch = append(filesToPatch, path)\n\t\t}\n\t\treturn nil\n\t})\n\n\ttotalFiles := len(filesToPatch)\n\n\tbar, _ := pterm.DefaultProgressbar.\n\t\tWithTotal(totalFiles).\n\t\tWithTitle(\"Patching files\").\n\t\tWithTitleStyle(pterm.NewStyle(pterm.Bold)).\n\t\tWithShowCount(true).\n\t\tStart()\n\tfor _, path := range filesToPatch {\n\t\tinfo, err := os.Stat(path)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfileName := info.Name()\n\t\textension := filepath.Ext(fileName)\n\n\t\tswitch extension {\n\t\tcase \".js\":\n\t\t\tutils.ModifyFile(path, func(content string) string {\n\t\t\t\tif flags.DisableSentry && (fileName == \"xpui.js\" || fileName == \"xpui-snapshot.js\") {\n\t\t\t\t\tcontent = disableSentry(content)\n\t\t\t\t}\n\n\t\t\t\tif flags.DisableLogging {\n\t\t\t\t\tcontent = disableLogging(content)\n\t\t\t\t}\n\n\t\t\t\tif flags.ExposeAPIs {\n\t\t\t\t\tswitch fileName {\n\t\t\t\t\tcase \"xpui-modules.js\", \"xpui-snapshot.js\":\n\t\t\t\t\t\tcontent = exposeAPIs_main(content)\n\t\t\t\t\t\tcontent = exposeAPIs_vendor(content)\n\t\t\t\t\tcase \"xpui.js\":\n\t\t\t\t\t\tcontent = exposeAPIs_main(content)\n\t\t\t\t\t\tif spotifyMajor >= 1 && spotifyMinor >= 2 && spotifyPatch >= 57 {\n\t\t\t\t\t\t\tcontent = exposeAPIs_vendor(content)\n\t\t\t\t\t\t}\n\t\t\t\t\tcase \"vendor~xpui.js\":\n\t\t\t\t\t\tcontent = exposeAPIs_vendor(content)\n\t\t\t\t\t}\n\n\t\t\t\t\tif spotifyMajor >= 1 && spotifyMinor >= 2 && (spotifyPatch >= 28 && spotifyPatch <= 57) {\n\t\t\t\t\t\tutils.ReplaceOnce(&content, `(typeName\\])`, func(submatches ...string) string {\n\t\t\t\t\t\t\treturn fmt.Sprintf(`%s || []`, submatches[1])\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\n\t\t\t\t\t// to avoid syntaxerror on Spotify 1.2.78 and above\n\t\t\t\t\tif spotifyMajor >= 1 && spotifyMinor >= 2 && spotifyPatch < 78 {\n\t\t\t\t\t\tutils.ReplaceOnce(&content, `\\(\\({[^}]*,\\s*imageSrc`, func(submatches ...string) string {\n\t\t\t\t\t\t\treturn fmt.Sprintf(\"Spicetify.Snackbar.enqueueImageSnackbar=%s\", submatches[0])\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\n\t\t\t\t\tcontent = additionalPatches(content)\n\t\t\t\t}\n\n\t\t\t\tif fileName == \"dwp-top-bar.js\" || fileName == \"dwp-now-playing-bar.js\" || fileName == \"dwp-home-chips-row.js\" {\n\t\t\t\t\tutils.ReplaceOnce(&content, `e\\.state\\.cinemaState`, func(submatches ...string) string {\n\t\t\t\t\t\treturn \"e.state?.cinemaState\"\n\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\t\tfor k, v := range cssTranslationMap {\n\t\t\t\t\tutils.Replace(&content, k, func(submatches ...string) string {\n\t\t\t\t\t\treturn v\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tcontent = colorVariableReplaceForJS(content)\n\n\t\t\t\treturn content\n\t\t\t})\n\t\tcase \".css\":\n\t\t\tutils.ModifyFile(path, func(content string) string {\n\t\t\t\tfor k, v := range cssTranslationMap {\n\t\t\t\t\tutils.Replace(&content, k, func(submatches ...string) string {\n\t\t\t\t\t\treturn v\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tif flags.RemoveRTL {\n\t\t\t\t\tcontent = removeRTL(content)\n\t\t\t\t}\n\t\t\t\tif fileName == \"xpui.css\" || fileName == \"xpui-snapshot.css\" {\n\t\t\t\t\tcontent = content + `\n\t\t\t\t\t.main-gridContainer-fixedWidth{grid-template-columns: repeat(auto-fill, var(--column-width));width: calc((var(--column-count) - 1) * var(--grid-gap)) + var(--column-count) * var(--column-width));}.main-cardImage-imageWrapper{background-color: var(--card-color, #333);border-radius: 6px;-webkit-box-shadow: 0 8px 24px rgba(0, 0, 0, .5);box-shadow: 0 8px 24px rgba(0, 0, 0, .5);padding-bottom: 100%;position: relative;width:100%;}.main-cardImage-image,.main-card-imagePlaceholder{height: 100%;left: 0;position: absolute;top: 0;width: 100%};.main-content-view{height:100%;}\n\t\t\t\t\t`\n\t\t\t\t}\n\t\t\t\treturn content\n\t\t\t})\n\n\t\tcase \".html\":\n\t\t\tutils.ModifyFile(path, func(content string) string {\n\t\t\t\tvar tags string\n\t\t\t\ttags += \"<link rel='stylesheet' class='userCSS' href='colors.css'>\\n\"\n\t\t\t\ttags += \"<link rel='stylesheet' class='userCSS' href='user.css'>\\n\"\n\n\t\t\t\tif flags.ExposeAPIs {\n\t\t\t\t\ttags += \"<script src='helper/spicetifyWrapper.js'></script>\\n\"\n\t\t\t\t\ttags += \"<!-- spicetify helpers -->\\n\"\n\t\t\t\t}\n\n\t\t\t\tutils.Replace(&content, `<body(\\sclass=\"[^\"]*\")?>`, func(submatches ...string) string {\n\t\t\t\t\treturn fmt.Sprintf(\"%s\\n%s\", submatches[0], tags)\n\t\t\t\t})\n\n\t\t\t\treturn content\n\t\t\t})\n\t\t}\n\n\t\tbar.Increment()\n\t}\n}\n\n// StartCSS modifies all CSS files in extractedAppsPath to change\n// all colors value with CSS variables.\nfunc StartCSS(extractedAppsPath string) {\n\tappPath := filepath.Join(extractedAppsPath, \"xpui\")\n\tfilepath.Walk(appPath, func(path string, info os.FileInfo, err error) error {\n\t\t// temp so text won't be black ._.\n\t\tif strings.HasPrefix(info.Name(), \"pip-mini-player\") && strings.HasSuffix(info.Name(), \".css\") {\n\t\t\treturn nil\n\t\t}\n\n\t\tif filepath.Ext(info.Name()) == \".css\" {\n\t\t\tutils.ModifyFile(path, func(content string) string {\n\t\t\t\treturn colorVariableReplace(content)\n\t\t\t})\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc colorVariableReplace(content string) string {\n\tcolorPatches := []Patch{\n\t\t{\n\t\t\tName:  \"CSS: --spice-player\",\n\t\t\tRegex: `#(181818|212121)\\b`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"var(--spice-player)\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS: --spice-card\",\n\t\t\tRegex: `#282828\\b`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"var(--spice-card)\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS: --spice-main-elevated\",\n\t\t\tRegex: `#(242424|1f1f1f)\\b`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"var(--spice-main-elevated)\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS: --spice-main\",\n\t\t\tRegex: `#121212\\b`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"var(--spice-main)\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS: --spice-card-elevated\",\n\t\t\tRegex: `#(242424|1f1f1f)\\b`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"var(--spice-card-elevated)\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS: --spice-highlight\",\n\t\t\tRegex: `#1a1a1a\\b`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"var(--spice-highlight)\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS: --spice-highlight-elevated\",\n\t\t\tRegex: `#2a2a2a\\b`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"var(--spice-highlight-elevated)\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS: --spice-sidebar\",\n\t\t\tRegex: `#(000|000000)\\b`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"var(--spice-sidebar)\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS: --spice-text\",\n\t\t\tRegex: `(white;|#fff|#ffffff|#f8f8f8)\\b`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"var(--spice-text)\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS: --spice-subtext\",\n\t\t\tRegex: `#(b3b3b3|a7a7a7)\\b`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"var(--spice-subtext)\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS: --spice-button\",\n\t\t\tRegex: `#(1db954|1877f2)\\b`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"var(--spice-button)\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS: --spice-button-active\",\n\t\t\tRegex: `#(1ed760|1fdf64|169c46)\\b`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"var(--spice-button-active)\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS: --spice-button-disabled\",\n\t\t\tRegex: `#535353\\b`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"var(--spice-button-disabled)\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS: --spice-tab-active\",\n\t\t\tRegex: `#(333|333333)\\b`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"var(--spice-tab-active)\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS: --spice-misc\",\n\t\t\tRegex: `#7f7f7f\\b`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"var(--spice-misc)\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS: --spice-notification\",\n\t\t\tRegex: `#(4687d6|2e77d0)\\b`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"var(--spice-notification)\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS: --spice-notification-error\",\n\t\t\tRegex: `#(e22134|cd1a2b)\\b`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"var(--spice-notification-error)\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS (rgba): --spice-main\",\n\t\t\tRegex: `rgba\\(18,18,18,([\\d\\.]+)\\)`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"rgba(var(--spice-main),%s)\", submatches[1])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS (rgba): --spice-card\",\n\t\t\tRegex: `rgba\\(40,40,40,([\\d\\.]+)\\)`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"rgba(var(--spice-card),%s)\", submatches[1])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS (rgba): --spice-rgb-shadow\",\n\t\t\tRegex: `rgba\\(0,0,0,([\\d\\.]+)\\)`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"rgba(var(--spice-rgb-shadow),%s)\", submatches[1])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS (hsla): --spice-rgb-text\",\n\t\t\tRegex: `hsla\\(0,0%,100%,\\.9\\)`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"rgba(var(--spice-rgb-text),.9)\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS (hsla): --spice-rgb-selected-row\",\n\t\t\tRegex: `hsla\\(0,0%,100%,([\\d\\.]+)\\)`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"rgba(var(--spice-rgb-selected-row),%s)\", submatches[1])\n\t\t\t},\n\t\t},\n\t}\n\n\treturn applyPatches(content, colorPatches)\n}\n\nfunc colorVariableReplaceForJS(content string) string {\n\tcolorVariablePatches := []Patch{\n\t\t{\n\t\t\tName:  \"CSS (JS): --spice-button\",\n\t\t\tRegex: `\"#1db954\"`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn ` getComputedStyle(document.body).getPropertyValue(\"--spice-button\").trim()`\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS (JS): --spice-subtext\",\n\t\t\tRegex: `\"#b3b3b3\"`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn ` getComputedStyle(document.body).getPropertyValue(\"--spice-subtext\").trim()`\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS (JS): --spice-text\",\n\t\t\tRegex: `\"#ffffff\"`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn ` getComputedStyle(document.body).getPropertyValue(\"--spice-text\").trim()`\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"CSS (JS): --spice-text white\",\n\t\t\tRegex: `color:\"white\"`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn `color:\"var(--spice-text)\"`\n\t\t\t},\n\t\t},\n\t}\n\n\treturn applyPatches(content, colorVariablePatches)\n}\n\nfunc disableSentry(input string) string {\n\t//utils.Replace(&input, `\\(([^,]+),([^,]+),\\{sampleRate:([^,]+),tracesSampleRate:([^,]+)(,.*?)?\\}`, func(submatches ...string) string {\n\t//\treturn fmt.Sprintf(\",%s\", submatches[0])\n\t//})\n\t// Spotify enables sentry only for versions that are newer than 30 days old.\n\tutils.Replace(&input, \"/864e5<30\", func(submatches ...string) string {\n\t\treturn \"<0\"\n\t})\n\treturn input\n}\n\nfunc disableLogging(input string) string {\n\tloggingPatches := []Patch{\n\t\t{\n\t\t\tName:  \"Remove sp://logging/v3/*\",\n\t\t\tRegex: `sp://logging/v3/\\w+`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Remove /v3/events endpoints\",\n\t\t\tRegex: `[^\"\\/]+\\/[^\"\\/]+\\/(public\\/)?v3\\/events`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable registerEventListeners\",\n\t\t\tRegex: `key:\"registerEventListeners\",value:function\\(\\)\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn;\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable logInteraction\",\n\t\t\tRegex: `key:\"logInteraction\",value:function\\([\\w,]+\\)\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn {interactionId:null,pageInstanceId:null};\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable logNonAuthInteraction\",\n\t\t\tRegex: `key:\"logNonAuthInteraction\",value:function\\([\\w,]+\\)\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn {interactionId:null,pageInstanceId:null};\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable logImpression\",\n\t\t\tRegex: `key:\"logImpression\",value:function\\([\\w,]+\\)\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn;\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable logNonAuthImpression\",\n\t\t\tRegex: `key:\"logNonAuthImpression\",value:function\\([\\w,]+\\)\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn;\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable logNavigation\",\n\t\t\tRegex: `key:\"logNavigation\",value:function\\([\\w,]+\\)\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn;\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable handleBackgroundStates\",\n\t\t\tRegex: `key:\"handleBackgroundStates\",value:function\\(\\)\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn;\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable createLoggingParams\",\n\t\t\tRegex: `key:\"createLoggingParams\",value:function\\([\\w,]+\\)\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn;\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable initSendingEvents\",\n\t\t\tRegex: `key:\"initSendingEvents\",value:function\\(\\)\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn;\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable flush\",\n\t\t\tRegex: `key:\"flush\",value:function\\(\\)\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn;\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable send\",\n\t\t\tRegex: `(\\{key:\"send\",value:function\\([\\w,]+\\))\\{[\\d\\w\\s,{}()[\\]\\.,!\\?=>&|;:_\"\"]+?\\}(\\},\\{key:\"hasContext\")`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%s{return;}%s\", submatches[1], submatches[2])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable lastFlush\",\n\t\t\tRegex: `key:\"lastFlush\",value:function\\(\\)\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn Promise.resolve({fired:true});\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable addItemInEventsStorage\",\n\t\t\tRegex: `key:\"addItemInEventsStorage\",value:function\\([^)]*\\)\\s*\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn;\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable createLoggingParams (new)\",\n\t\t\tRegex: `key:\"createLoggingParams\",value:function\\([^)]*\\)\\s*\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn {interactionIds:null,pageInstanceIds:null};\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable addEventsToESSData\",\n\t\t\tRegex: `key:\"addEventsToESSData\",value:function\\([^)]*\\)\\s*\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn;\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable registerEventListeners (new)\",\n\t\t\tRegex: `registerEventListeners\\([^)]*\\)\\s*\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn;\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable logInteraction (new)\",\n\t\t\tRegex: `logInteraction\\([^)]*\\)\\s*\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn {interactionId:null,pageInstanceId:null};\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable logImpression (new)\",\n\t\t\tRegex: `logImpression\\([^)]*\\)\\s*\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn;\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable logNavigation (new)\",\n\t\t\tRegex: `logNavigation\\([^)]*\\)\\s*\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn;\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable handleBackgroundStates (new)\",\n\t\t\tRegex: `handleBackgroundStates\\([^)]*\\)\\s*\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn;\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable initSendingEvents (new)\",\n\t\t\tRegex: `initSendingEvents\\([^)]*\\)\\s*\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn;\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable sendEvents\",\n\t\t\tRegex: `sendEvents\\([^)]*\\)\\s*\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn;\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable storeEvent\",\n\t\t\tRegex: `storeEvent\\([^)]*\\)\\s*\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn;\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable lastFlush (new)\",\n\t\t\tRegex: `lastFlush\\([^)]*\\)\\s*\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn Promise.resolve({fired:true});\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable addItemInEventsStorage (new)\",\n\t\t\tRegex: `addItemInEventsStorage\\([^)]*\\)\\s*\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn;\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable createLoggingParams (new)\",\n\t\t\tRegex: `createLoggingParams\\([^)]*\\)\\s*\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn {interactionIds:null,pageInstanceIds:null};\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Disable addEventsToESSData (new)\",\n\t\t\tRegex: `addEventsToESSData\\([^)]*\\)\\s*\\{`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sreturn;\", submatches[0])\n\t\t\t},\n\t\t},\n\t}\n\treturn applyPatches(input, loggingPatches)\n}\n\nfunc removeRTL(input string) string {\n\trtlPatches := []Patch{\n\t\t{\n\t\t\tName:  \"Remove }[dir=ltr]\",\n\t\t\tRegex: `}\\[dir=ltr\\]\\s?`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"} \"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Remove html[dir=ltr]\",\n\t\t\tRegex: `html\\[dir=ltr\\]`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"html\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Remove ', [dir=rtl]' selectors\",\n\t\t\tRegex: `,\\s?\\[dir=rtl\\].+?(\\{.+?\\})`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn submatches[1]\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Remove [something][dir=rtl] blocks\",\n\t\t\tRegex: `[\\w\\-\\.]+\\[dir=rtl\\].+?\\{.+?\\}`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Remove }[lang=ar] blocks\",\n\t\t\tRegex: `\\}\\[lang=ar\\].+?\\{.+?\\}`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"}\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Remove }[dir=rtl] blocks\",\n\t\t\tRegex: `\\}\\[dir=rtl\\].+?\\{.+?\\}`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"}\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Remove }html[dir=rtl] blocks\",\n\t\t\tRegex: `\\}html\\[dir=rtl\\].+?\\{.+?\\}`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"}\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Remove }html[lang=ar] blocks\",\n\t\t\tRegex: `\\}html\\[lang=ar\\].+?\\{.+?\\}`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"}\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Remove [lang=ar] blocks\",\n\t\t\tRegex: `\\[lang=ar\\].+?\\{.+?\\}`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Remove html[dir=rtl] blocks\",\n\t\t\tRegex: `html\\[dir=rtl\\].+?\\{.+?\\}`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Remove html[lang=ar] blocks\",\n\t\t\tRegex: `html\\[lang=ar\\].+?\\{.+?\\}`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Remove [dir=rtl] blocks\",\n\t\t\tRegex: `\\[dir=rtl\\][^)]+?\\{.+?\\}`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"\"\n\t\t\t},\n\t\t},\n\t}\n\n\treturn applyPatches(input, rtlPatches)\n}\n\nfunc additionalPatches(input string) string {\n\tgraphQLPatches := []Patch{\n\t\t{\n\t\t\tName:  \"GraphQL definitions (<=1.2.30)\",\n\t\t\tRegex: `((?:\\w+ ?)?[\\w$]+=)(\\{kind:\"Document\",definitions:\\[\\{(?:\\w+:[\\w\"]+,)+name:\\{(?:\\w+:[\\w\"]+,?)+value:(\"\\w+\"))`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sSpicetify.GraphQL.Definitions[%s]=%s\", submatches[1], submatches[3], submatches[2])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"GraphQL definitions (>=1.2.31)\",\n\t\t\tRegex: `(=new [\\w_\\$][\\w_\\$\\d]*\\.[\\w_\\$][\\w_\\$\\d]*\\(\"(\\w+)\",\"(query|mutation)\",\"[\\w\\d]{64}\",null\\))`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(`=Spicetify.GraphQL.Definitions[\"%s\"]%s`, submatches[2], submatches[1])\n\t\t\t},\n\t\t},\n\t}\n\n\treturn applyPatches(input, graphQLPatches)\n}\n\nfunc exposeAPIs_main(input string) string {\n\tinputContextMenu := utils.FindFirstMatch(input, `.*(?:value:\"contextmenu\"|\"[^\"]*\":\"context-menu\")`)\n\tif len(inputContextMenu) > 0 {\n\t\tcroppedInput := inputContextMenu[0]\n\t\treact := utils.FindLastMatch(croppedInput, `([a-zA-Z_\\$][\\w\\$]*)\\.useRef`)[1]\n\t\tcandicates := utils.FindLastMatch(croppedInput, `\\(\\{[^}]*menu:([a-zA-Z_\\$][\\w\\$]*),[^}]*trigger:([a-zA-Z_\\$][\\w\\$]*),[^}]*triggerRef:([a-zA-Z_\\$][\\w\\$]*)`)\n\t\toldCandicates := utils.FindLastMatch(croppedInput, `([a-zA-Z_\\$][\\w\\$]*)=[\\w_$]+\\.menu[^}]*,([a-zA-Z_\\$][\\w\\$]*)=[\\w_$]+\\.trigger[^}]*,([a-zA-Z_\\$][\\w\\$]*)=[\\w_$]+\\.triggerRef`)\n\t\tvar menu, trigger, target string\n\t\tif len(oldCandicates) != 0 {\n\t\t\tmenu = oldCandicates[1]\n\t\t\ttrigger = oldCandicates[2]\n\t\t\ttarget = oldCandicates[3]\n\t\t} else if len(candicates) != 0 {\n\t\t\tmenu = candicates[1]\n\t\t\ttrigger = candicates[2]\n\t\t\ttarget = candicates[3]\n\t\t} else {\n\t\t\tmenu = \"e.menu\"\n\t\t\ttrigger = \"e.trigger\"\n\t\t\ttarget = \"e.triggerRef\"\n\t\t}\n\n\t\tutils.Replace(&input, `\\(0,([\\w_$]+)\\.jsx\\)\\((?:[\\w_$]+\\.[\\w_$]+,\\{value:\"contextmenu\"[^}]+\\}\\)\\}\\)|\"[\\w-]+\",\\{[^}]+:\"context-menu\"[^}]+\\}\\))`, func(submatches ...string) string {\n\t\t\treturn fmt.Sprintf(\"(0,%s.jsx)((Spicetify.ContextMenuV2._context||(Spicetify.ContextMenuV2._context=%s.createContext(null))).Provider,{value:{props:%s?.props,trigger:%s,target:%s},children:%s})\", submatches[1], react, menu, trigger, target, submatches[0])\n\t\t})\n\t}\n\n\txpuiPatches := []Patch{\n\t\t{\n\t\t\tName:  \"showNotification\",\n\t\t\tRegex: `(?:\\w+ |,)([\\w$]+)=(\\([\\w$]+=[\\w$]+\\.dispatch)`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(`;globalThis.Spicetify.showNotification=(message,isError=false,msTimeout)=>%s({message,feedbackType:isError?\"ERROR\":\"NOTICE\",msTimeout});const %s=%s`, submatches[1], submatches[1], submatches[2])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Remove list of exclusive shows\",\n\t\t\tRegex: `\\[\"spotify:show.+?\\]`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"[]\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Remove Star Wars easter eggs\",\n\t\t\tRegex: `\\w+\\(\\)\\.createElement\\(\\w+,\\{onChange:this\\.handleSaberStateChange\\}\\),`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Remove data-testid\",\n\t\t\tRegex: `\"data-testid\":`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn `\"\":`\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Expose PlatformAPI\",\n\t\t\tRegex: `((?:setTitlebarHeight|registerFactory)[\\w(){}<>:.,&$!=;\"\"?!#%/\\- ]+)(\\{version:[a-zA-Z_\\$][\\w\\$]*,)`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sSpicetify._platform=%s\", submatches[1], submatches[2])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Redux store\",\n\t\t\tRegex: `(,[\\w$]+=)(([$\\w,.:=;(){}]+\\(\\{session:[\\w$]+,features:[\\w$]+,seoExperiment:[\\w$]+\\}))`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sSpicetify.Platform.ReduxStore=%s\", submatches[1], submatches[2])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"React Component: Platform Provider\",\n\t\t\tRegex: `(,[$\\w]+=)((function\\([\\w$]{1}\\)\\{var [\\w$]+=[\\w$]+\\.platform,[\\w$]+=[\\w$]+\\.children,)|(\\(\\{platform:[\\w$]+,children:[\\w$]+\\}\\)=>\\{))`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sSpicetify.ReactComponent.PlatformProvider=%s\", submatches[1], submatches[2])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Prevent breaking popupLyrics\",\n\t\t\tRegex: `document.pictureInPictureElement&&\\(\\w+.current=[!\\w]+,document\\.exitPictureInPicture\\(\\)\\),\\w+\\.current=null`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn \"\"\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Spotify Custom Snackbar Interfaces (<=1.2.37)\",\n\t\t\tRegex: `\\b\\w\\s*\\(\\)\\s*[^;,]*enqueueCustomSnackbar:\\s*(\\w)\\s*[^;]*;`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sSpicetify.Snackbar.enqueueCustomSnackbar=%s;\", submatches[0], submatches[1])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Spotify Custom Snackbar Interfaces (>=1.2.38)\",\n\t\t\tRegex: `(=)[^=]*\\(\\)\\.enqueueCustomSnackbar;`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"=Spicetify.Snackbar.enqueueCustomSnackbar%s;\", submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Spotify Image Snackbar Interface\",\n\t\t\tRegex: `(=)(\\(\\({[^}]*,\\s*imageSrc)`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sSpicetify.Snackbar.enqueueImageSnackbar=%s\", submatches[1], submatches[2])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"React Component: Navigation for navLinks\",\n\t\t\tRegex: `(;const [\\w\\d]+=)((?:\\(0,[\\w\\d]+\\.memo\\))[\\(\\d,\\w\\.\\){:}=]+\\=[\\d\\w]+\\.[\\d\\w]+\\.getLocaleForURLPath\\(\\))`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%sSpicetify.ReactComponent.Navigation=%s\", submatches[1], submatches[2])\n\t\t\t},\n\t\t\tOnce: true,\n\t\t},\n\t\t{\n\t\t\tName:  \"Context Menu V2\",\n\t\t\tRegex: `(\"Menu\".+?children:)([\\w$][\\w$\\d]*)`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%s[Spicetify.ContextMenuV2.renderItems(),%s].flat()\", submatches[1], submatches[2])\n\t\t\t},\n\t\t},\n\t}\n\n\treturn applyPatches(input, xpuiPatches)\n}\n\nfunc exposeAPIs_vendor(input string) string {\n\t// URI\n\tutils.Replace(\n\t\t&input,\n\t\t`,(\\w+)\\.prototype\\.toAppType`,\n\t\tfunc(submatches ...string) string {\n\t\t\treturn fmt.Sprintf(`,(globalThis.Spicetify.URI=%s)%s`, submatches[1], submatches[0])\n\t\t})\n\tvendorPatches := []Patch{\n\t\t{\n\t\t\tName:  \"Spicetify.URI\",\n\t\t\tRegex: `,(\\w+)\\.prototype\\.toAppType`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(`,(globalThis.Spicetify.URI=%s)%s`, submatches[1], submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Map styled-components classes\",\n\t\t\tRegex: `(\\w+ [\\w$_]+)=[\\w$_]+\\([\\w$_]+>>>0\\)`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%s=Spicetify._getStyledClassName(arguments,this)\", submatches[1])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Tippy.js\",\n\t\t\tRegex: `([\\w\\$_]+)\\.setDefaultProps=`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"Spicetify.Tippy=%s;%s\", submatches[1], submatches[0])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:  \"Flipper components\",\n\t\t\tRegex: `([\\w$]+)=((?:function|\\()([\\w$.,{}()= ]+(?:springConfig|overshootClamping)){2})`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"%s=Spicetify.ReactFlipToolkit.spring=%s\", submatches[1], submatches[2])\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// https://github.com/iamhosseindhv/notistack\n\t\t\tName:  \"Snackbar\",\n\t\t\tRegex: `\\w+\\s*=\\s*\\w\\.call\\(this,[^)]+\\)\\s*\\|\\|\\s*this\\)\\.enqueueSnackbar`,\n\t\t\tReplacement: func(submatches ...string) string {\n\t\t\t\treturn fmt.Sprintf(\"Spicetify.Snackbar=%s\", submatches[0])\n\t\t\t},\n\t\t},\n\t}\n\n\t// URI after 1.2.4\n\tif !strings.Contains(input, \"Spicetify.URI\") {\n\t\tURIObj := regexp.MustCompile(`(?:class ([\\w$_]+)\\{constructor|([\\w$_]+)=function\\(\\)\\{function ?[\\w$_]+)\\([\\w$.,={}]+\\)\\{[\\w !?:=.,>&(){}[\\];]*this\\.hasBase62Id`).FindStringSubmatch(input)\n\n\t\tif len(URIObj) != 0 {\n\t\t\tURI := utils.SeekToCloseParen(\n\t\t\t\tinput,\n\t\t\t\t`\\{(?:constructor|function ?[\\w$_]+)\\([\\w$.,={}]+\\)\\{[\\w !?:=.,>&(){}[\\];]*this\\.hasBase62Id`,\n\t\t\t\t'{', '}')\n\n\t\t\tif URIObj[1] == \"\" {\n\t\t\t\tURIObj[1] = URIObj[2]\n\t\t\t\t// Class is a self-invoking function\n\t\t\t\tURI = fmt.Sprintf(\"%s()\", URI)\n\t\t\t}\n\n\t\t\tinput = strings.Replace(\n\t\t\t\tinput,\n\t\t\t\tURI,\n\t\t\t\tfmt.Sprintf(\"%s;Spicetify.URI=%s;\", URI, URIObj[1]),\n\t\t\t\t1)\n\t\t}\n\t}\n\n\treturn applyPatches(input, vendorPatches)\n}\n\nfunc validateReleaseBuild(spotifyBinaryPath string) error {\n\tfileContent, err := os.ReadFile(spotifyBinaryPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not read %s: %w\", filepath.Base(spotifyBinaryPath), err)\n\t}\n\n\tbuildRegex := regexp.MustCompile(`(Master|Release|PR|Local) Build.+(?:cef_)?(\\d+\\.\\d+\\.\\d+\\+g[0-9a-f]+\\+chromium-\\d+\\.\\d+\\.\\d+\\.\\d+)`)\n\tmatches := buildRegex.FindSubmatch(fileContent)\n\n\tif len(matches) == 0 {\n\t\tutils.PrintWarning(fmt.Sprintf(\"Could not detect Spotify build type in %s, skipping validation\", filepath.Base(spotifyBinaryPath)))\n\t\treturn nil\n\t}\n\n\tbuildType := string(matches[1])\n\tif buildType != \"Release\" {\n\t\treturn fmt.Errorf(\"detected %s Spotify build! spicetify works only on Release builds. Please install latest Release version of Spotify\", buildType)\n\t}\n\n\tutils.PrintSuccess(fmt.Sprintf(\"Spotify's build type is %s. Continuing...\", string(matches[1])))\n\treturn nil\n}\n\ntype githubRelease = utils.GithubRelease\n\nfunc splitVersion(version string) ([3]int, error) {\n\tvstring := version\n\tif vstring[0:1] == \"v\" {\n\t\tvstring = version[1:]\n\t}\n\tvSplit := strings.Split(vstring, \".\")\n\tvar vInts [3]int\n\tif len(vSplit) != 3 {\n\t\treturn [3]int{}, errors.New(\"invalid version string\")\n\t}\n\tfor i := 0; i < 3; i++ {\n\t\tconv, err := strconv.Atoi(vSplit[i])\n\t\tif err != nil {\n\t\t\treturn [3]int{}, err\n\t\t}\n\t\tvInts[i] = conv\n\t}\n\treturn vInts, nil\n}\n\nfunc FetchLatestTagMatchingOrMain(version string) (string, error) {\n\ttag, err := utils.FetchLatestTag()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tver, err := splitVersion(tag)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tversionS, err := splitVersion(version)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\t// major version matches latest, use main branch\n\tif ver[0] == versionS[0] && ver[1] == versionS[1] {\n\t\treturn \"main\", nil\n\t} else {\n\t\treturn FetchLatestTagMatchingVersion(version)\n\t}\n}\n\nfunc FetchLatestTagMatchingVersion(version string) (string, error) {\n\tif version == \"Dev\" {\n\t\treturn \"Dev\", nil\n\t}\n\tres, err := http.Get(\"https://api.github.com/repos/spicetify/cli/releases\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdefer res.Body.Close()\n\tbody, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar releases []githubRelease\n\tif err = json.Unmarshal(body, &releases); err != nil {\n\t\treturn \"\", err\n\t}\n\tcurVer := strings.Split(version, \".\")\n\tcurVerMin, err2 := strconv.Atoi(curVer[2])\n\tif err2 != nil {\n\t\treturn \"\", err2\n\t}\n\tfor _, rel := range releases {\n\t\tver := strings.Split(rel.TagName[1:], \".\")\n\t\tif len(ver) != 3 {\n\t\t\tbreak\n\t\t} else {\n\t\t\tverMin, err := strconv.Atoi(ver[2])\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\tif ver[0] == curVer[0] && ver[1] == curVer[1] && verMin > curVerMin {\n\t\t\t\tcurVerMin = verMin\n\t\t\t}\n\t\t}\n\t}\n\treturn \"v\" + curVer[0] + \".\" + curVer[1] + \".\" + strconv.Itoa(curVerMin), nil\n}\n"
  },
  {
    "path": "src/status/backup/backup.go",
    "content": "package backupstatus\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/spicetify/cli/src/utils\"\n)\n\ntype status struct {\n\tstate int\n}\n\n// Status .\ntype Status interface {\n\tIsBackuped() bool\n\tIsEmpty() bool\n\tIsOutdated() bool\n}\n\nconst (\n\t// EMPTY No backup found\n\tEMPTY int = iota\n\t// BACKUPED There is available backup\n\tBACKUPED\n\t// OUTDATED Available backup has different version from Spotify version\n\tOUTDATED\n)\n\n// Get returns status of backup folder\nfunc Get(prefsPath, backupPath, backupVersion string) Status {\n\tfileList, err := os.ReadDir(backupPath)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tcur := EMPTY\n\n\tif len(fileList) != 0 {\n\t\tspaCount := 0\n\t\tfor _, file := range fileList {\n\t\t\tif !file.IsDir() && strings.HasSuffix(file.Name(), \".spa\") {\n\t\t\t\tspaCount++\n\t\t\t}\n\t\t}\n\n\t\tif spaCount > 0 {\n\t\t\tspotifyVersion := utils.GetSpotifyVersion(prefsPath)\n\n\t\t\tif backupVersion != spotifyVersion {\n\t\t\t\tcur = OUTDATED\n\t\t\t} else {\n\t\t\t\tcur = BACKUPED\n\t\t\t}\n\t\t}\n\t}\n\n\treturn status{\n\t\tstate: cur}\n}\n\nfunc (s status) IsBackuped() bool {\n\treturn s.state == BACKUPED\n}\n\nfunc (s status) IsEmpty() bool {\n\treturn s.state == EMPTY\n}\n\nfunc (s status) IsOutdated() bool {\n\treturn s.state == OUTDATED\n}\n"
  },
  {
    "path": "src/status/spotify/spotify.go",
    "content": "package spotifystatus\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n)\n\ntype status struct {\n\tstate int\n}\n\n// Status .\ntype Status interface {\n\tIsBackupable() bool\n\tIsModdable() bool\n\tIsStock() bool\n\tIsMixed() bool\n\tIsApplied() bool\n\tIsInvalid() bool\n}\n\nconst (\n\t// STOCK Spotify is in original state\n\tSTOCK int = iota\n\t// INVALID Apps folder is empty\n\tINVALID\n\t// APPLIED Spotify is modified\n\tAPPLIED\n\t// MIXED Spotify has modified files and stock files\n\tMIXED\n)\n\n// Get returns status of Spotify's Apps folder\nfunc Get(appsFolder string) Status {\n\tfileList, err := os.ReadDir(appsFolder)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tspaCount := 0\n\tdirCount := 0\n\tfor _, file := range fileList {\n\t\tif file.IsDir() {\n\t\t\tdirCount++\n\t\t} else if strings.HasSuffix(file.Name(), \".spa\") {\n\t\t\tspaCount++\n\t\t}\n\t}\n\n\tcur := INVALID\n\tif spaCount > 0 && dirCount > 0 {\n\t\tcur = MIXED\n\t} else if spaCount > 0 {\n\t\tcur = STOCK\n\t} else if dirCount > 0 {\n\t\tcur = APPLIED\n\t}\n\n\treturn status{\n\t\tstate: cur}\n}\n\nfunc (s status) IsBackupable() bool {\n\treturn s.state == STOCK || s.state == MIXED\n}\n\nfunc (s status) IsModdable() bool {\n\treturn s.state == APPLIED || s.state == MIXED\n}\n\nfunc (s status) IsStock() bool {\n\treturn s.state == STOCK\n}\n\nfunc (s status) IsMixed() bool {\n\treturn s.state == MIXED\n}\n\nfunc (s status) IsApplied() bool {\n\treturn s.state == APPLIED\n}\n\nfunc (s status) IsInvalid() bool {\n\treturn s.state == INVALID\n}\n"
  },
  {
    "path": "src/utils/color.go",
    "content": "package utils\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nvar (\n\txrdb map[string]string\n\n\t// BaseColorList is color names list and their default values\n\tBaseColorList = map[string]string{\n\t\t\"text\":               \"ffffff\",\n\t\t\"subtext\":            \"b3b3b3\",\n\t\t\"main\":               \"121212\",\n\t\t\"main-elevated\":      \"242424\",\n\t\t\"highlight\":          \"1a1a1a\",\n\t\t\"highlight-elevated\": \"2a2a2a\",\n\t\t\"sidebar\":            \"000000\",\n\t\t\"player\":             \"181818\",\n\t\t\"card\":               \"282828\",\n\t\t\"shadow\":             \"000000\",\n\t\t\"selected-row\":       \"ffffff\",\n\t\t\"button\":             \"1db954\",\n\t\t\"button-active\":      \"1ed760\",\n\t\t\"button-disabled\":    \"535353\",\n\t\t\"tab-active\":         \"333333\",\n\t\t\"notification\":       \"4687d6\",\n\t\t\"notification-error\": \"e22134\",\n\t\t\"misc\":               \"7f7f7f\",\n\t}\n\n\t// BaseColorOrder is color name list in an order\n\tBaseColorOrder = []string{\n\t\t\"text\",\n\t\t\"subtext\",\n\t\t\"main\",\n\t\t\"main-elevated\",\n\t\t\"highlight\",\n\t\t\"highlight-elevated\",\n\t\t\"sidebar\",\n\t\t\"player\",\n\t\t\"card\",\n\t\t\"shadow\",\n\t\t\"selected-row\",\n\t\t\"button\",\n\t\t\"button-active\",\n\t\t\"button-disabled\",\n\t\t\"tab-active\",\n\t\t\"notification\",\n\t\t\"notification-error\",\n\t\t\"misc\",\n\t}\n)\n\ntype color struct {\n\tred, green, blue int64\n}\n\n// Color stores hex and rgb value of color\ntype Color interface {\n\tHex() string\n\tRGB() string\n\tTerminalRGB() string\n}\n\n// ParseColor parses a string in both hex or rgb\n// or from XResources or env variable\n// and converts to both rgb and hex value\nfunc ParseColor(raw string) Color {\n\tvar red, green, blue int64\n\n\tif strings.HasPrefix(raw, \"${\") {\n\t\tendIndex := len(raw) - 1\n\t\traw = raw[2:endIndex]\n\n\t\t// From XResources database\n\t\tif strings.HasPrefix(raw, \"xrdb:\") {\n\t\t\traw = fromXResources(raw)\n\n\t\t\t// From environment variable\n\t\t} else if env := os.Getenv(raw); len(env) > 0 {\n\t\t\traw = env\n\t\t}\n\t}\n\n\t// rrr,bbb,ggg\n\tif strings.Contains(raw, \",\") {\n\t\tlist := strings.SplitN(raw, \",\", 3)\n\t\tlist = append(list, \"255\", \"255\")\n\n\t\tred = stringToInt(list[0], 10)\n\t\tgreen = stringToInt(list[1], 10)\n\t\tblue = stringToInt(list[2], 10)\n\n\t} else {\n\t\tre := regexp.MustCompile(\"[a-fA-F0-9]+\")\n\t\thex := re.FindString(raw)\n\n\t\t// Support short hex color form e.g. #fff, #121\n\t\tif len(hex) == 3 {\n\t\t\texpanded := []byte{\n\t\t\t\thex[0], hex[0],\n\t\t\t\thex[1], hex[1],\n\t\t\t\thex[2], hex[2]}\n\n\t\t\thex = string(expanded)\n\t\t}\n\n\t\thex += \"ffffff\"\n\n\t\tred = stringToInt(hex[:2], 16)\n\t\tgreen = stringToInt(hex[2:4], 16)\n\t\tblue = stringToInt(hex[4:6], 16)\n\t}\n\n\treturn color{red, green, blue}\n}\n\nfunc (c color) Hex() string {\n\treturn fmt.Sprintf(\"%02x%02x%02x\", c.red, c.green, c.blue)\n}\n\nfunc (c color) RGB() string {\n\treturn fmt.Sprintf(\"%d,%d,%d\", c.red, c.green, c.blue)\n}\n\nfunc (c color) TerminalRGB() string {\n\treturn fmt.Sprintf(\"%d;%d;%d\", c.red, c.green, c.blue)\n}\n\nfunc stringToInt(raw string, base int) int64 {\n\tvalue, err := strconv.ParseInt(raw, base, 0)\n\tif err != nil {\n\t\tvalue = 255\n\t}\n\n\tif value < 0 {\n\t\tvalue = 0\n\t}\n\n\tif value > 255 {\n\t\tvalue = 255\n\t}\n\n\treturn value\n}\n\nfunc getXRDB() error {\n\tdb := map[string]string{}\n\n\tif len(xrdb) > 0 {\n\t\treturn nil\n\t}\n\n\toutput, err := exec.Command(\"xrdb\", \"-query\").Output()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tscanner := bufio.NewScanner(bytes.NewReader(output))\n\tre := regexp.MustCompile(`^\\*\\.?(\\w+?):\\s*?#([A-Za-z0-9]+)`)\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tfor _, match := range re.FindAllStringSubmatch(line, -1) {\n\t\t\tif match != nil {\n\t\t\t\tdb[match[1]] = match[2]\n\t\t\t}\n\t\t}\n\t}\n\n\txrdb = db\n\n\treturn nil\n}\n\nfunc fromXResources(input string) string {\n\t// Example input:\n\t//   xrdb:color1\n\t//   xrdb:color2:#f0c\n\t//   xrdb:color5:40,50,60\n\tqueries := strings.Split(input, \":\")\n\tif len(queries[1]) == 0 {\n\t\tPrintError(`\"` + input + `\": Wrong XResources lookup syntax`)\n\t\tos.Exit(0)\n\t}\n\n\tif err := getXRDB(); err != nil {\n\t\tFatal(err)\n\t}\n\n\tif len(xrdb) < 1 {\n\t\tPrintError(\"XResources is not available\")\n\t\tos.Exit(0)\n\t}\n\n\tvalue, ok := xrdb[queries[1]]\n\n\tif !ok || len(value) == 0 {\n\t\tif len(queries) > 2 {\n\t\t\t// Fallback value\n\t\t\tvalue = queries[2]\n\t\t} else {\n\t\t\tPrintError(\"Variable is not available in XResources\")\n\t\t\tos.Exit(0)\n\t\t}\n\t}\n\n\treturn value\n}\n"
  },
  {
    "path": "src/utils/config.go",
    "content": "package utils\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/go-ini/ini\"\n)\n\nvar (\n\tconfigLayout = map[string]map[string]string{\n\t\t\"Setting\": {\n\t\t\t\"spotify_path\":           \"\",\n\t\t\t\"prefs_path\":             \"\",\n\t\t\t\"current_theme\":          \"\",\n\t\t\t\"color_scheme\":           \"\",\n\t\t\t\"inject_theme_js\":        \"1\",\n\t\t\t\"inject_css\":             \"1\",\n\t\t\t\"replace_colors\":         \"1\",\n\t\t\t\"overwrite_assets\":       \"0\",\n\t\t\t\"spotify_launch_flags\":   \"\",\n\t\t\t\"check_spicetify_update\": \"1\",\n\t\t\t\"always_enable_devtools\": \"0\",\n\t\t},\n\t\t\"Preprocesses\": {\n\t\t\t\"disable_sentry\":     \"1\",\n\t\t\t\"disable_ui_logging\": \"1\",\n\t\t\t\"remove_rtl_rule\":    \"1\",\n\t\t\t\"expose_apis\":        \"1\",\n\t\t},\n\t\t\"AdditionalOptions\": {\n\t\t\t\"extensions\":            \"\",\n\t\t\t\"custom_apps\":           \"\",\n\t\t\t\"sidebar_config\":        \"0\",\n\t\t\t\"home_config\":           \"1\",\n\t\t\t\"experimental_features\": \"1\",\n\t\t},\n\t\t\"Patch\": {},\n\t}\n)\n\ntype config struct {\n\tpath    string\n\tcontent *ini.File\n}\n\n// Config .\ntype Config interface {\n\tWrite() error\n\tGetSection(string) *ini.Section\n\tGetPath() string\n}\n\n// ParseConfig read config file content, return default config\n// if file doesn't exist.\nfunc ParseConfig(configPath string) Config {\n\tcfg, err := ini.LoadSources(\n\t\tini.LoadOptions{\n\t\t\tIgnoreContinuation:  true,\n\t\t\tIgnoreInlineComment: true,\n\t\t},\n\t\tconfigPath)\n\n\tif err != nil {\n\t\tdefaultConfig := config{\n\t\t\tpath:    configPath,\n\t\t\tcontent: getDefaultConfig(),\n\t\t}\n\t\tif err := defaultConfig.Write(); err != nil {\n\t\t\tPrintWarning(fmt.Sprintf(\"Failed to save config: %s\", err.Error()))\n\t\t} else {\n\t\t\tPrintSuccess(\"Default config-xpui.ini generated\")\n\t\t}\n\t\treturn defaultConfig\n\t}\n\n\tneedRewrite := false\n\tfor sectionName, keyList := range configLayout {\n\t\tsection, err := cfg.GetSection(sectionName)\n\t\tif err != nil {\n\t\t\tsection, _ = cfg.NewSection(sectionName)\n\t\t\tneedRewrite = true\n\t\t}\n\t\tfor keyName, defaultValue := range keyList {\n\t\t\tif _, err := section.GetKey(keyName); err != nil {\n\t\t\t\tsection.NewKey(keyName, defaultValue)\n\t\t\t\tneedRewrite = true\n\t\t\t}\n\t\t}\n\t}\n\n\tif needRewrite {\n\t\tPrintSuccess(\"Config is updated\")\n\t\tcfg.SaveTo(configPath)\n\t}\n\n\treturn config{\n\t\tpath:    configPath,\n\t\tcontent: cfg,\n\t}\n}\n\n// Write writes content to config file.\nfunc (c config) Write() error {\n\treturn c.content.SaveTo(c.path)\n}\n\nfunc (c config) GetSection(name string) *ini.Section {\n\tsec, err := c.content.GetSection(name)\n\n\tif err != nil {\n\t\tsec, _ = c.content.NewSection(name)\n\t\tfor keyName, defaultValue := range configLayout[name] {\n\t\t\tsec.NewKey(keyName, defaultValue)\n\t\t}\n\t}\n\n\treturn sec\n}\n\nfunc (c config) GetPath() string {\n\treturn c.path\n}\n\nfunc getDefaultConfig() *ini.File {\n\tvar cfg = ini.Empty()\n\n\tspotifyPath := FindAppPath()\n\tprefsFilePath := FindPrefFilePath()\n\n\tif len(spotifyPath) == 0 {\n\t\tPrintError(\"Could not detect Spotify location\")\n\t} else {\n\t\tconfigLayout[\"Setting\"][\"spotify_path\"] = spotifyPath\n\t}\n\n\tif len(prefsFilePath) == 0 {\n\t\tPrintError(\"Could not detect \\\"prefs\\\" file location\")\n\t} else {\n\t\tconfigLayout[\"Setting\"][\"prefs_path\"] = prefsFilePath\n\t}\n\n\tfor sectionName, keyList := range configLayout {\n\t\tsection, err := cfg.NewSection(sectionName)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tfor keyName, defaultValue := range keyList {\n\t\t\tsection.NewKey(keyName, defaultValue)\n\t\t}\n\t}\n\n\tversion, err := cfg.NewSection(\"Backup\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tversion.Comment = \"DO NOT CHANGE!\"\n\tversion.NewKey(\"version\", \"\")\n\tversion.NewKey(\"with\", \"\")\n\treturn cfg\n}\n\n// FindAppPath finds Spotify location in various possible places\n// of each platform and returns it.\n// Returns blank string if none of default locations exists.\nfunc FindAppPath() string {\n\tswitch runtime.GOOS {\n\tcase \"windows\":\n\t\tpath := winApp()\n\t\tif len(path) == 0 {\n\t\t\tpath = WinXApp()\n\t\t}\n\t\treturn path\n\n\tcase \"linux\":\n\t\treturn linuxApp()\n\n\tcase \"darwin\":\n\t\treturn darwinApp()\n\t}\n\n\treturn \"\"\n}\n\n// FindPrefFilePath finds Spotify \"prefs\" file location\n// in various possible places of each platform and returns it.\n// Returns blank string if none of default locations exists.\nfunc FindPrefFilePath() string {\n\tswitch runtime.GOOS {\n\tcase \"windows\":\n\t\tpath := winPrefs()\n\t\tif len(path) == 0 && len(WinXApp()) != 0 {\n\t\t\tpath = WinXPrefs()\n\t\t}\n\t\tif len(path) == 0 {\n\t\t\tPrintError(\"No valid path options found, ensure you have Spotify installed and have ran it for at least 30 seconds\")\n\t\t}\n\t\treturn path\n\n\tcase \"linux\":\n\t\treturn linuxPrefs()\n\n\tcase \"darwin\":\n\t\treturn darwinPrefs()\n\t}\n\n\treturn \"\"\n}\n\nfunc winApp() string {\n\tpath := filepath.Join(os.Getenv(\"APPDATA\"), \"Spotify\")\n\tif _, err := os.Stat(filepath.Join(path, \"Spotify.exe\")); err == nil {\n\t\treturn path\n\t}\n\n\treturn \"\"\n}\n\nfunc winPrefs() string {\n\tpath := filepath.Join(os.Getenv(\"APPDATA\"), \"Spotify\", \"prefs\")\n\tif _, err := os.Stat(path); err == nil {\n\t\treturn path\n\t}\n\n\treturn \"\"\n}\n\nfunc WinXApp() string {\n\tps, _ := exec.LookPath(\"powershell.exe\")\n\tcmd := exec.Command(ps,\n\t\t\"-NoProfile\",\n\t\t\"-NonInteractive\",\n\t\t`(Get-AppxPackage | Where-Object -Property Name -Eq \"SpotifyAB.SpotifyMusic\").InstallLocation`)\n\n\tstdOut, err := cmd.CombinedOutput()\n\tif err == nil {\n\t\treturn strings.TrimSpace(string(stdOut))\n\t}\n\n\treturn \"\"\n}\n\nfunc WinXPrefs() string {\n\tps, _ := exec.LookPath(\"powershell.exe\")\n\tcmd := exec.Command(ps,\n\t\t\"-NoProfile\",\n\t\t\"-NonInteractive\",\n\t\t`(Get-AppxPackage | Where-Object -Property Name -Match \"^SpotifyAB\").PackageFamilyName`)\n\n\tstdOut, err := cmd.CombinedOutput()\n\tif err == nil {\n\t\tpath := filepath.Join(\n\t\t\tos.Getenv(\"LOCALAPPDATA\"),\n\t\t\t\"Packages\",\n\t\t\tstrings.TrimSpace(string(stdOut)),\n\t\t\t\"LocalState\",\n\t\t\t\"Spotify\",\n\t\t\t\"prefs\")\n\t\tif _, err := os.Stat(path); err == nil {\n\t\t\treturn path\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\nfunc linuxApp() string {\n\tpath, err := exec.Command(\"whereis\", \"-b\", \"spotify\").Output()\n\n\tif err == nil {\n\t\tpathString := strings.Replace(string(path), \"spotify: \", \"\", 1)\n\t\tpathString = strings.Replace(pathString, \"\\n\", \"\", -1)\n\t\tbinList := strings.Split(pathString, \" \")\n\n\t\tfor _, v := range binList {\n\t\t\tbin := v\n\n\t\t\tstat, err := os.Lstat(bin)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (stat.Mode() & os.ModeSymlink) != 0 {\n\t\t\t\tbinDest, err := os.Readlink(v)\n\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tbin = binDest\n\t\t\t}\n\n\t\t\tbin = filepath.Dir(bin)\n\n\t\t\tif _, err := os.Stat(filepath.Join(bin, \"Apps\")); err == nil {\n\t\t\t\treturn bin\n\t\t\t}\n\t\t}\n\t}\n\n\tpotentialList := []string{\n\t\t\"/opt/spotify/\",\n\t\t\"/opt/spotify/spotify-client/\",\n\t\t\"/usr/share/spotify/\",\n\t\t\"/usr/libexec/spotify/\",\n\t\t\"/usr/share/spotify-client/\",\n\t\t\"/var/lib/flatpak/app/com.spotify.Client/x86_64/stable/active/files/extra/share/spotify/\",\n\t\t\"$HOME/.local/share/flatpak/app/com.spotify.Client/x86_64/stable/active/files/extra/share/spotify/\",\n\t\t\"$HOME/.local/share/spotify-launcher/install/usr/share/spotify/\",\n\t}\n\n\tfor _, v := range potentialList {\n\t\t_, err := os.Stat(filepath.Join(ReplaceEnvVarsInString(v), \"Apps\"))\n\t\t_, err2 := os.Stat(filepath.Join(ReplaceEnvVarsInString(v), \"spotify\"))\n\t\tif err == nil && err2 == nil {\n\t\t\treturn v\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\nfunc linuxPrefs() string {\n\tdotConfig := os.Getenv(\"XDG_CONFIG_HOME\")\n\n\tif len(dotConfig) == 0 {\n\t\tdotConfig = filepath.Join(os.Getenv(\"HOME\"), \".config\")\n\t}\n\n\tpref := filepath.Join(dotConfig, \"spotify\", \"prefs\")\n\tif _, err := os.Stat(pref); err == nil {\n\t\treturn pref\n\t}\n\n\treturn \"\"\n}\n\nfunc darwinApp() string {\n\tpath := filepath.Join(\"/Applications\", \"Spotify.app\", \"Contents\", \"Resources\")\n\tif _, err := os.Stat(path); err == nil {\n\t\treturn path\n\t}\n\n\treturn \"\"\n}\n\nfunc darwinPrefs() string {\n\tpref := filepath.Join(os.Getenv(\"HOME\"), \"Library/Application Support/Spotify/prefs\")\n\tif _, err := os.Stat(pref); err == nil {\n\t\treturn pref\n\t}\n\n\treturn \"\"\n}\n"
  },
  {
    "path": "src/utils/file-utils.go",
    "content": "package utils\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"os\"\n\t\"unicode/utf16\"\n)\n\nfunc ReadStringFromUTF16Binary(inputFile string, startMarker []byte, endMarker []byte) (string, int, int, error) {\n\tfileContent, err := os.ReadFile(inputFile)\n\tif err != nil {\n\t\treturn \"\", -1, -1, fmt.Errorf(\"error reading file %s: %w\", inputFile, err)\n\t}\n\n\tisUTF16LE := false\n\tif len(fileContent) >= 2 && fileContent[0] == 0xFF && fileContent[1] == 0xFE {\n\t\tisUTF16LE = true\n\t}\n\n\tif !isUTF16LE && len(fileContent) > 100 && fileContent[1] == 0x00 {\n\t\tisUTF16LE = true\n\t}\n\n\tvar startIdx, endIdx int\n\tvar contentToSearch []byte\n\tvar searchStartMarker, searchEndMarker []byte\n\n\tif !isUTF16LE {\n\t\treturn \"\", -1, -1, fmt.Errorf(\"file is not in UTF-16LE format: %s\", inputFile)\n\t}\n\n\tcontentToSearch = fileContent[2:]\n\tsearchStartMarker = encodeUTF16LE(startMarker)\n\tsearchEndMarker = encodeUTF16LE(endMarker)\n\n\tstartIdx = bytes.Index(contentToSearch, searchStartMarker)\n\tif startIdx == -1 {\n\t\treturn \"\", -1, -1, fmt.Errorf(\"start marker not found: %s\", string(startMarker))\n\t}\n\n\tsearchSpace := contentToSearch[startIdx+len(searchStartMarker):]\n\tendIdx = bytes.Index(searchSpace, searchEndMarker)\n\tif endIdx == -1 {\n\t\treturn \"\", -1, -1, fmt.Errorf(\"end marker not found after start index %d: %s\", startIdx+len(searchStartMarker), string(endMarker))\n\t}\n\n\tstringContentBytes := contentToSearch[startIdx : startIdx+len(searchStartMarker)+endIdx+len(searchEndMarker)]\n\n\tdecodedStringBytes, err := decodeUTF16LE(stringContentBytes)\n\tif err != nil {\n\t\treturn \"\", -1, -1, fmt.Errorf(\"error decoding UTF-16LE content: %w\", err)\n\t}\n\n\t// Adjust indices to be byte offsets in the original file\n\toriginalStartIdx := 2 + startIdx\n\toriginalEndIdx := 2 + endIdx + len(stringContentBytes)\n\treturn string(decodedStringBytes), originalStartIdx, originalEndIdx, nil\n}\n\n// Helper function to encode a byte slice (assumed UTF-8) to UTF-16LE\nfunc encodeUTF16LE(data []byte) []byte {\n\tutf16Bytes := utf16.Encode([]rune(string(data)))\n\tbyteSlice := make([]byte, len(utf16Bytes)*2)\n\tfor i, r := range utf16Bytes {\n\t\tbinary.LittleEndian.PutUint16(byteSlice[i*2:], r)\n\t}\n\n\treturn byteSlice\n}\n\n// Helper function to decode a byte slice (UTF-16LE) to UTF-8\nfunc decodeUTF16LE(data []byte) ([]byte, error) {\n\tif len(data)%2 != 0 {\n\t\treturn nil, fmt.Errorf(\"invalid UTF-16LE data length\")\n\t}\n\n\tuint16s := make([]uint16, len(data)/2)\n\tfor i := 0; i < len(data)/2; i++ {\n\t\tuint16s[i] = binary.LittleEndian.Uint16(data[i*2:])\n\t}\n\n\trunes := utf16.Decode(uint16s)\n\treturn []byte(string(runes)), nil\n}\n"
  },
  {
    "path": "src/utils/isAdmin/unix.go",
    "content": "//go:build !windows\n// +build !windows\n\npackage isAdmin\n\nimport \"os\"\n\nfunc Check(bypassAdminCheck bool) bool {\n\tif bypassAdminCheck {\n\t\treturn false\n\t}\n\treturn os.Geteuid() == 0\n}\n"
  },
  {
    "path": "src/utils/isAdmin/windows.go",
    "content": "//go:build windows\n// +build windows\n\npackage isAdmin\n\nimport (\n\t\"golang.org/x/sys/windows\"\n)\n\nfunc Check(bypassAdminCheck bool) bool {\n\tif bypassAdminCheck {\n\t\treturn false\n\t}\n\n\tvar sid *windows.SID\n\terr := windows.AllocateAndInitializeSid(\n\t\t&windows.SECURITY_NT_AUTHORITY,\n\t\t2,\n\t\twindows.SECURITY_BUILTIN_DOMAIN_RID,\n\t\twindows.DOMAIN_ALIAS_RID_ADMINS,\n\t\t0, 0, 0, 0, 0, 0,\n\t\t&sid)\n\tif err != nil {\n\t\treturn false\n\t}\n\tdefer windows.FreeSid(sid)\n\n\ttoken := windows.Token(0)\n\tmember, err := token.IsMember(sid)\n\treturn err == nil && member\n}\n"
  },
  {
    "path": "src/utils/path-utils.go",
    "content": "package utils\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n)\n\nfunc MigrateConfigFolder() {\n\tif runtime.GOOS == \"windows\" {\n\t\tsource := filepath.Join(os.Getenv(\"USERPROFILE\"), \".spicetify\")\n\t\tif _, err := os.Stat(source); err == nil {\n\t\t\tspinner, _ := Spinner.Start(\"Migrating config folder\")\n\t\t\tdestination := GetSpicetifyFolder()\n\t\t\terr := Copy(source, destination, true, nil)\n\t\t\tif err != nil {\n\t\t\t\tspinner.Fail(\"Failed to migrate config folder\")\n\t\t\t\tFatal(err)\n\t\t\t}\n\t\t\tos.RemoveAll(source)\n\t\t\tspinner.Success(\"Migrated config folder\")\n\t\t}\n\t}\n}\n\nfunc MigrateFolders() {\n\tbackupPath := filepath.Join(GetSpicetifyFolder(), \"Backup\")\n\textractedPath := filepath.Join(GetSpicetifyFolder(), \"Extracted\")\n\n\tif _, err := os.Stat(backupPath); err == nil {\n\t\tnewBackupPath := GetStateFolder(\"Backup\")\n\t\toldAbs, err := filepath.Abs(backupPath)\n\t\tif err != nil {\n\t\t\tFatal(err)\n\t\t}\n\t\tnewAbs, err := filepath.Abs(newBackupPath)\n\t\tif err != nil {\n\t\t\tFatal(err)\n\t\t}\n\n\t\tif oldAbs != newAbs {\n\t\t\tspinner, _ := Spinner.Start(\"Migrating backup folder\")\n\t\t\terr := Copy(backupPath, newBackupPath, true, nil)\n\t\t\tif err != nil {\n\t\t\t\tspinner.Fail(\"Failed to migrate backup folder\")\n\t\t\t\tFatal(err)\n\t\t\t}\n\t\t\tos.RemoveAll(backupPath)\n\t\t\tspinner.Success(\"Migrated backup folder\")\n\t\t}\n\t}\n\n\tif _, err := os.Stat(extractedPath); err == nil {\n\t\tnewExtractedPath := GetStateFolder(\"Extracted\")\n\t\toldAbs, err := filepath.Abs(extractedPath)\n\t\tif err != nil {\n\t\t\tFatal(err)\n\t\t}\n\t\tnewAbs, err := filepath.Abs(newExtractedPath)\n\t\tif err != nil {\n\t\t\tFatal(err)\n\t\t}\n\t\tif oldAbs != newAbs {\n\t\t\tspinner, _ := Spinner.Start(\"Migrating extracted folder\")\n\t\t\terr := Copy(extractedPath, newExtractedPath, true, nil)\n\t\t\tif err != nil {\n\t\t\t\tspinner.Fail(\"Failed to migrate extracted folder\")\n\t\t\t\tFatal(err)\n\t\t\t}\n\t\t\tos.RemoveAll(extractedPath)\n\t\t\tspinner.Success(\"Migrated extracted folder\")\n\t\t}\n\t}\n}\n\nfunc ReplaceEnvVarsInString(input string) string {\n\tvar replacements []string\n\tfor _, v := range os.Environ() {\n\t\tpair := strings.SplitN(v, \"=\", 2)\n\t\treplacements = append(replacements, \"$\"+pair[0], pair[1])\n\t}\n\treplacer := strings.NewReplacer(replacements...)\n\treturn replacer.Replace(input)\n}\n\nfunc GetSpicetifyFolder() string {\n\tresult, isAvailable := os.LookupEnv(\"SPICETIFY_CONFIG\")\n\tdefer func() { CheckExistAndCreate(result) }()\n\n\tif isAvailable && len(result) > 0 {\n\t\treturn result\n\t}\n\n\tif runtime.GOOS == \"windows\" {\n\t\tparent := os.Getenv(\"APPDATA\")\n\n\t\tresult = filepath.Join(parent, \"spicetify\")\n\t} else if runtime.GOOS == \"linux\" {\n\t\tparent, isAvailable := os.LookupEnv(\"XDG_CONFIG_HOME\")\n\n\t\tif !isAvailable || len(parent) == 0 {\n\t\t\tparent = filepath.Join(os.Getenv(\"HOME\"), \".config\")\n\t\t\tCheckExistAndCreate(parent)\n\t\t}\n\n\t\tresult = filepath.Join(parent, \"spicetify\")\n\t} else if runtime.GOOS == \"darwin\" {\n\t\tparent := filepath.Join(os.Getenv(\"HOME\"), \".config\")\n\t\tCheckExistAndCreate(parent)\n\n\t\tresult = filepath.Join(parent, \"spicetify\")\n\t}\n\n\treturn result\n}\n\nfunc GetStateFolder(name string) string {\n\tresult, isAvailable := os.LookupEnv(\"SPICETIFY_STATE\")\n\tdefer func() { CheckExistAndCreate(result) }()\n\n\tif isAvailable && len(result) > 0 {\n\t\treturn result\n\t}\n\n\tif runtime.GOOS == \"windows\" {\n\t\tparent := os.Getenv(\"APPDATA\")\n\n\t\tresult = filepath.Join(parent, \"spicetify\")\n\t} else if runtime.GOOS == \"linux\" {\n\t\tparent, isAvailable := os.LookupEnv(\"XDG_STATE_HOME\")\n\n\t\tif !isAvailable || len(parent) == 0 {\n\t\t\tparent = filepath.Join(os.Getenv(\"HOME\"), \".local\", \"state\")\n\t\t\tCheckExistAndCreate(parent)\n\t\t}\n\n\t\tresult = filepath.Join(parent, \"spicetify\")\n\t} else if runtime.GOOS == \"darwin\" {\n\t\tparent := filepath.Join(os.Getenv(\"HOME\"), \".local\", \"state\")\n\t\tCheckExistAndCreate(parent)\n\n\t\tresult = filepath.Join(parent, \"spicetify\")\n\t}\n\n\treturn GetSubFolder(result, name)\n}\n\n// GetSubFolder checks if folder `name` is available in specified folder,\n// else creates then returns the path.\nfunc GetSubFolder(folder string, name string) string {\n\tdir := filepath.Join(folder, name)\n\tCheckExistAndCreate(dir)\n\n\treturn dir\n}\n\nvar userAppsFolder = GetSubFolder(GetSpicetifyFolder(), \"CustomApps\")\nvar userExtensionsFolder = GetSubFolder(GetSpicetifyFolder(), \"Extensions\")\n\nfunc GetCustomAppSubfolderPath(folderPath string) string {\n\tentries, err := os.ReadDir(folderPath)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tfor _, entry := range entries {\n\t\tif entry.IsDir() {\n\t\t\tsubfolderPath := filepath.Join(folderPath, entry.Name())\n\t\t\tindexPath := filepath.Join(subfolderPath, \"index.js\")\n\n\t\t\tif _, err := os.Stat(indexPath); err == nil {\n\t\t\t\treturn subfolderPath\n\t\t\t}\n\n\t\t\tif subfolderPath := GetCustomAppSubfolderPath(subfolderPath); subfolderPath != \"\" {\n\t\t\t\treturn subfolderPath\n\t\t\t}\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\nfunc GetCustomAppPath(name string) (string, error) {\n\tcustomAppFolderPath := filepath.Join(userAppsFolder, name)\n\n\tif _, err := os.Stat(customAppFolderPath); err == nil {\n\t\tcustomAppActualFolderPath := GetCustomAppSubfolderPath(customAppFolderPath)\n\t\tif customAppActualFolderPath != \"\" {\n\t\t\treturn customAppActualFolderPath, nil\n\t\t}\n\t\treturn customAppFolderPath, nil\n\t}\n\n\tcustomAppFolderPath = filepath.Join(GetExecutableDir(), \"CustomApps\", name)\n\n\tif _, err := os.Stat(customAppFolderPath); err == nil {\n\t\tcustomAppActualFolderPath := GetCustomAppSubfolderPath(customAppFolderPath)\n\t\tif customAppActualFolderPath != \"\" {\n\t\t\treturn customAppActualFolderPath, nil\n\t\t}\n\t\treturn customAppFolderPath, nil\n\t}\n\n\treturn \"\", errors.New(\"custom app not found\")\n}\n\nfunc GetExtensionPath(name string) (string, error) {\n\textFilePath := filepath.Join(userExtensionsFolder, name)\n\n\tif _, err := os.Stat(extFilePath); err == nil {\n\t\treturn extFilePath, nil\n\t}\n\n\textFilePath = filepath.Join(GetExecutableDir(), \"Extensions\", name)\n\n\tif _, err := os.Stat(extFilePath); err == nil {\n\t\treturn extFilePath, nil\n\t}\n\n\treturn \"\", errors.New(\"extension not found\")\n}\n"
  },
  {
    "path": "src/utils/print.go",
    "content": "package utils\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/pterm/pterm\"\n)\n\nvar (\n\tInfo = pterm.PrefixPrinter{\n\t\tPrefix: pterm.Prefix{\n\t\t\tText:  \"info\",\n\t\t\tStyle: &pterm.Style{pterm.FgBlue},\n\t\t},\n\t}\n\tSuccess = pterm.PrefixPrinter{\n\t\tPrefix: pterm.Prefix{\n\t\t\tText:  \"success\",\n\t\t\tStyle: &pterm.Style{pterm.FgGreen},\n\t\t},\n\t}\n\tWarning = pterm.PrefixPrinter{\n\t\tPrefix: pterm.Prefix{\n\t\t\tText:  \"warning\",\n\t\t\tStyle: &pterm.Style{pterm.FgYellow},\n\t\t},\n\t}\n\tError = pterm.PrefixPrinter{\n\t\tPrefix: pterm.Prefix{\n\t\t\tText:  \"error\",\n\t\t\tStyle: &pterm.Style{pterm.FgRed},\n\t\t},\n\t}\n\tNote = pterm.PrefixPrinter{\n\t\tPrefix: pterm.Prefix{\n\t\t\tText:  \"note\",\n\t\t\tStyle: &pterm.Style{pterm.FgYellow},\n\t\t},\n\t}\n\tFatalPrefix = pterm.PrefixPrinter{\n\t\tPrefix: pterm.Prefix{\n\t\t\tText:  \"fatal\",\n\t\t\tStyle: &pterm.Style{pterm.BgRed, pterm.FgBlack},\n\t\t},\n\t}\n\tSpinner = pterm.SpinnerPrinter{\n\t\tSequence:       []string{\"-\", \"\\\\\", \"|\", \"/\"},\n\t\tStyle:          &pterm.ThemeDefault.SpinnerStyle,\n\t\tDelay:          time.Millisecond * 200,\n\t\tTimerStyle:     &pterm.ThemeDefault.TimerStyle,\n\t\tMessageStyle:   &pterm.ThemeDefault.SpinnerTextStyle,\n\t\tInfoPrinter:    &Info,\n\t\tSuccessPrinter: &Success,\n\t\tFailPrinter:    &Error,\n\t\tWarningPrinter: &Warning,\n\t\tWriter:         os.Stderr,\n\t}\n)\n\n// Bold .\nfunc Bold(text string) string {\n\treturn \"\\x1B[1m\" + text + \"\\033[0m\"\n}\n\n// Red .\nfunc Red(text string) string {\n\treturn \"\\x1B[31m\" + text + \"\\x1B[0m\"\n}\n\n// PrintBold prints a bold message\nfunc PrintBold(text string) {\n\tlog.Println(Bold(text))\n}\n\n// PrintNote prints a warning message\nfunc PrintNote(text string) {\n\tNote.Println(text)\n}\n\n// PrintWarning prints a warning message\nfunc PrintWarning(text string) {\n\tWarning.Println(text)\n}\n\n// PrintError prints an error message\nfunc PrintError(text string) {\n\tError.Println(text)\n}\n\n// PrintSuccess prints a success message\nfunc PrintSuccess(text string) {\n\tSuccess.Println(text)\n}\n\n// PrintInfo prints an info message\nfunc PrintInfo(text string) {\n\tInfo.Println(text)\n}\n\n// Fatal prints fatal message and exits process\nfunc Fatal(err error) {\n\tFatalPrefix.Println(err.Error())\n\tos.Exit(1)\n}\n"
  },
  {
    "path": "src/utils/scanner.go",
    "content": "package utils\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os/exec\"\n)\n\n// CmdScanner is a helper function to scan output from exec.Cmd\nfunc CmdScanner(cmd *exec.Cmd) {\n\tstdout, _ := cmd.StdoutPipe()\n\tcmd.Start()\n\n\tscanner := bufio.NewScanner(stdout)\n\tfor scanner.Scan() {\n\t\tm := scanner.Text()\n\t\tfmt.Println(m)\n\t}\n\tcmd.Wait()\n}\n"
  },
  {
    "path": "src/utils/show-dir.go",
    "content": "package utils\n\nimport (\n\t\"os/exec\"\n\t\"runtime\"\n)\n\n// ShowDirectory shows directory in user's default file manager application\nfunc ShowDirectory(dir string) error {\n\tvar err error\n\terr = nil\n\n\tif runtime.GOOS == \"windows\" {\n\t\t_, err = exec.Command(\"explorer\", dir).Output()\n\t\tif err != nil && err.Error() == \"exit status 1\" {\n\t\t\terr = nil\n\t\t}\n\t} else if runtime.GOOS == \"linux\" {\n\t\t_, err = exec.Command(\"xdg-open\", dir).Output()\n\t} else if runtime.GOOS == \"darwin\" {\n\t\t_, err = exec.Command(\"open\", dir).Output()\n\t}\n\n\treturn err\n}\n"
  },
  {
    "path": "src/utils/utils.go",
    "content": "package utils\n\nimport (\n\t\"archive/zip\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-ini/ini\"\n)\n\n// CheckExistAndCreate checks folder existence\n// and makes that folder, recursively, if it does not exist\nfunc CheckExistAndCreate(dir string) {\n\t_, err := os.Stat(dir)\n\tif err != nil {\n\t\tos.MkdirAll(dir, 0700)\n\t}\n}\n\n// CheckExistAndDelete checks folder existence\n// and deletes that folder if it does exist\nfunc CheckExistAndDelete(dir string) {\n\t_, err := os.Stat(dir)\n\tif err == nil {\n\t\tos.RemoveAll(dir)\n\t}\n}\n\n// Unzip unzips zip\nfunc Unzip(src, dest string) error {\n\tr, err := zip.OpenReader(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer r.Close()\n\n\tfor _, f := range r.File {\n\t\trc, err := f.Open()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer rc.Close()\n\n\t\tfpath := filepath.Join(dest, f.Name)\n\t\tif f.FileInfo().IsDir() {\n\t\t\tos.MkdirAll(fpath, 0700)\n\t\t} else {\n\t\t\tvar fdir string\n\t\t\tif lastIndex := strings.LastIndex(fpath, string(os.PathSeparator)); lastIndex > -1 {\n\t\t\t\tfdir = fpath[:lastIndex]\n\t\t\t}\n\n\t\t\terr = os.MkdirAll(fdir, 0700)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatal(err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tf, err := os.OpenFile(\n\t\t\t\tfpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0700)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tdefer f.Close()\n\n\t\t\t_, err = io.Copy(f, rc)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// Copy .\nfunc Copy(src, dest string, recursive bool, filters []string) error {\n\tdir, err := os.ReadDir(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tos.MkdirAll(dest, 0700)\n\n\tfor _, file := range dir {\n\t\tfileName := file.Name()\n\t\tfSrcPath := filepath.Join(src, fileName)\n\n\t\tfDestPath := filepath.Join(dest, fileName)\n\t\tif file.IsDir() && recursive {\n\t\t\tos.MkdirAll(fDestPath, 0700)\n\t\t\tif err = Copy(fSrcPath, fDestPath, true, filters); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tif len(filters) > 0 {\n\t\t\t\tisMatch := false\n\n\t\t\t\tfor _, filter := range filters {\n\t\t\t\t\tif strings.Contains(fileName, filter) {\n\t\t\t\t\t\tisMatch = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif !isMatch {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfSrc, err := os.Open(fSrcPath)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tdefer fSrc.Close()\n\n\t\t\tfDest, err := os.OpenFile(\n\t\t\t\tfDestPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0700)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tdefer fDest.Close()\n\n\t\t\t_, err = io.Copy(fDest, fSrc)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// CopyFile .\nfunc CopyFile(srcPath, dest string) error {\n\tfSrc, err := os.Open(srcPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer fSrc.Close()\n\n\tCheckExistAndCreate(dest)\n\tdestPath := filepath.Join(dest, filepath.Base(srcPath))\n\tfDest, err := os.OpenFile(\n\t\tdestPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0700)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer fDest.Close()\n\n\t_, err = io.Copy(fDest, fSrc)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Replace uses Regexp to find any matched from `input` with `regexpTerm`\n// and replaces them with `replaceTerm` then returns new string.\nfunc Replace(str *string, pattern string, repl func(submatches ...string) string) {\n\tre := regexp.MustCompile(pattern)\n\t*str = re.ReplaceAllStringFunc(*str, func(match string) string {\n\t\tsubmatches := re.FindStringSubmatch(match)\n\t\treturn repl(submatches...)\n\t})\n}\n\nfunc ReplaceOnce(str *string, pattern string, repl func(submatches ...string) string) {\n\tre := regexp.MustCompile(pattern)\n\tfirstMatch := true\n\t*str = re.ReplaceAllStringFunc(*str, func(match string) string {\n\t\tif firstMatch {\n\t\t\tfirstMatch = false\n\t\t\tsubmatches := re.FindStringSubmatch(match)\n\t\t\tif submatches != nil {\n\t\t\t\treturn repl(submatches...)\n\t\t\t}\n\t\t}\n\t\treturn match\n\t})\n}\n\nfunc ReplaceOnceWithPriority(str *string, patterns []string, repl func(index int, submatches ...string) string) {\n\tfor i, pattern := range patterns {\n\t\tre := regexp.MustCompile(pattern)\n\t\tfirstMatch := true\n\t\t*str = re.ReplaceAllStringFunc(*str, func(match string) string {\n\t\t\tif firstMatch {\n\t\t\t\tfirstMatch = false\n\t\t\t\tsubmatches := re.FindStringSubmatch(match)\n\t\t\t\tif submatches != nil {\n\t\t\t\t\treturn repl(i, submatches...)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn match\n\t\t})\n\t\tif !firstMatch {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\nfunc FindMatch(input string, regexpTerm string) [][]string {\n\tre := regexp.MustCompile(regexpTerm)\n\tmatches := re.FindAllStringSubmatch(input, -1)\n\treturn matches\n}\n\nfunc FindFirstMatch(input string, regexpTerm string) []string {\n\tmatches := FindMatch(input, regexpTerm)\n\tif len(matches) > 0 {\n\t\treturn matches[0]\n\t}\n\treturn nil\n}\n\nfunc FindLastMatch(input string, regexpTerm string) []string {\n\tmatches := FindMatch(input, regexpTerm)\n\tif len(matches) > 0 {\n\t\treturn matches[len(matches)-1]\n\t}\n\treturn nil\n}\n\n// ModifyFile opens file, changes file content by executing\n// `repl` callback function and writes new content.\nfunc ModifyFile(path string, repl func(string) string) {\n\traw, err := os.ReadFile(path)\n\tif err != nil {\n\t\tlog.Print(err)\n\t\treturn\n\t}\n\n\tcontent := repl(string(raw))\n\n\tos.WriteFile(path, []byte(content), 0700)\n}\n\n// CreateFile creates a file with given path and content.\nfunc CreateFile(path string, content string) error {\n\terr := os.WriteFile(path, []byte(content), 0600)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// GetSpotifyVersion .\nfunc GetSpotifyVersion(prefsPath string) string {\n\tpref, err := ini.Load(prefsPath)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\trootSection, err := pref.GetSection(\"\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tversion := rootSection.Key(\"app.last-launched-version\")\n\treturn version.MustString(\"\")\n}\n\n// GetExecutableDir returns directory of current process\nfunc GetExecutableDir() string {\n\texe, err := os.Executable()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\texeDir := filepath.Dir(exe)\n\n\tif link, err := filepath.EvalSymlinks(exe); err == nil {\n\t\treturn filepath.Dir(link)\n\t}\n\n\treturn exeDir\n}\n\n// GetJsHelperDir returns jsHelper directory in executable directory\nfunc GetJsHelperDir() string {\n\treturn filepath.Join(GetExecutableDir(), \"jsHelper\")\n}\n\n// PrependTime prepends current time string to text and returns new string\nfunc PrependTime(text string) string {\n\tdate := time.Now()\n\treturn fmt.Sprintf(\"%02d:%02d:%02d \", date.Hour(), date.Minute(), date.Second()) + text\n}\n\n// FindSymbol uses regexp from one or multiple clues to find variable or\n// function symbol in obfuscated code.\nfunc FindSymbol(debugInfo, content string, clues []string) []string {\n\tfor _, v := range clues {\n\t\tre := regexp.MustCompile(v)\n\t\tfound := re.FindStringSubmatch(content)\n\t\tif found != nil {\n\t\t\treturn found[1:]\n\t\t}\n\t}\n\n\tif len(debugInfo) > 0 {\n\t\tPrintError(\"Cannot find symbol for \" + debugInfo)\n\t}\n\n\treturn nil\n}\n\n// FindSymbolWithPattern uses regexp from one or multiple clues to find variable or\n// function symbol in obfuscated code. Returns the matched symbols and the pattern that matched.\nfunc FindSymbolWithPattern(debugInfo, content string, clues []string) ([]string, string) {\n\tfor _, v := range clues {\n\t\tre := regexp.MustCompile(v)\n\t\tfound := re.FindStringSubmatch(content)\n\t\tif found != nil {\n\t\t\treturn found[1:], v\n\t\t}\n\t}\n\n\tif len(debugInfo) > 0 {\n\t\tPrintError(\"Cannot find symbol for \" + debugInfo)\n\t}\n\n\treturn nil, \"\"\n}\n\n// CreateJunction creates a junction in Windows or a symlink in Linux/Mac.\nfunc CreateJunction(location, destination string) error {\n\tCheckExistAndDelete(destination)\n\tswitch runtime.GOOS {\n\tcase \"windows\":\n\t\texec.Command(\"cmd\", \"/C\", \"rmdir\", destination).Run()\n\t\treturn exec.Command(\"cmd\", \"/C\", \"mklink\", \"/J\", destination, location).Run()\n\tcase \"linux\", \"darwin\":\n\t\treturn exec.Command(\"ln\", \"-Fsf\", location, destination).Run()\n\t}\n\n\treturn nil\n}\n\nfunc SeekToCloseParen(content string, regexpTerm string, leftChar, rightChar byte) string {\n\tloc := regexp.MustCompile(regexpTerm).FindStringIndex(content)\n\tif len(loc) > 0 {\n\t\tstart := loc[0]\n\t\tend := start\n\t\tcount := 0\n\t\tinit := false\n\n\t\tfor {\n\t\t\tswitch content[end] {\n\t\t\tcase leftChar:\n\t\t\t\tcount += 1\n\t\t\t\tinit = true\n\t\t\tcase rightChar:\n\t\t\t\tcount -= 1\n\t\t\t}\n\t\t\tend += 1\n\t\t\tif count == 0 && init {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\treturn content[start:end]\n\t}\n\treturn \"\"\n}\n\ntype AppManifest struct {\n\tFiles          []string `json:\"subfiles\"`\n\tExtensionFiles []string `json:\"subfiles_extension\"`\n\tAssets         []string `json:\"assets\"`\n}\n\nfunc GetAppManifest(app string) (AppManifest, string, error) {\n\tcustomAppPath, err := GetCustomAppPath(app)\n\tif err != nil {\n\t\tPrintError(`Custom app \"` + app + `\" not found.`)\n\t\treturn AppManifest{}, customAppPath, err\n\t}\n\tmanifestFileContent, err := os.ReadFile(filepath.Join(customAppPath, \"manifest.json\"))\n\tif err != nil {\n\t\tmanifestFileContent = []byte{'{', '}'}\n\t}\n\tvar manifestJson AppManifest\n\tif err = json.Unmarshal(manifestFileContent, &manifestJson); err == nil {\n\t\treturn manifestJson, customAppPath, err\n\t}\n\treturn manifestJson, customAppPath, err\n}\n"
  },
  {
    "path": "src/utils/vcs.go",
    "content": "package utils\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n)\n\ntype GithubRelease struct {\n\tTagName string `json:\"tag_name\"`\n\tMessage string `json:\"message\"`\n}\n\nfunc FetchLatestTag() (string, error) {\n\tres, err := http.Get(\"https://api.github.com/repos/spicetify/cli/releases/latest\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tbody, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar release GithubRelease\n\tif err = json.Unmarshal(body, &release); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif release.TagName == \"\" {\n\t\treturn \"\", errors.New(\"GitHub response: \" + release.Message)\n\t}\n\n\treturn release.TagName[1:], nil\n}\n"
  },
  {
    "path": "src/utils/watcher.go",
    "content": "package utils\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"golang.org/x/net/websocket\"\n)\n\nvar (\n\t// INTERVAL .\n\tINTERVAL = 200 * time.Millisecond\n)\n\n// Watch .\nfunc Watch(fileList []string, callbackEach func(fileName string, err error), callbackAfter func()) {\n\tvar cache = map[string][]byte{}\n\n\tfor {\n\t\tfinalCallback := false\n\t\tfor _, v := range fileList {\n\t\t\tcurr, err := os.ReadFile(v)\n\t\t\tif err != nil {\n\t\t\t\tcallbackEach(v, err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif !bytes.Equal(cache[v], curr) {\n\t\t\t\tcallbackEach(v, nil)\n\t\t\t\tcache[v] = curr\n\t\t\t\tfinalCallback = true\n\t\t\t}\n\t\t}\n\n\t\tif callbackAfter != nil && finalCallback {\n\t\t\tcallbackAfter()\n\t\t}\n\n\t\ttime.Sleep(INTERVAL)\n\t}\n}\n\n// WatchRecursive .\nfunc WatchRecursive(root string, callbackEach func(fileName string, err error), callbackAfter func()) {\n\tvar cache = map[string][]byte{}\n\n\tfor {\n\t\tfinalCallback := false\n\n\t\tfilepath.WalkDir(root, func(filePath string, info os.DirEntry, _ error) error {\n\t\t\tif info.IsDir() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tcurr, err := os.ReadFile(filePath)\n\t\t\tif err != nil {\n\t\t\t\tcallbackEach(filePath, err)\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif !bytes.Equal(cache[filePath], curr) {\n\t\t\t\tcallbackEach(filePath, nil)\n\t\t\t\tcache[filePath] = curr\n\t\t\t\tfinalCallback = true\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\n\t\tif callbackAfter != nil && finalCallback {\n\t\t\tcallbackAfter()\n\t\t}\n\n\t\ttime.Sleep(INTERVAL)\n\t}\n}\n\ntype debugger struct {\n\tDescription          string\n\tDevtoolsFrontendUrl  string\n\tId                   string\n\tTitle                string\n\tType                 string\n\tUrl                  string\n\tWebSocketDebuggerUrl string\n}\n\n// GetDebuggerPath fetches opening debugger list from localhost and returns\n// the Spotify one.\nfunc GetDebuggerPath() string {\n\tres, err := http.Get(\"http://localhost:9222/json/list\")\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tbody, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tvar list []debugger\n\tif err = json.Unmarshal(body, &list); err != nil {\n\t\treturn \"\"\n\t}\n\n\tfor _, debugger := range list {\n\t\tif strings.Contains(debugger.Url, \"spotify\") {\n\t\t\treturn debugger.WebSocketDebuggerUrl\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\n// SendReload sends reload command to debugger Websocket server\nfunc SendReload(debuggerURL *string) error {\n\tif len(*debuggerURL) == 0 {\n\t\t*debuggerURL = GetDebuggerPath()\n\t}\n\n\tsocket, err := websocket.Dial(*debuggerURL, \"\", \"http://localhost/\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer socket.Close()\n\n\tif _, err := socket.Write([]byte(`{\"id\":0,\"method\":\"Runtime.evaluate\",\"params\":{\"expression\":\"window.location.reload()\"}}`)); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  }
]