Full Code of spicetify/cli for AI

main 919a0197562e cached
100 files
930.5 KB
310.0k tokens
688 symbols
1 requests
Download .txt
Showing preview only (969K chars total). Download the full file or copy to clipboard to get everything.
Repository: spicetify/cli
Branch: main
Commit: 919a0197562e
Files: 100
Total size: 930.5 KB

Directory structure:
gitextract_d9xm56qv/

├── .coderabbit.yaml
├── .github/
│   ├── dependabot.yml
│   ├── labeler.yml
│   └── workflows/
│       ├── build.yml
│       ├── labeler.yml
│       ├── linter.yml
│       └── lintpr.yml
├── .gitignore
├── .vscode/
│   ├── extensions.json
│   └── settings.json
├── CONTRIBUTING.md
├── CustomApps/
│   ├── lyrics-plus/
│   │   ├── OptionsMenu.js
│   │   ├── Pages.js
│   │   ├── PlaybarButton.js
│   │   ├── ProviderGenius.js
│   │   ├── ProviderLRCLIB.js
│   │   ├── ProviderMusixmatch.js
│   │   ├── ProviderNetease.js
│   │   ├── Providers.js
│   │   ├── README.md
│   │   ├── Settings.js
│   │   ├── TabBar.js
│   │   ├── Translator.js
│   │   ├── Utils.js
│   │   ├── index.js
│   │   ├── manifest.json
│   │   └── style.css
│   ├── new-releases/
│   │   ├── Card.js
│   │   ├── Icons.js
│   │   ├── Settings.js
│   │   ├── index.js
│   │   ├── manifest.json
│   │   └── style.css
│   └── reddit/
│       ├── Card.js
│       ├── Icons.js
│       ├── OptionsMenu.js
│       ├── Settings.js
│       ├── SortBox.js
│       ├── TabBar.js
│       ├── index.js
│       ├── manifest.json
│       └── style.css
├── Extensions/
│   ├── autoSkipExplicit.js
│   ├── autoSkipVideo.js
│   ├── bookmark.js
│   ├── fullAppDisplay.js
│   ├── keyboardShortcut.js
│   ├── loopyLoop.js
│   ├── popupLyrics.js
│   ├── shuffle+.js
│   ├── trashbin.js
│   └── webnowplaying.js
├── LICENSE
├── README.md
├── Themes/
│   └── SpicetifyDefault/
│       ├── color.ini
│       └── user.css
├── biome.json
├── css-map.json
├── globals.d.ts
├── go.mod
├── go.sum
├── install.ps1
├── install.sh
├── jsHelper/
│   ├── expFeatures.js
│   ├── homeConfig.js
│   ├── sidebarConfig.js
│   └── spicetifyWrapper.js
├── manifest.json
├── spicetify.go
└── src/
    ├── apply/
    │   └── apply.go
    ├── backup/
    │   └── backup.go
    ├── cmd/
    │   ├── apply.go
    │   ├── auto.go
    │   ├── backup.go
    │   ├── block-updates.go
    │   ├── cmd.go
    │   ├── color.go
    │   ├── config-dir.go
    │   ├── config.go
    │   ├── devtools.go
    │   ├── patch.go
    │   ├── path.go
    │   ├── restart.go
    │   ├── update.go
    │   └── watch.go
    ├── preprocess/
    │   └── preprocess.go
    ├── status/
    │   ├── backup/
    │   │   └── backup.go
    │   └── spotify/
    │       └── spotify.go
    └── utils/
        ├── color.go
        ├── config.go
        ├── file-utils.go
        ├── isAdmin/
        │   ├── unix.go
        │   └── windows.go
        ├── path-utils.go
        ├── print.go
        ├── scanner.go
        ├── show-dir.go
        ├── utils.go
        ├── vcs.go
        └── watcher.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .coderabbit.yaml
================================================
issue_enrichment:
  auto_enrich:
    enabled: false


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "daily"
  - package-ecosystem: "gomod"
    directory: "/"
    schedule:
      interval: "daily"


================================================
FILE: .github/labeler.yml
================================================
📦 aur:
  - "(aur)"
📦 snap:
  - "(snap)"
📦 brew:
  - "(brew)"
🪟 windows:
  - "(windows)"
🐧 linux:
  - "(linux)"
🍎 macos:
  - "(macos)"
🔵 extension:
  - "(extension|auto.*?skip|bookmark|full.*?app.*?display|keyboard.*?shortcut|shuffle|web.*?now.*?playing|popup.*?lyrics)"
🔴 custom app:
  - "(custom.*?app|lyrics.*?plus|new.*?releases|store|reddit|lyrics)"
🖇 duplicate:
  - "(duplicate)"


================================================
FILE: .github/workflows/build.yml
================================================
name: Build

on:
  pull_request:
    branches:
      - "main"
      - "*/main/*/**"
  push:
    branches:
      - "main"
      - "*/main/*/**"
  release:
    types: [published]

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup Go
        uses: actions/setup-go@v6
        with:
          go-version-file: "go.mod"

      - name: Build
        run: go build .

      - name: Format
        run: |
          gofmt -s -l .
          if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then exit 1; fi

  release:
    permissions:
      id-token: write
      contents: write
      attestations: write
    name: Release
    strategy:
      matrix:
        os: ["linux", "darwin", "windows"]
        arch: ["amd64", "arm64", "386"]
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v2')
    needs: build

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Get Tag
        run: echo "TAG=${GITHUB_REF#refs/*/v}" >> $GITHUB_ENV

      - name: Is Unix Platform
        run: echo "IS_UNIX=${{ matrix.os != 'windows' && matrix.arch != '386' && (matrix.os != 'linux' || matrix.arch != 'arm64') }}" >> $GITHUB_ENV

      - name: Is Windows Platform
        run: echo "IS_WIN=${{ matrix.os == 'windows' }}" >> $GITHUB_ENV

      - name: Setup Go
        uses: actions/setup-go@v6
        with:
          go-version-file: "go.mod"

      - name: Build
        if: env.IS_UNIX == 'true' || env.IS_WIN == 'true'
        run: |
          go build -ldflags "-X main.version=${{ env.TAG }}" -o "./spicetify${{ matrix.os == 'windows' && '.exe' || '' }}"
          chmod +x "./spicetify${{ matrix.os == 'windows' && '.exe' || '' }}"
        env:
          GOOS: ${{ matrix.os }}
          GOARCH: ${{ matrix.arch }}
          CGO_ENABLED: 0

      - name: Upload Artifact for Signing
        if: env.IS_WIN == 'true'
        id: upload-artifact-for-signing
        uses: actions/upload-artifact@v6
        with:
          name: spicetify-${{ env.TAG }}-${{ matrix.os }}-${{ (matrix.arch == 'amd64' && 'x64') || (matrix.arch == 'arm64' && 'arm64') || 'x32' }}-unsigned
          path: ./spicetify.exe

      - name: Sign Windows Executable
        if: env.IS_WIN == 'true'
        uses: signpath/github-action-submit-signing-request@v2
        with:
          api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
          organization-id: ${{ secrets.SIGNPATH_ORG_ID }}
          project-slug: "cli"
          signing-policy-slug: "release-signing"
          github-artifact-id: ${{ steps.upload-artifact-for-signing.outputs.artifact-id }}
          wait-for-completion: true
          output-artifact-directory: "./signed"

      - name: Copy Signed Windows Executable
        if: env.IS_WIN == 'true'
        run: |
          cp ./signed/spicetify.exe ./spicetify.exe

      - name: Attest output
        uses: actions/attest-build-provenance@v4
        if: env.IS_UNIX == 'true' || env.IS_WIN == 'true'
        with:
          subject-path: "./spicetify${{ matrix.os == 'windows' && '.exe' || '' }}"
          subject-name: "spicetify v${{ env.TAG }} (${{ matrix.os }}, ${{ (matrix.os == 'windows' && matrix.arch == 'amd64' && 'x64') || (matrix.os == 'windows' && matrix.arch == '386' && 'x32') || matrix.arch }})"

      - name: 7z - .tar
        if: env.IS_UNIX == 'true'
        uses: edgarrc/action-7z@v1
        with:
          args: 7z a -bb0 "spicetify-${{ env.TAG }}-${{ matrix.os }}-${{ matrix.arch }}.tar" "./spicetify" "./CustomApps" "./Extensions" "./Themes" "./jsHelper" "globals.d.ts" "css-map.json"

      - name: 7z - .tar.gz
        if: env.IS_UNIX == 'true'
        uses: edgarrc/action-7z@v1
        with:
          args: 7z a -bb0 -sdel -mx9 "spicetify-${{ env.TAG }}-${{ matrix.os }}-${{ matrix.arch }}.tar.gz" "spicetify-${{ env.TAG }}-${{ matrix.os }}-${{ matrix.arch }}.tar"

      - name: 7z - .zip
        if: env.IS_WIN == 'true'
        uses: edgarrc/action-7z@v1
        with:
          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"

      - name: Release
        if: env.IS_UNIX == 'true' || env.IS_WIN == 'true'
        uses: softprops/action-gh-release@v2
        with:
          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' }}"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  trigger-release:
    name: Trigger Homebrew/AUR Release
    runs-on: ubuntu-latest
    needs: release
    steps:
      - name: Update AUR package
        uses: fjogeleit/http-request-action@master
        with:
          url: https://spicetify-update.itsmeow.dev/spicetify-update
          method: GET
      - name: Update Winget package
        uses: vedantmgoyal9/winget-releaser@main
        with:
          identifier: Spicetify.Spicetify
          installers-regex: '-windows-\w+\.zip$'
          token: ${{ secrets.SPICETIFY_WINGET_TOKEN }}


================================================
FILE: .github/workflows/labeler.yml
================================================
name: Issue Labeler

on:
  issues:
    types: [opened, edited]

jobs:
  triage:
    runs-on: ubuntu-latest
    steps:
      - uses: github/issue-labeler@v3.4
        with:
          repo-token: "${{ secrets.GITHUB_TOKEN }}"
          configuration-path: .github/labeler.yml
          enable-versioned-regex: 0


================================================
FILE: .github/workflows/linter.yml
================================================
name: Code quality

on:
  pull_request:
    branches:
      - "main"
      - "*/main/*/**"
  push:
    branches:
      - "main"
      - "*/main/*/**"

jobs:
  linter:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout the repo
        uses: actions/checkout@v6

      - name: Setup Biome
        uses: biomejs/setup-biome@v2
        with:
          version: latest

      - name: Run Biome
        run: biome ci . --files-ignore-unknown=true --diagnostic-level=error


================================================
FILE: .github/workflows/lintpr.yml
================================================
name: Lint Pull Request

on:
  pull_request_target:
    types: [opened, edited, synchronize]

jobs:
  lintpr:
    runs-on: ubuntu-latest
    steps:
      - name: Lint pull request title
        uses: amannn/action-semantic-pull-request@v6
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          disallowScopes: |
            [A-Z]+
          subjectPattern: ^(?![A-Z]).+$


================================================
FILE: .gitignore
================================================
# Executables
bin
cli
spicetify
spicetify-cli
*.exe

# MacOS
.DS_Store

# Node.js
node_modules
package-lock.json
package.json

# Logs
install.log

pnpm-lock.yaml


================================================
FILE: .vscode/extensions.json
================================================
{
	"recommendations": ["timonwong.shellcheck", "biomejs.biome", "golang.go", "ms-vscode.powershell"]
}


================================================
FILE: .vscode/settings.json
================================================
{
	"editor.formatOnSave": true,
	"[go]": {
		"editor.defaultFormatter": "golang.go"
	},
	"[powershell]": {
		"editor.defaultFormatter": "ms-vscode.powershell"
	},
	"[javascript][typescript][json]": {
		"editor.defaultFormatter": "biomejs.biome"
	}
}


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Spicetify-cli

## Table of Contents

- [I Have a Question](#i-have-a-question)
- [How to Contribute](#how-to-contribute)
  - [Reporting Bugs](#reporting-bugs)
  - [Suggesting Enhancements](#suggesting-enhancements)
  - [Your First Code Contribution](#your-first-code-contribution)
  - [Improving The Documentation](#improving-the-documentation)
  - [Commit Message Format](#commit-message-format)

## I Have a Question

> If you want to ask a question, we assume that you have read the available [Documentation](https://spicetify.app/docs/getting-started/).

Before 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.

If you then still feel the need to ask a question and need clarification, we recommend the following:

- Open an [issue](https://github.com/spicetify/cli/issues/new).
- Provide both Spicetify and Spotify version.
- Explain what the problem is.

We will then take care of the issue as soon as possible.

## How to Contribute

> ### Legal Notice
> 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.

### Reporting Bugs

#### Before Submitting a Bug Report

A 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.

- Make sure that you are using the latest version.
- 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)).
- 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).

#### How Do I Submit a Good Bug Report?

We use GitHub issues to track bugs and errors. If you run into an issue with the project:

- 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.)
  - Use the provided [bug template](https://github.com/spicetify/cli/issues/new?assignees=&labels=%F0%9F%90%9B+bug&projects=&template=bug_report.yml).
- Explain the behavior you would expect and the actual behavior.
- 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.
- Provide the information you collected in the previous section.

### Suggesting Enhancements

This 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.

#### Before Submitting an Enhancement

- Make sure that you are using the latest version.
- Read the [documentation](https://spicetify.app/docs/getting-started/) carefully and find out if the functionality is already covered, maybe by an individual configuration.
- 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.
- 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.

#### How Do I Submit a Good Enhancement Suggestion?

Enhancement 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).

- Use a **clear and descriptive title** for the issue to identify the suggestion.
- Provide a **step-by-step description of the suggested enhancement** in as many details as possible.
- **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.
- 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.
- **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.

### Your First Code Contribution

#### Requirements

- [Go](https://go.dev/dl/)

#### Environment Setup and Development

Follow the steps outlined in the [documentation](https://spicetify.app/docs/development/compiling) or the steps below.
1. Clone the repository using `git clone https://github.com/spicetify/cli`.
2. Enter the repository directory and build the project.
   * Windows
      ```
      cd cli
      go build -o spicetify.exe
      ```
   * Linux and MacOS
      ```
      cd cli
      go build -o spicetify
      ```
3. Execute the executable file generated by `go build` using `./spicetify` or `./spicetify.exe`.

### Improving The Documentation

To improve the [documentation](https://spicetify.app/docs/getting-started), navigate to the documentation [repository](https://github.com/spicetify/docs).

### Commit Message Format

    <type>(<scope>): <subject>
    <BLANK LINE>
    <body>[optional]

*   **type:** feat | fix | docs | chore | revert
    *   **feat:** A new feature
    *   **fix:** A bug fix
    *   **docs:** Documentation only changes
    *   **chore:** Changes to build process, auxiliary tools, libraries, and other things
    *   **revert:** A reversion to a previous commit
*   **scope:** Anything specifying place of the commit change
*   **subject:** What changes you have done
    *   Use the imperative, present tense: "change" not "changed" nor "changes"
    *   Don't capitalize first letter
    *   No dot (.) at the end
*   **body**: More details of your changes, you can mention the most important changes here
    *   Use the imperative, present tense: "change" not "changed" nor "changes"

If you want to learn more, view the [Angular - Git Commit Guidelines](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines).


================================================
FILE: CustomApps/lyrics-plus/OptionsMenu.js
================================================
const OptionsMenuItemIcon = react.createElement(
	"svg",
	{
		width: 16,
		height: 16,
		viewBox: "0 0 16 16",
		fill: "currentColor",
	},
	react.createElement("path", {
		d: "M13.985 2.383L5.127 12.754 1.388 8.375l-.658.77 4.397 5.149 9.618-11.262z",
	})
);

const OptionsMenuItem = react.memo(({ onSelect, value, isSelected }) => {
	return react.createElement(
		Spicetify.ReactComponent.MenuItem,
		{
			onClick: onSelect,
			icon: isSelected ? OptionsMenuItemIcon : null,
			trailingIcon: isSelected ? OptionsMenuItemIcon : null,
		},
		value
	);
});

const OptionsMenu = react.memo(({ options, onSelect, selected, defaultValue, bold = false }) => {
	/**
	 * <Spicetify.ReactComponent.ContextMenu
	 *      menu = { options.map(a => <OptionsMenuItem>) }
	 * >
	 *      <button>
	 *          <span> {select.value} </span>
	 *          <svg> arrow icon </svg>
	 *      </button>
	 * </Spicetify.ReactComponent.ContextMenu>
	 */
	const menuRef = react.useRef(null);
	return react.createElement(
		Spicetify.ReactComponent.ContextMenu,
		{
			menu: react.createElement(
				Spicetify.ReactComponent.Menu,
				{},
				options.map(({ key, value }) =>
					react.createElement(OptionsMenuItem, {
						value,
						onSelect: () => {
							onSelect(key);
							// Close menu on item click
							menuRef.current?.click();
						},
						isSelected: selected?.key === key,
					})
				)
			),
			trigger: "click",
			action: "toggle",
			renderInline: false,
		},
		react.createElement(
			"button",
			{
				className: "optionsMenu-dropBox",
				ref: menuRef,
			},
			react.createElement(
				"span",
				{
					className: bold ? "main-type-mestoBold" : "main-type-mesto",
				},
				selected?.value || defaultValue
			),
			react.createElement(
				"svg",
				{
					height: "16",
					width: "16",
					fill: "currentColor",
					viewBox: "0 0 16 16",
				},
				react.createElement("path", {
					d: "M3 6l5 5.794L13 6z",
				})
			)
		)
	);
});

function getMusixmatchTranslationPrefix() {
	if (typeof window !== "undefined" && typeof window.__lyricsPlusMusixmatchTranslationPrefix === "string") {
		return window.__lyricsPlusMusixmatchTranslationPrefix;
	}

	return "musixmatchTranslation:";
}

const TranslationMenu = react.memo(({ friendlyLanguage, hasTranslation, musixmatchLanguages, musixmatchSelectedLanguage }) => {
	const musixmatchTranslationPrefix = getMusixmatchTranslationPrefix();

	const [languageMap, setLanguageMap] = react.useState({});

	react.useEffect(() => {
		let cancelled = false;

		if (typeof ProviderMusixmatch !== "undefined" && ProviderMusixmatch && typeof ProviderMusixmatch.getLanguages === "function") {
			(async () => {
				try {
					const languages = await ProviderMusixmatch.getLanguages();
					if (!cancelled) {
						setLanguageMap(languages);
					}
				} catch (error) {
					console.error("Failed to fetch Musixmatch languages:", error);
				}
			})();
		}

		return () => {
			cancelled = true;
		};
	}, []);

	const items = useMemo(() => {
		let sourceOptions = {
			none: "None",
		};

		const translationDisplayOptions = {
			replace: "Replace original",
			below: "Below original",
		};

		const languageOptions = {
			off: "Off",
			"zh-hans": "Chinese (Simplified)",
			"zh-hant": "Chinese (Traditional)",
			ja: "Japanese",
			ko: "Korean",
		};

		let modeOptions = {
			none: "None",
		};

		const musixmatchDisplay = new Intl.DisplayNames(["en"], { type: "language" });
		const availableMusixmatchLanguages = Array.isArray(musixmatchLanguages) ? [...new Set(musixmatchLanguages.filter(Boolean))] : [];
		const activeMusixmatchLanguage = musixmatchSelectedLanguage && musixmatchSelectedLanguage !== "none" ? musixmatchSelectedLanguage : null;
		if (hasTranslation.musixmatch && activeMusixmatchLanguage) {
			availableMusixmatchLanguages.push(activeMusixmatchLanguage);
		}

		if (availableMusixmatchLanguages.length) {
			const musixmatchOptionsArray = availableMusixmatchLanguages.map((code) => {
				let label = "";
				try {
					if (languageMap && languageMap[code]) {
						label = languageMap[code];
					} else {
						label = musixmatchDisplay.of(code) ?? code.toUpperCase();
					}
				} catch (e) {
					label = code.toUpperCase();
				}
				return {
					key: `${musixmatchTranslationPrefix}${code}`,
					label: `${label} (Musixmatch)`,
				};
			});

			musixmatchOptionsArray.sort((a, b) => a.label.localeCompare(b.label));

			const musixmatchOptions = musixmatchOptionsArray.reduce((acc, { key, label }) => {
				acc[key] = label;
				return acc;
			}, {});
			sourceOptions = { ...sourceOptions, ...musixmatchOptions };
		}

		if (hasTranslation.netease) {
			sourceOptions = {
				...sourceOptions,
				neteaseTranslation: "Chinese (Netease)",
			};
		}

		switch (friendlyLanguage) {
			case "japanese": {
				modeOptions = {
					furigana: "Furigana",
					romaji: "Romaji",
					hiragana: "Hiragana",
					katakana: "Katakana",
				};
				break;
			}
			case "korean": {
				modeOptions = {
					romaja: "Romaja",
				};
				break;
			}
			case "chinese": {
				modeOptions = {
					cn: "Simplified Chinese",
					hk: "Traditional Chinese (Hong Kong)",
					tw: "Traditional Chinese (Taiwan)",
				};
				break;
			}
		}

		const configItems = [
			{
				desc: "Translation Provider",
				key: "translate:translated-lyrics-source",
				type: ConfigSelection,
				options: sourceOptions,
				renderInline: true,
			},
			{
				desc: "Translation Display",
				key: "translate:display-mode",
				type: ConfigSelection,
				options: translationDisplayOptions,
				renderInline: true,
			},
			{
				desc: "Language Override",
				key: "translate:detect-language-override",
				type: ConfigSelection,
				options: languageOptions,
				renderInline: true,
				// for songs in languages that support translation but not Convert (e.g., English), the option is disabled.
				when: () => friendlyLanguage,
			},
			{
				desc: "Display Mode",
				key: `translation-mode:${friendlyLanguage}`,
				type: ConfigSelection,
				options: modeOptions,
				renderInline: true,
				// for songs in languages that support translation but not Convert (e.g., English), the option is disabled.
				when: () => friendlyLanguage,
			},
			{
				desc: "Convert",
				key: "translate",
				type: ConfigSlider,
				trigger: "click",
				action: "toggle",
				renderInline: true,
				// for songs in languages that support translation but not Convert (e.g., English), the option is disabled.
				when: () => friendlyLanguage,
			},
		];

		return configItems;
	}, [
		friendlyLanguage,
		hasTranslation.musixmatch,
		hasTranslation.netease,
		Array.isArray(musixmatchLanguages) ? musixmatchLanguages.join(",") : "",
		musixmatchSelectedLanguage || "",
		musixmatchTranslationPrefix,
		languageMap,
	]);

	useEffect(() => {
		// Currently opened Context Menu does not receive prop changes
		// If we were to use keys the Context Menu would close on re-render
		const event = new CustomEvent("lyrics-plus", {
			detail: {
				type: "translation-menu",
				items,
			},
		});
		document.dispatchEvent(event);
	}, [friendlyLanguage, items]);

	return react.createElement(
		Spicetify.ReactComponent.TooltipWrapper,
		{
			label: "Conversion",
		},
		react.createElement(
			"div",
			{
				className: "lyrics-tooltip-wrapper",
			},
			react.createElement(
				Spicetify.ReactComponent.ContextMenu,
				{
					menu: react.createElement(
						Spicetify.ReactComponent.Menu,
						{},
						react.createElement("h3", null, " Conversions"),
						react.createElement(OptionList, {
							type: "translation-menu",
							items,
							onChange: (name, value) => {
								if (name === "translate") {
									CONFIG.visual["translate:translated-lyrics-source"] = "none";
									localStorage.setItem(`${APP_NAME}:visual:translate:translated-lyrics-source`, "none");
								}
								if (name === "translate:translated-lyrics-source") {
									const hasTranslationProvider = typeof value === "string" && value !== "none";
									if (hasTranslationProvider && CONFIG.visual.translate) {
										CONFIG.visual.translate = false;
										localStorage.setItem(`${APP_NAME}:visual:translate`, "false");
									}

									let nextMusixmatchLanguage = "none";
									if (typeof value === "string" && value.startsWith(musixmatchTranslationPrefix)) {
										nextMusixmatchLanguage = value.slice(musixmatchTranslationPrefix.length) || "none";
									}

									if (CONFIG.visual["musixmatch-translation-language"] !== nextMusixmatchLanguage) {
										CONFIG.visual["musixmatch-translation-language"] = nextMusixmatchLanguage;
										localStorage.setItem(`${APP_NAME}:visual:musixmatch-translation-language`, nextMusixmatchLanguage);
									}
								}

								CONFIG.visual[name] = value;
								localStorage.setItem(`${APP_NAME}:visual:${name}`, value);
								lyricContainerUpdate?.();
							},
						})
					),
					trigger: "click",
					action: "toggle",
					renderInline: true,
				},
				react.createElement(
					"button",
					{
						className: "lyrics-config-button",
					},
					react.createElement(
						"p1",
						{
							width: 16,
							height: 16,
							viewBox: "0 0 16 10.3",
							fill: "currentColor",
						},
						"⇄"
					)
				)
			)
		)
	);
});

const AdjustmentsMenu = react.memo(({ mode, hasPerformer }) => {
	return react.createElement(
		Spicetify.ReactComponent.TooltipWrapper,
		{
			label: "Adjustments",
		},
		react.createElement(
			"div",
			{
				className: "lyrics-tooltip-wrapper",
			},
			react.createElement(
				Spicetify.ReactComponent.ContextMenu,
				{
					menu: react.createElement(
						Spicetify.ReactComponent.Menu,
						{},
						react.createElement("h3", null, " Adjustments"),
						react.createElement(OptionList, {
							items: [
								{
									desc: "Font size",
									key: "font-size",
									type: ConfigAdjust,
									min: fontSizeLimit.min,
									max: fontSizeLimit.max,
									step: fontSizeLimit.step,
								},
								{
									desc: "Track delay",
									key: "delay",
									type: ConfigAdjust,
									min: Number.NEGATIVE_INFINITY,
									max: Number.POSITIVE_INFINITY,
									step: 250,
									when: () => mode === SYNCED || mode === KARAOKE,
								},
								{
									desc: "Compact",
									key: "synced-compact",
									type: ConfigSlider,
									when: () => mode === SYNCED || mode === KARAOKE,
								},
								{
									desc: "Show performers",
									key: "show-performers",
									type: ConfigSlider,
									when: () => hasPerformer && (mode === SYNCED || mode === KARAOKE || mode === UNSYNCED),
								},
								{
									desc: "Dual panel",
									key: "dual-genius",
									type: ConfigSlider,
									when: () => mode === GENIUS,
								},
							],
							onChange: (name, value) => {
								CONFIG.visual[name] = value;
								localStorage.setItem(`${APP_NAME}:visual:${name}`, value);
								name === "delay" && localStorage.setItem(`lyrics-delay:${Spicetify.Player.data.item.uri}`, value);
								lyricContainerUpdate?.();
							},
						})
					),
					trigger: "click",
					action: "toggle",
					renderInline: true,
				},
				react.createElement(
					"button",
					{
						className: "lyrics-config-button",
					},
					react.createElement(
						"svg",
						{
							width: 16,
							height: 16,
							viewBox: "0 0 16 10.3",
							fill: "currentColor",
						},
						react.createElement("path", {
							d: "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",
						})
					)
				)
			)
		)
	);
});


================================================
FILE: CustomApps/lyrics-plus/Pages.js
================================================
const CreditFooter = react.memo(({ provider, copyright }) => {
	if (provider === "local") return null;
	const credit = [Spicetify.Locale.get("web-player.lyrics.providedBy", provider)];
	if (copyright) {
		credit.push(...copyright.split("\n"));
	}

	return (
		provider &&
		react.createElement(
			"p",
			{
				className: "lyrics-lyricsContainer-Provider main-type-mesto",
				dir: "auto",
			},
			credit.join(" • ")
		)
	);
});

const IdlingIndicator = ({ isActive, progress, delay }) => {
	return react.createElement(
		"div",
		{
			className: `lyrics-idling-indicator ${
				!isActive ? "lyrics-idling-indicator-hidden" : ""
			} lyrics-lyricsContainer-LyricsLine lyrics-lyricsContainer-LyricsLine-active`,
			style: {
				"--position-index": 0,
				"--animation-index": 1,
				"--indicator-delay": `${delay}ms`,
			},
		},
		react.createElement("div", { className: `lyrics-idling-indicator__circle ${progress >= 0.05 ? "active" : ""}` }),
		react.createElement("div", { className: `lyrics-idling-indicator__circle ${progress >= 0.33 ? "active" : ""}` }),
		react.createElement("div", { className: `lyrics-idling-indicator__circle ${progress >= 0.66 ? "active" : ""}` })
	);
};

const emptyLine = {
	startTime: 0,
	endTime: 0,
	text: [],
};

const useTrackPosition = (callback) => {
	const callbackRef = useRef();
	callbackRef.current = callback;

	useEffect(() => {
		const interval = setInterval(callbackRef.current, 50);

		return () => {
			clearInterval(interval);
		};
	}, [callbackRef]);
};

const KaraokeLine = ({ text, isActive, position, startTime, endTime }) => {
	if (endTime && position > endTime) {
		return text.map(({ word }) => word).join("");
	}

	return text.map(({ word, time }, i) => {
		const isWordActive = position >= startTime;
		startTime += time;
		return react.createElement(
			"span",
			{
				key: i,
				className: `lyrics-lyricsContainer-Karaoke-Word${isWordActive ? " lyrics-lyricsContainer-Karaoke-WordActive" : ""}`,
				style: {
					"--word-duration": `${time}ms`,
					// don't animate unless we have to
					transition: !isWordActive ? "all 0s linear" : "",
				},
			},
			word
		);
	});
};

const SyncedLyricsPage = react.memo(({ lyrics = [], provider, copyright, isKara }) => {
	const [position, setPosition] = useState(0);
	const activeLineEle = useRef();
	const lyricContainerEle = useRef();

	useTrackPosition(() => {
		const newPos = Spicetify.Player.getProgress();
		const delay = CONFIG.visual["global-delay"] + CONFIG.visual.delay;
		if (newPos !== position) {
			setPosition(newPos + delay);
		}
	});

	const lyricWithEmptyLines = useMemo(
		() =>
			[emptyLine, emptyLine, ...lyrics].map((line, i) => ({
				...line,
				lineNumber: i,
			})),
		[lyrics]
	);

	const lyricsId = lyrics[0].text;

	let activeLineIndex = 0;
	for (let i = lyricWithEmptyLines.length - 1; i > 0; i--) {
		if (position >= lyricWithEmptyLines[i].startTime) {
			activeLineIndex = i;
			break;
		}
	}

	const activeLines = useMemo(() => {
		const startIndex = Math.max(activeLineIndex - 1 - CONFIG.visual["lines-before"], 0);
		// 3 lines = 1 padding top + 1 padding bottom + 1 active
		const linesCount = CONFIG.visual["lines-before"] + CONFIG.visual["lines-after"] + 3;
		return lyricWithEmptyLines.slice(startIndex, startIndex + linesCount);
	}, [activeLineIndex, lyricWithEmptyLines]);

	let offset = lyricContainerEle.current ? lyricContainerEle.current.clientHeight / 2 : 0;
	if (activeLineEle.current) {
		offset += -(activeLineEle.current.offsetTop + activeLineEle.current.clientHeight / 2);
	}

	return react.createElement(
		"div",
		{
			className: "lyrics-lyricsContainer-SyncedLyricsPage",
			ref: lyricContainerEle,
		},
		react.createElement(
			"div",
			{
				className: "lyrics-lyricsContainer-SyncedLyrics",
				style: {
					"--offset": `${offset}px`,
				},
				key: lyricsId,
			},
			activeLines.map(({ text, lineNumber, startTime, endTime, originalText, performer }, i) => {
				if (i === 1 && activeLineIndex === 1) {
					return react.createElement(IdlingIndicator, {
						progress: position / activeLines[2].startTime,
						delay: activeLines[2].startTime / 3,
					});
				}

				let className = "lyrics-lyricsContainer-LyricsLine";
				const activeElementIndex = Math.min(activeLineIndex, CONFIG.visual["lines-before"] + 1);
				let ref;

				const isActive = activeElementIndex === i;
				if (isActive) {
					className += " lyrics-lyricsContainer-LyricsLine-active";
					ref = activeLineEle;
				}

				let animationIndex;
				if (activeLineIndex <= CONFIG.visual["lines-before"]) {
					animationIndex = i - activeLineIndex;
				} else {
					animationIndex = i - CONFIG.visual["lines-before"] - 1;
				}

				const paddingLine = (animationIndex < 0 && -animationIndex > CONFIG.visual["lines-before"]) || animationIndex > CONFIG.visual["lines-after"];
				if (paddingLine) {
					className += " lyrics-lyricsContainer-LyricsLine-paddingLine";
				}
				const showTranslatedBelow = CONFIG.visual["translate:display-mode"] === "below";
				// If we have original text and we are showing translated below, we should show the original text
				// Otherwise we should show the translated text
				const lineText = originalText && showTranslatedBelow ? originalText : text;

				// Convert lyrics to text for comparison
				const belowOrigin = (typeof originalText === "object" ? originalText?.props?.children?.[0] : originalText)?.replace(/\s+/g, "");
				const belowTxt = (typeof text === "object" ? text?.props?.children?.[0] : text)?.replace(/\s+/g, "");

				const belowMode = showTranslatedBelow && originalText && belowOrigin !== belowTxt;

				return react.createElement(
					"div",
					{
						className,
						style: {
							cursor: "pointer",
							"--position-index": animationIndex,
							"--animation-index": (animationIndex < 0 ? 0 : animationIndex) + 1,
							"--blur-index": Math.abs(animationIndex),
						},
						dir: "auto",
						ref,
						key: lineNumber,
						onClick: (event) => {
							if (startTime) {
								Spicetify.Player.seek(startTime);
							}
						},
					},
					react.createElement(
						"p",
						{
							onContextMenu: (event) => {
								event.preventDefault();
								Spicetify.Platform.ClipboardAPI.copy(Utils.convertParsedToLRC(lyrics, belowMode).original)
									.then(() => Spicetify.showNotification("Lyrics copied to clipboard"))
									.catch(() => Spicetify.showNotification("Failed to copy lyrics to clipboard"));
							},
						},
						(() => {
							if (!CONFIG.visual["show-performers"] || !performer) return null;

							if (!CONFIG.visual["synced-compact"]) {
								const previousLine = lyricWithEmptyLines[lineNumber - 1];
								if (previousLine && previousLine.performer === performer) return null;
							}

							return react.createElement(
								"span",
								{
									className: "lyrics-lyricsContainer-Performer",
								},
								performer
							);
						})(),
						!isKara ? lineText : react.createElement(KaraokeLine, { text, startTime, endTime, position, isActive })
					),
					belowMode &&
						react.createElement(
							"p",
							{
								style: {
									opacity: 0.5,
								},
								onContextMenu: (event) => {
									event.preventDefault();
									Spicetify.Platform.ClipboardAPI.copy(Utils.convertParsedToLRC(lyrics, belowMode).conver)
										.then(() => Spicetify.showNotification("Translated lyrics copied to clipboard"))
										.catch(() => Spicetify.showNotification("Failed to copy translated lyrics to clipboard"));
								},
							},
							text
						)
				);
			})
		),
		react.createElement(CreditFooter, {
			provider,
			copyright,
		})
	);
});

class SearchBar extends react.Component {
	constructor() {
		super();
		this.state = {
			hidden: true,
			atNode: 0,
			foundNodes: [],
		};
		this.container = null;
	}

	componentDidMount() {
		this.viewPort = document.querySelector(".main-view-container .os-viewport");
		this.mainViewOffsetTop = document.querySelector(".Root__main-view").offsetTop;
		this.toggleCallback = () => {
			if (!(Spicetify.Platform.History.location.pathname === "/lyrics-plus" && this.container)) return;

			if (this.state.hidden) {
				this.setState({ hidden: false });
				this.container.focus();
			} else {
				this.setState({ hidden: true });
				this.container.blur();
			}
		};
		this.unFocusCallback = () => {
			this.container.blur();
			this.setState({ hidden: true });
		};
		this.loopThroughCallback = (event) => {
			if (!this.state.foundNodes.length) {
				return;
			}

			if (event.key === "Enter") {
				const dir = event.shiftKey ? -1 : 1;
				let atNode = this.state.atNode + dir;
				if (atNode < 0) {
					atNode = this.state.foundNodes.length - 1;
				}
				atNode %= this.state.foundNodes.length;
				const rects = this.state.foundNodes[atNode].getBoundingClientRect();
				this.viewPort.scrollBy(0, rects.y - 100);
				this.setState({ atNode });
			}
		};

		Spicetify.Mousetrap().bind("mod+shift+f", this.toggleCallback);
		Spicetify.Mousetrap(this.container).bind("mod+shift+f", this.toggleCallback);
		Spicetify.Mousetrap(this.container).bind("enter", this.loopThroughCallback);
		Spicetify.Mousetrap(this.container).bind("shift+enter", this.loopThroughCallback);
		Spicetify.Mousetrap(this.container).bind("esc", this.unFocusCallback);
	}

	componentWillUnmount() {
		Spicetify.Mousetrap().unbind("mod+shift+f", this.toggleCallback);
		Spicetify.Mousetrap(this.container).unbind("mod+shift+f", this.toggleCallback);
		Spicetify.Mousetrap(this.container).unbind("enter", this.loopThroughCallback);
		Spicetify.Mousetrap(this.container).unbind("shift+enter", this.loopThroughCallback);
		Spicetify.Mousetrap(this.container).unbind("esc", this.unFocusCallback);
	}

	getNodeFromInput(event) {
		const value = event.target.value.toLowerCase();
		if (!value) {
			this.setState({ foundNodes: [] });
			this.viewPort.scrollTo(0, 0);
			return;
		}

		const lyricsPage = document.querySelector(".lyrics-lyricsContainer-UnsyncedLyricsPage");
		const walker = document.createTreeWalker(
			lyricsPage,
			NodeFilter.SHOW_TEXT,
			(node) => {
				if (node.textContent.toLowerCase().includes(value)) {
					return NodeFilter.FILTER_ACCEPT;
				}
				return NodeFilter.FILTER_REJECT;
			},
			false
		);

		const foundNodes = [];
		while (walker.nextNode()) {
			const range = document.createRange();
			range.selectNodeContents(walker.currentNode);
			foundNodes.push(range);
		}

		if (!foundNodes.length) {
			this.viewPort.scrollBy(0, 0);
		} else {
			const rects = foundNodes[0].getBoundingClientRect();
			this.viewPort.scrollBy(0, rects.y - 100);
		}

		this.setState({ foundNodes, atNode: 0 });
	}

	render() {
		let y = 0;
		let height = 0;
		if (this.state.foundNodes.length) {
			const node = this.state.foundNodes[this.state.atNode];
			const rects = node.getBoundingClientRect();
			y = rects.y + this.viewPort.scrollTop - this.mainViewOffsetTop;
			height = rects.height;
		}
		return react.createElement(
			"div",
			{
				className: `lyrics-Searchbar${this.state.hidden ? " hidden" : ""}`,
			},
			react.createElement("input", {
				ref: (c) => {
					this.container = c;
				},
				onChange: this.getNodeFromInput.bind(this),
			}),
			react.createElement("svg", {
				width: 16,
				height: 16,
				viewBox: "0 0 16 16",
				fill: "currentColor",
				dangerouslySetInnerHTML: {
					__html: Spicetify.SVGIcons.search,
				},
			}),
			react.createElement(
				"span",
				{
					hidden: this.state.foundNodes.length === 0,
				},
				`${this.state.atNode + 1}/${this.state.foundNodes.length}`
			),
			react.createElement("div", {
				className: "lyrics-Searchbar-highlight",
				style: {
					"--search-highlight-top": `${y}px`,
					"--search-highlight-height": `${height}px`,
				},
			})
		);
	}
}

function isInViewport(element) {
	const rect = element.getBoundingClientRect();
	return (
		rect.top >= 0 &&
		rect.left >= 0 &&
		rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
		rect.right <= (window.innerWidth || document.documentElement.clientWidth)
	);
}

const SyncedExpandedLyricsPage = react.memo(({ lyrics, provider, copyright, isKara }) => {
	const [position, setPosition] = useState(0);
	const activeLineRef = useRef(null);
	const pageRef = useRef(null);

	useTrackPosition(() => {
		if (!Spicetify.Player.data.is_paused) {
			setPosition(Spicetify.Player.getProgress() + CONFIG.visual["global-delay"] + CONFIG.visual.delay);
		}
	});

	const padded = useMemo(() => [emptyLine, ...lyrics], [lyrics]);

	const intialScroll = useMemo(() => [false], [lyrics]);

	const lyricsId = lyrics[0].text;

	let activeLineIndex = 0;
	for (let i = padded.length - 1; i >= 0; i--) {
		const line = padded[i];
		if (position >= line.startTime) {
			activeLineIndex = i;
			break;
		}
	}

	useEffect(() => {
		if (activeLineRef.current && (!intialScroll[0] || isInViewport(activeLineRef.current))) {
			activeLineRef.current.scrollIntoView({
				behavior: "smooth",
				block: "center",
				inline: "nearest",
			});
			intialScroll[0] = true;
		}
	}, [activeLineRef.current]);

	return react.createElement(
		"div",
		{
			className: "lyrics-lyricsContainer-UnsyncedLyricsPage",
			key: lyricsId,
			ref: pageRef,
		},
		react.createElement("p", {
			className: "lyrics-lyricsContainer-LyricsUnsyncedPadding",
		}),
		padded.map(({ text, startTime, endTime, originalText, performer }, i) => {
			if (i === 0) {
				return react.createElement(IdlingIndicator, {
					isActive: activeLineIndex === 0,
					progress: position / padded[1].startTime,
					delay: padded[1].startTime / 3,
				});
			}

			const isActive = i === activeLineIndex;
			const showTranslatedBelow = CONFIG.visual["translate:display-mode"] === "below";
			// If we have original text and we are showing translated below, we should show the original text
			// Otherwise we should show the translated text
			const lineText = originalText && showTranslatedBelow ? originalText : text;

			// Convert lyrics to text for comparison
			const belowOrigin = (typeof originalText === "object" ? originalText?.props?.children?.[0] : originalText)?.replace(/\s+/g, "");
			const belowTxt = (typeof text === "object" ? text?.props?.children?.[0] : text)?.replace(/\s+/g, "");

			const belowMode = showTranslatedBelow && originalText && belowOrigin !== belowTxt;

			return react.createElement(
				"div",
				{
					className: `lyrics-lyricsContainer-LyricsLine${i <= activeLineIndex ? " lyrics-lyricsContainer-LyricsLine-active" : ""}`,
					key: i,
					style: {
						cursor: "pointer",
					},
					dir: "auto",
					ref: isActive ? activeLineRef : null,
					onClick: (event) => {
						if (startTime) {
							Spicetify.Player.seek(startTime);
						}
					},
				},
				react.createElement(
					"p",
					{
						onContextMenu: (event) => {
							event.preventDefault();
							Spicetify.Platform.ClipboardAPI.copy(Utils.convertParsedToLRC(lyrics, belowMode).original)
								.then(() => Spicetify.showNotification("Lyrics copied to clipboard"))
								.catch(() => Spicetify.showNotification("Failed to copy lyrics to clipboard"));
						},
					},
					(() => {
						if (!CONFIG.visual["show-performers"] || !performer) return null;

						if (!CONFIG.visual["synced-compact"]) {
							const previousLine = padded[i - 1];
							if (previousLine && previousLine.performer === performer) return null;
						}

						return react.createElement(
							"span",
							{
								className: "lyrics-lyricsContainer-Performer",
							},
							performer
						);
					})(),
					!isKara ? lineText : react.createElement(KaraokeLine, { text, startTime, endTime, position, isActive })
				),
				belowMode &&
					react.createElement(
						"p",
						{
							style: { opacity: 0.5 },
							onContextMenu: (event) => {
								event.preventDefault();
								Spicetify.Platform.ClipboardAPI.copy(Utils.convertParsedToLRC(lyrics, belowMode).conver)
									.then(() => Spicetify.showNotification("Translated lyrics copied to clipboard"))
									.catch(() => Spicetify.showNotification("Failed to copy translated lyrics to clipboard"));
							},
						},
						text
					)
			);
		}),
		react.createElement("p", {
			className: "lyrics-lyricsContainer-LyricsUnsyncedPadding",
		}),
		react.createElement(CreditFooter, {
			provider,
			copyright,
		}),
		react.createElement(SearchBar, null)
	);
});

const UnsyncedLyricsPage = react.memo(({ lyrics, provider, copyright }) => {
	return react.createElement(
		"div",
		{
			className: "lyrics-lyricsContainer-UnsyncedLyricsPage",
		},
		react.createElement("p", {
			className: "lyrics-lyricsContainer-LyricsUnsyncedPadding",
		}),
		lyrics.map(({ text, originalText, performer }, index) => {
			const showTranslatedBelow = CONFIG.visual["translate:display-mode"] === "below";
			// If we have original text and we are showing translated below, we should show the original text
			// Otherwise we should show the translated text
			const lineText = originalText && showTranslatedBelow ? originalText : text;

			// Convert lyrics to text for comparison
			const belowOrigin = (typeof originalText === "object" ? originalText?.props?.children?.[0] : originalText)?.replace(/\s+/g, "");
			const belowTxt = (typeof text === "object" ? text?.props?.children?.[0] : text)?.replace(/\s+/g, "");

			const belowMode = showTranslatedBelow && originalText && belowOrigin !== belowTxt;

			return react.createElement(
				"div",
				{
					className: "lyrics-lyricsContainer-LyricsLine lyrics-lyricsContainer-LyricsLine-active",
					key: index,
					dir: "auto",
				},
				react.createElement(
					"p",
					{
						onContextMenu: (event) => {
							event.preventDefault();
							Spicetify.Platform.ClipboardAPI.copy(Utils.convertParsedToUnsynced(lyrics, belowMode).original)
								.then(() => Spicetify.showNotification("Lyrics copied to clipboard"))
								.catch(() => Spicetify.showNotification("Failed to copy lyrics to clipboard"));
						},
					},
					(() => {
						if (!CONFIG.visual["show-performers"] || !performer) return null;

						const previousLine = lyrics[index - 1];
						if (previousLine && previousLine.performer === performer) return null;

						return react.createElement(
							"span",
							{
								className: "lyrics-lyricsContainer-Performer",
							},
							performer
						);
					})(),
					lineText
				),
				belowMode &&
					react.createElement(
						"p",
						{
							style: { opacity: 0.5 },
							onContextMenu: (event) => {
								event.preventDefault();
								Spicetify.Platform.ClipboardAPI.copy(Utils.convertParsedToUnsynced(lyrics, belowMode).conver)
									.then(() => Spicetify.showNotification("Translated lyrics copied to clipboard"))
									.catch(() => Spicetify.showNotification("Failed to copy translated lyrics to clipboard"));
							},
						},
						text
					)
			);
		}),
		react.createElement("p", {
			className: "lyrics-lyricsContainer-LyricsUnsyncedPadding",
		}),
		react.createElement(CreditFooter, {
			provider,
			copyright,
		}),
		react.createElement(SearchBar, null)
	);
});

const noteContainer = document.createElement("div");
noteContainer.classList.add("lyrics-Genius-noteContainer");
const noteDivider = document.createElement("div");
noteDivider.classList.add("lyrics-Genius-divider");
noteDivider.innerHTML = `<svg width="32" height="32" viewBox="0 0 13 4" fill="currentColor"><path d=\"M13 10L8 4.206 3 10z\"/></svg>`;
noteDivider.style.setProperty("--link-left", 0);
const noteTextContainer = document.createElement("div");
noteTextContainer.classList.add("lyrics-Genius-noteTextContainer");
noteTextContainer.onclick = (event) => {
	event.preventDefault();
	event.stopPropagation();
};
noteContainer.append(noteDivider, noteTextContainer);

function showNote(parent, note) {
	if (noteContainer.parentElement === parent) {
		noteContainer.remove();
		return;
	}
	noteTextContainer.innerText = note;
	parent.append(noteContainer);
	const arrowPos = parent.offsetLeft - noteContainer.offsetLeft;
	noteDivider.style.setProperty("--link-left", `${arrowPos}px`);
	const box = noteTextContainer.getBoundingClientRect();
	if (box.y + box.height > window.innerHeight) {
		// Wait for noteContainer is mounted
		setTimeout(() => {
			noteContainer.scrollIntoView({
				behavior: "smooth",
				block: "center",
				inline: "nearest",
			});
		}, 50);
	}
}

const GeniusPage = react.memo(
	({ lyrics, provider, copyright, versions, versionIndex, onVersionChange, isSplitted, lyrics2, versionIndex2, onVersionChange2 }) => {
		let notes = {};
		let container = null;
		let container2 = null;

		// Fetch notes
		useEffect(() => {
			if (!container) return;
			notes = {};
			let links = container.querySelectorAll("a");
			if (isSplitted && container2) {
				links = [...links, ...container2.querySelectorAll("a")];
			}
			for (const link of links) {
				let id = link.pathname.match(/\/(\d+)\//);
				if (!id) {
					id = link.dataset.id;
				} else {
					id = id[1];
				}
				ProviderGenius.getNote(id).then((note) => {
					notes[id] = note;
					link.classList.add("fetched");
				});
				link.onclick = (event) => {
					event.preventDefault();
					if (!notes[id]) return;
					showNote(link, notes[id]);
				};
			}
		}, [lyrics, lyrics2]);

		const lyricsEl1 = react.createElement(
			"div",
			null,
			react.createElement(VersionSelector, { items: versions, index: versionIndex, callback: onVersionChange }),
			react.createElement("div", {
				className: "lyrics-lyricsContainer-LyricsLine lyrics-lyricsContainer-LyricsLine-active",
				ref: (c) => {
					container = c;
				},
				dangerouslySetInnerHTML: {
					__html: lyrics,
				},
				onContextMenu: (event) => {
					event.preventDefault();
					const copylyrics = lyrics.replace(/<br>/g, "\n").replace(/<[^>]*>/g, "");
					Spicetify.Platform.ClipboardAPI.copy(copylyrics)
						.then(() => Spicetify.showNotification("Lyrics copied to clipboard"))
						.catch(() => Spicetify.showNotification("Failed to copy lyrics to clipboard"));
				},
			})
		);

		const mainContainer = [lyricsEl1];
		const shouldSplit = versions.length > 1 && isSplitted;

		if (shouldSplit) {
			const lyricsEl2 = react.createElement(
				"div",
				null,
				react.createElement(VersionSelector, { items: versions, index: versionIndex2, callback: onVersionChange2 }),
				react.createElement("div", {
					className: "lyrics-lyricsContainer-LyricsLine lyrics-lyricsContainer-LyricsLine-active",
					ref: (c) => {
						container2 = c;
					},
					dangerouslySetInnerHTML: {
						__html: lyrics2,
					},
					onContextMenu: (event) => {
						event.preventDefault();
						const copylyrics = lyrics.replace(/<br>/g, "\n").replace(/<[^>]*>/g, "");
						Spicetify.Platform.ClipboardAPI.copy(copylyrics)
							.then(() => Spicetify.showNotification("Lyrics copied to clipboard"))
							.catch(() => Spicetify.showNotification("Failed to copy lyrics to clipboard"));
					},
				})
			);
			mainContainer.push(lyricsEl2);
		}

		return react.createElement(
			"div",
			{
				className: "lyrics-lyricsContainer-UnsyncedLyricsPage",
			},
			react.createElement("p", {
				className: "lyrics-lyricsContainer-LyricsUnsyncedPadding main-type-ballad",
			}),
			react.createElement("div", { className: shouldSplit ? "split" : "" }, mainContainer),
			react.createElement(CreditFooter, {
				provider,
				copyright,
			}),
			react.createElement(SearchBar, null)
		);
	}
);

const LoadingIcon = react.createElement(
	"svg",
	{
		width: "200px",
		height: "200px",
		viewBox: "0 0 100 100",
		preserveAspectRatio: "xMidYMid",
	},
	react.createElement(
		"circle",
		{
			cx: "50",
			cy: "50",
			r: "0",
			fill: "none",
			stroke: "currentColor",
			"stroke-width": "2",
		},
		react.createElement("animate", {
			attributeName: "r",
			repeatCount: "indefinite",
			dur: "1s",
			values: "0;40",
			keyTimes: "0;1",
			keySplines: "0 0.2 0.8 1",
			calcMode: "spline",
			begin: "0s",
		}),
		react.createElement("animate", {
			attributeName: "opacity",
			repeatCount: "indefinite",
			dur: "1s",
			values: "1;0",
			keyTimes: "0;1",
			keySplines: "0.2 0 0.8 1",
			calcMode: "spline",
			begin: "0s",
		})
	),
	react.createElement(
		"circle",
		{
			cx: "50",
			cy: "50",
			r: "0",
			fill: "none",
			stroke: "currentColor",
			"stroke-width": "2",
		},
		react.createElement("animate", {
			attributeName: "r",
			repeatCount: "indefinite",
			dur: "1s",
			values: "0;40",
			keyTimes: "0;1",
			keySplines: "0 0.2 0.8 1",
			calcMode: "spline",
			begin: "-0.5s",
		}),
		react.createElement("animate", {
			attributeName: "opacity",
			repeatCount: "indefinite",
			dur: "1s",
			values: "1;0",
			keyTimes: "0;1",
			keySplines: "0.2 0 0.8 1",
			calcMode: "spline",
			begin: "-0.5s",
		})
	)
);

const VersionSelector = react.memo(({ items, index, callback }) => {
	if (items.length < 2) {
		return null;
	}
	return react.createElement(
		"div",
		{
			className: "lyrics-versionSelector",
		},
		react.createElement(
			"select",
			{
				onChange: (event) => {
					callback(items, event.target.value);
				},
				value: index,
			},
			items.map((a, i) => {
				return react.createElement("option", { value: i }, a.title);
			})
		),
		react.createElement(
			"svg",
			{
				height: "16",
				width: "16",
				fill: "currentColor",
				viewBox: "0 0 16 16",
			},
			react.createElement("path", {
				d: "M3 6l5 5.794L13 6z",
			})
		)
	);
});


================================================
FILE: CustomApps/lyrics-plus/PlaybarButton.js
================================================
(function PlaybarButton() {
	if (!Spicetify.Platform.History) {
		setTimeout(PlaybarButton, 300);
		return;
	}

	const button = new Spicetify.Playbar.Button(
		"Lyrics Plus",
		`<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>`,
		() =>
			Spicetify.Platform.History.location.pathname !== "/lyrics-plus"
				? Spicetify.Platform.History.push("/lyrics-plus")
				: Spicetify.Platform.History.goBack(),
		false,
		Spicetify.Platform.History.location.pathname === "/lyrics-plus",
		false
	);

	const style = document.createElement("style");
	style.innerHTML = `
		.main-nowPlayingBar-lyricsButton[data-testid="lyrics-button"] {
			display: none !important;
		}
		li[data-id="/lyrics-plus"] {
			display: none;
		}
	`;
	style.classList.add("lyrics-plus:visual:playbar-button");

	if (Spicetify.LocalStorage.get("lyrics-plus:visual:playbar-button") === "true") setPlaybarButton();
	window.addEventListener("lyrics-plus", (event) => {
		if (event.detail?.name === "playbar-button") event.detail.value ? setPlaybarButton() : removePlaybarButton();
	});

	Spicetify.Platform.History.listen((location) => {
		button.active = location.pathname === "/lyrics-plus";
	});

	function setPlaybarButton() {
		document.head.appendChild(style);
		button.register();
	}

	function removePlaybarButton() {
		style.remove();
		button.deregister();
	}
})();


================================================
FILE: CustomApps/lyrics-plus/ProviderGenius.js
================================================
const ProviderGenius = (() => {
	function getChildDeep(parent, isDeep = false) {
		let acc = "";

		if (!parent.children) {
			return acc;
		}

		for (const child of parent.children) {
			if (typeof child === "string") {
				acc += child;
			} else if (child.children) {
				acc += getChildDeep(child, true);
			}
			if (!isDeep) {
				acc += "\n";
			}
		}
		return acc.trim();
	}

	async function getNote(id) {
		const body = await Spicetify.CosmosAsync.get(`https://genius.com/api/annotations/${id}`);
		const response = body.response;
		let note = "";

		// Authors annotations
		if (response.referent && response.referent.classification === "verified") {
			const referentsBody = await Spicetify.CosmosAsync.get(`https://genius.com/api/referents/${id}`);
			const referents = referentsBody.response;
			for (const ref of referents.referent.annotations) {
				note += getChildDeep(ref.body.dom);
			}
		}

		// Users annotations
		if (!note && response.annotation) {
			note = getChildDeep(response.annotation.body.dom);
		}

		// Users comments
		if (!note && response.annotation && response.annotation.top_comment) {
			note += getChildDeep(response.annotation.top_comment.body.dom);
		}
		note = note.replace(/\n\n\n?/, "\n");

		return note;
	}

	function fetchHTML(url) {
		return new Promise((resolve, reject) => {
			const request = JSON.stringify({
				method: "GET",
				uri: url,
			});

			window.sendCosmosRequest({
				request,
				persistent: false,
				onSuccess: resolve,
				onFailure: reject,
			});
		});
	}

	async function fetchLyricsVersion(results, index) {
		const result = results[index];
		if (!result) {
			console.warn(result);
			return;
		}

		const site = await fetchHTML(result.url);
		const body = JSON.parse(site)?.body;
		if (!body) {
			return null;
		}

		let lyrics = "";
		const parser = new DOMParser();
		const htmlDoc = parser.parseFromString(body, "text/html");
		const lyricsDiv = htmlDoc.querySelectorAll('div[data-lyrics-container="true"]');

		for (const i of lyricsDiv) {
			lyrics += `${i.innerHTML}<br>`;
		}

		if (!lyrics?.length) {
			console.warn("forceError");
			return null;
		}

		return lyrics;
	}

	async function fetchLyrics(info) {
		const titles = new Set([info.title]);

		const titleNoExtra = Utils.removeExtraInfo(info.title);
		titles.add(titleNoExtra);
		titles.add(Utils.removeSongFeat(info.title));
		titles.add(Utils.removeSongFeat(titleNoExtra));

		let lyrics;
		let hits;
		for (const title of titles) {
			const query = new URLSearchParams({ per_page: 20, q: `${info.artist} ${title}` });
			const url = `https://genius.com/api/search/song?${query.toString()}`;

			const geniusSearch = await Spicetify.CosmosAsync.get(url);

			hits = geniusSearch.response.sections[0].hits.map((item) => ({
				title: item.result.full_title,
				url: item.result.url,
			}));

			if (!hits.length) {
				continue;
			}

			lyrics = await fetchLyricsVersion(hits, 0);
			break;
		}

		if (!lyrics) {
			return { lyrics: null, versions: [] };
		}

		return { lyrics, versions: hits };
	}

	return { fetchLyrics, getNote, fetchLyricsVersion };
})();


================================================
FILE: CustomApps/lyrics-plus/ProviderLRCLIB.js
================================================
const ProviderLRCLIB = (() => {
	async function findLyrics(info) {
		const baseURL = "https://lrclib.net/api/get";
		const durr = info.duration / 1000;
		const params = {
			track_name: info.title,
			artist_name: info.artist,
			album_name: info.album,
			duration: durr,
		};

		const finalURL = `${baseURL}?${Object.keys(params)
			.map((key) => `${key}=${encodeURIComponent(params[key])}`)
			.join("&")}`;

		const body = await fetch(finalURL, {
			headers: {
				"x-user-agent": `spicetify v${Spicetify.Config.version} (https://github.com/spicetify/cli)`,
			},
		});

		if (body.status !== 200) {
			return {
				error: "Request error: Track wasn't found",
				uri: info.uri,
			};
		}

		return await body.json();
	}

	function getUnsynced(body) {
		const unsyncedLyrics = body?.plainLyrics;
		const isInstrumental = body.instrumental;
		if (isInstrumental) return [{ text: "♪ Instrumental ♪" }];

		if (!unsyncedLyrics) return null;

		return Utils.parseLocalLyrics(unsyncedLyrics).unsynced;
	}

	function getSynced(body) {
		const syncedLyrics = body?.syncedLyrics;
		const isInstrumental = body.instrumental;
		if (isInstrumental) return [{ text: "♪ Instrumental ♪" }];

		if (!syncedLyrics) return null;

		return Utils.parseLocalLyrics(syncedLyrics).synced;
	}

	return { findLyrics, getSynced, getUnsynced };
})();


================================================
FILE: CustomApps/lyrics-plus/ProviderMusixmatch.js
================================================
const ProviderMusixmatch = (() => {
	const headers = {
		authority: "apic-desktop.musixmatch.com",
		cookie: "x-mxm-token-guid=",
	};

	function findTranslationStatus(body) {
		if (!body || typeof body !== "object") {
			return null;
		}

		if (Array.isArray(body)) {
			for (const item of body) {
				const result = findTranslationStatus(item);
				if (result) {
					return result;
				}
			}

			return null;
		}

		if (Array.isArray(body.track_lyrics_translation_status)) {
			return body.track_lyrics_translation_status;
		}

		for (const value of Object.values(body)) {
			const result = findTranslationStatus(value);
			if (result) {
				return result;
			}
		}

		return null;
	}

	async function findLyrics(info) {
		const baseURL =
			"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&";

		const durr = info.duration / 1000;

		const params = {
			q_album: info.album,
			q_artist: info.artist,
			q_artists: info.artist,
			q_track: info.title,
			track_spotify_id: info.uri,
			q_duration: durr,
			f_subtitle_length: Math.floor(durr),
			usertoken: CONFIG.providers.musixmatch.token,
			part: "track_lyrics_translation_status,track_structure,track_performer_tagging",
		};

		const finalURL =
			baseURL +
			Object.keys(params)
				.map((key) => `${key}=${encodeURIComponent(params[key])}`)
				.join("&");

		let body = await Spicetify.CosmosAsync.get(finalURL, null, headers);

		body = body.message.body.macro_calls;

		if (body["matcher.track.get"].message.header.status_code !== 200) {
			return {
				error: `Requested error: ${body["matcher.track.get"].message.header.mode}`,
				uri: info.uri,
			};
		}
		if (body["track.lyrics.get"]?.message?.body?.lyrics?.restricted) {
			return {
				error: "Unfortunately we're not authorized to show these lyrics.",
				uri: info.uri,
			};
		}

		const translationStatus = findTranslationStatus(body);
		const meta = body?.["matcher.track.get"]?.message?.body;
		const availableTranslations = Array.isArray(translationStatus) ? [...new Set(translationStatus.map((status) => status?.to).filter(Boolean))] : [];

		Object.defineProperties(body, {
			__musixmatchTranslationStatus: {
				value: availableTranslations,
			},
			__musixmatchTrackId: {
				value: meta?.track?.track_id ?? null,
			},
		});

		return body;
	}

	function parsePerformerData(meta) {
		if (!meta || !meta.track || !meta.track.performer_tagging) {
			return [];
		}

		const tagging = meta.track.performer_tagging;
		const miscTags = meta.track.performer_tagging_misc_tags || {};
		let performerMap = [];
		if (tagging && tagging.content && tagging.content.length > 0) {
			const resources = tagging.resources?.artists || [];
			const resourcesList = Array.isArray(resources) ? resources : Object.values(resources);

			performerMap = tagging.content
				.map((c) => {
					if (!c.performers || c.performers.length === 0) return null;

					const resolvedPerformers = c.performers
						.map((p) => {
							let name = "Unknown";
							if (p.type === "artist") {
								const fqid = p.fqid;
								const idFromFqid = fqid ? parseInt(fqid.split(":")[2]) : null;

								const artist = resourcesList.find((r) => r.artist_id === idFromFqid);
								if (artist) name = artist.artist_name;
							} else if (miscTags[p.type]) {
								name = miscTags[p.type];
							}
							return {
								fqid: p.fqid,
								artist_id: p.fqid ? parseInt(p.fqid.split(":")[2]) : null,
								name: name,
							};
						})
						.filter((p) => p.name !== "Unknown");

					const names = resolvedPerformers.map((p) => p.name);
					if (names.length === 0) return null;

					return {
						name: names.join(", "),
						snippet: c.snippet,
						performers: resolvedPerformers,
					};
				})
				.filter(Boolean);
		}

		const normalizeForMatch = (text) => text.replace(/\s+/g, "").toLowerCase();

		const snippetQueue = [];
		if (performerMap.length > 0) {
			for (const tag of performerMap) {
				if (!tag.snippet) continue;
				const snippetLines = tag.snippet
					.split(/\n+/)
					.map((s) => s.trim())
					.filter(Boolean);
				for (const sLine of snippetLines) {
					if (sLine.length < 2 && !/^[\u3131-\uD79D]/.test(sLine)) continue;
					snippetQueue.push({
						text: normalizeForMatch(sLine),
						raw: sLine,
						performers: tag.performers,
					});
				}
			}
		}
		return snippetQueue;
	}

	function matchSequential(lyricsLines, snippetQueue, getTextCallback = (l) => l.text) {
		if (!snippetQueue || snippetQueue.length === 0) return lyricsLines;

		const normalizeForMatch = (text) => text.replace(/\s+/g, "").toLowerCase();
		let queueCursor = 0;
		const LOOKAHEAD = 5;

		return lyricsLines.map((line) => {
			const lineText = getTextCallback(line) || "♪";
			let normalizedLine = normalizeForMatch(lineText);

			let matchedPerformers = [];

			while (queueCursor < snippetQueue.length) {
				let matchFoundAtOffset = -1;

				for (let i = 0; i < LOOKAHEAD && queueCursor + i < snippetQueue.length; i++) {
					const snippet = snippetQueue[queueCursor + i];

					if (normalizedLine.includes(snippet.text) && snippet.text.length > 0) {
						matchFoundAtOffset = i;
						break;
					}
				}

				if (matchFoundAtOffset !== -1) {
					queueCursor += matchFoundAtOffset;
					const matchedSnippet = snippetQueue[queueCursor];
					matchedPerformers.push(...matchedSnippet.performers);
					normalizedLine = normalizedLine.replace(matchedSnippet.text, "");
					queueCursor++;
				} else {
					break;
				}
			}

			const uniquePerformers = [];
			const sawMap = new Set();
			for (const p of matchedPerformers) {
				const key = p.fqid || p.name;
				if (!sawMap.has(key)) {
					sawMap.add(key);
					uniquePerformers.push(p);
				}
			}

			return {
				...line,
				performers: uniquePerformers,
			};
		});
	}

	async function getKaraoke(body) {
		const meta = body?.["matcher.track.get"]?.message?.body;
		if (!meta) {
			return null;
		}

		if (!meta.track.has_richsync || meta.track.instrumental) {
			return null;
		}

		const baseURL = "https://apic-desktop.musixmatch.com/ws/1.1/track.richsync.get?format=json&subtitle_format=mxm&app_id=web-desktop-app-v1.0&";

		const params = {
			f_subtitle_length: meta.track.track_length,
			q_duration: meta.track.track_length,
			commontrack_id: meta.track.commontrack_id,
			usertoken: CONFIG.providers.musixmatch.token,
		};

		const finalURL =
			baseURL +
			Object.keys(params)
				.map((key) => `${key}=${encodeURIComponent(params[key])}`)
				.join("&");

		let result = await Spicetify.CosmosAsync.get(finalURL, null, headers);

		if (result.message.header.status_code !== 200) {
			return null;
		}

		result = result.message.body;

		const snippetQueue = parsePerformerData(meta);

		const parsedKaraoke = JSON.parse(result.richsync.richsync_body).map((line) => {
			const startTime = line.ts * 1000;
			const endTime = line.te * 1000;
			const words = line.l;

			const text = words.map((word, index, words) => {
				const wordText = word.c;
				const wordStartTime = word.o * 1000;
				const nextWordStartTime = words[index + 1]?.o * 1000;

				const time = !Number.isNaN(nextWordStartTime) ? nextWordStartTime - wordStartTime : endTime - (wordStartTime + startTime);

				return {
					word: wordText,
					time,
				};
			});
			return {
				startTime,
				endTime,
				text,
			};
		});

		return matchSequential(parsedKaraoke, snippetQueue, (line) => {
			if (Array.isArray(line.text)) {
				return line.text.map((t) => t.word).join("");
			}
			return line.text;
		}).map((line) => {
			const performerNames = (line.performers || [])
				.map((p) => p.name)
				.filter(Boolean)
				.join(", ");
			return {
				...line,
				performer: performerNames || null,
			};
		});
	}

	function getSynced(body) {
		const meta = body?.["matcher.track.get"]?.message?.body;
		if (!meta) {
			return null;
		}

		const hasSynced = meta?.track?.has_subtitles;

		const isInstrumental = meta?.track?.instrumental;

		if (isInstrumental) {
			return [{ text: "♪ Instrumental ♪", startTime: "0000" }];
		}
		if (hasSynced) {
			const subtitle = body["track.subtitles.get"]?.message?.body?.subtitle_list?.[0]?.subtitle;
			if (!subtitle) {
				return null;
			}

			const snippetQueue = parsePerformerData(meta);
			const rawLines = JSON.parse(subtitle.subtitle_body);

			return matchSequential(rawLines, snippetQueue, (l) => l.text).map((line) => {
				const lineText = line.text || "♪";
				const performerNames = (line.performers || [])
					.map((p) => p.name)
					.filter(Boolean)
					.join(", ");

				return {
					text: lineText,
					startTime: line.time.total * 1000,
					performer: performerNames || null,
				};
			});
		}

		return null;
	}

	function getUnsynced(body) {
		const meta = body?.["matcher.track.get"]?.message?.body;
		if (!meta) {
			return null;
		}

		const hasUnSynced = meta.track.has_lyrics || meta.track.has_lyrics_crowd;

		const isInstrumental = meta?.track?.instrumental;

		if (isInstrumental) {
			return [{ text: "♪ Instrumental ♪" }];
		}
		if (hasUnSynced) {
			const lyrics = body["track.lyrics.get"]?.message?.body?.lyrics?.lyrics_body;
			if (!lyrics) {
				return null;
			}

			const snippetQueue = parsePerformerData(meta);
			const rawLines = lyrics.split("\n").map((text) => ({ text }));

			return matchSequential(rawLines, snippetQueue, (l) => l.text).map((line) => {
				const performerNames = (line.performers || [])
					.map((p) => p.name)
					.filter(Boolean)
					.join(", ");

				return {
					...line,
					performer: performerNames || null,
				};
			});
		}

		return null;
	}

	async function getTranslation(trackId) {
		if (!trackId) return null;

		const selectedLanguage = CONFIG.visual["musixmatch-translation-language"] || "none";
		if (selectedLanguage === "none") return null;

		const baseURL =
			"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&";

		const params = {
			track_id: trackId,
			selected_language: selectedLanguage,
			usertoken: CONFIG.providers.musixmatch.token,
		};

		const finalURL =
			baseURL +
			Object.keys(params)
				.map((key) => `${key}=${encodeURIComponent(params[key])}`)
				.join("&");

		let result = await Spicetify.CosmosAsync.get(finalURL, null, headers);

		if (result.message.header.status_code !== 200) return null;

		result = result.message.body;

		if (!result.translations_list?.length) return null;

		return result.translations_list.map(({ translation }) => ({
			translation: translation.description,
			matchedLine: translation.matched_line,
		}));
	}

	let languageMap = null;
	async function getLanguages() {
		if (languageMap) return languageMap;

		try {
			const cached = localStorage.getItem("lyrics-plus:musixmatch-languages");
			if (cached) {
				const tempMap = JSON.parse(cached);
				// Check cache version
				if (tempMap.__version === 1) {
					delete tempMap.__version;
					languageMap = tempMap;
					return languageMap;
				}
			}
		} catch (e) {
			console.warn("Failed to parse cached languages", e);
		}

		const baseURL = "https://apic-desktop.musixmatch.com/ws/1.1/languages.get?app_id=web-desktop-app-v1.0&get_romanized_info=1&";

		const params = {
			usertoken: CONFIG.providers.musixmatch.token,
		};

		const finalURL =
			baseURL +
			Object.keys(params)
				.map((key) => `${key}=${encodeURIComponent(params[key])}`)
				.join("&");

		try {
			let body = await Spicetify.CosmosAsync.get(finalURL, null, headers);
			if (body?.message?.body?.language_list) {
				languageMap = {};
				body.message.body.language_list.forEach((item) => {
					const lang = item.language;
					if (lang.language_name) {
						const name = lang.language_name.charAt(0).toUpperCase() + lang.language_name.slice(1);
						if (lang.language_iso_code_1) languageMap[lang.language_iso_code_1] = name;
						if (lang.language_iso_code_3) languageMap[lang.language_iso_code_3] = name;
					}
				});
				localStorage.setItem("lyrics-plus:musixmatch-languages", JSON.stringify({ ...languageMap, __version: 1 }));
				return languageMap;
			}
		} catch (e) {
			console.error("Failed to fetch languages", e);
		}
		return {};
	}

	return { findLyrics, getKaraoke, getSynced, getUnsynced, getTranslation, getLanguages };
})();


================================================
FILE: CustomApps/lyrics-plus/ProviderNetease.js
================================================
const ProviderNetease = (() => {
	const requestHeader = {
		"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0",
	};

	async function findLyrics(info) {
		const searchURL = "https://music.xianqiao.wang/neteaseapiv2/search?limit=10&type=1&keywords=";
		const lyricURL = "https://music.xianqiao.wang/neteaseapiv2/lyric?id=";

		const cleanTitle = Utils.removeExtraInfo(Utils.removeSongFeat(Utils.normalize(info.title)));
		const finalURL = searchURL + encodeURIComponent(`${cleanTitle} ${info.artist}`);

		const searchResults = await Spicetify.CosmosAsync.get(finalURL, null, requestHeader);
		const items = searchResults.result.songs;
		if (!items?.length) {
			throw "Cannot find track";
		}

		// normalized expected album name
		const neAlbumName = Utils.normalize(info.album);
		const expectedAlbumName = Utils.containsHanCharacter(neAlbumName) ? await Utils.toSimplifiedChinese(neAlbumName) : neAlbumName;
		let itemId = items.findIndex((val) => Utils.normalize(val.album.name) === expectedAlbumName);
		if (itemId === -1) itemId = items.findIndex((val) => Math.abs(info.duration - val.duration) < 3000);
		if (itemId === -1) itemId = items.findIndex((val) => val.name === cleanTitle);
		if (itemId === -1) throw "Cannot find track";

		return await Spicetify.CosmosAsync.get(lyricURL + items[itemId].id, null, requestHeader);
	}

	const creditInfo = [
		"\\s?作?\\s*词|\\s?作?\\s*曲|\\s?编\\s*曲?|\\s?监\\s*制?",
		".*编写|.*和音|.*和声|.*合声|.*提琴|.*录|.*工程|.*工作室|.*设计|.*剪辑|.*制作|.*发行|.*出品|.*后期|.*混音|.*缩混",
		"原唱|翻唱|题字|文案|海报|古筝|二胡|钢琴|吉他|贝斯|笛子|鼓|弦乐",
		"lrc|publish|vocal|guitar|program|produce|write|mix",
	];
	const creditInfoRegExp = new RegExp(`^(${creditInfo.join("|")}).*(:|:)`, "i");

	function containCredits(text) {
		return creditInfoRegExp.test(text);
	}

	function parseTimestamp(line) {
		// ["[ar:Beyond]"]
		// ["[03:10]"]
		// ["[03:10]", "lyrics"]
		// ["lyrics"]
		// ["[03:10]", "[03:10]", "lyrics"]
		// ["[1235,300]", "lyrics"]
		const matchResult = line.match(/(\[.*?\])|([^[\]]+)/g);
		if (!matchResult?.length || matchResult.length === 1) {
			return { text: line };
		}

		const textIndex = matchResult.findIndex((slice) => !slice.endsWith("]"));
		let text = "";

		if (textIndex > -1) {
			text = matchResult.splice(textIndex, 1)[0];
			text = Utils.capitalize(Utils.normalize(text, false));
		}

		const time = matchResult[0].replace("[", "").replace("]", "");

		return { time, text };
	}

	function breakdownLine(text) {
		// (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)
		const components = text.split(/\(\d+,(\d+)\)/g);
		// ["", "508", "Don't", "1", " ", "151", "want" , "1" ...]
		const result = [];
		for (let i = 1; i < components.length; i += 2) {
			if (components[i + 1] === " ") continue;
			result.push({
				word: `${components[i + 1]} `,
				time: Number.parseInt(components[i]),
			});
		}
		return result;
	}

	function getKaraoke(list) {
		const lyricStr = list?.klyric?.lyric;

		if (!lyricStr) {
			return null;
		}

		const lines = lyricStr.split(/\r?\n/).map((line) => line.trim());
		const karaoke = lines
			.map((line) => {
				const { time, text } = parseTimestamp(line);
				if (!time || !text) return null;

				const [key, value] = time.split(",") || [];
				const [start, durr] = [Number.parseFloat(key), Number.parseFloat(value)];

				if (!Number.isNaN(start) && !Number.isNaN(durr) && !containCredits(text)) {
					return {
						startTime: start,
						// endTime: start + durr,
						text: breakdownLine(text),
					};
				}
				return null;
			})
			.filter(Boolean);

		if (!karaoke.length) {
			return null;
		}

		return karaoke;
	}

	function getSynced(list) {
		const lyricStr = list?.lrc?.lyric;
		let noLyrics = false;

		if (!lyricStr) {
			return null;
		}

		const lines = lyricStr.split(/\r?\n/).map((line) => line.trim());
		const lyrics = lines
			.map((line) => {
				const { time, text } = parseTimestamp(line);
				if (text === "纯音乐, 请欣赏") noLyrics = true;
				if (!time || !text) return null;

				const [key, value] = time.split(":") || [];
				const [min, sec] = [Number.parseFloat(key), Number.parseFloat(value)];
				if (!Number.isNaN(min) && !Number.isNaN(sec) && !containCredits(text)) {
					return {
						startTime: (min * 60 + sec) * 1000,
						text: text || "",
					};
				}
				return null;
			})
			.filter(Boolean);

		if (!lyrics.length || noLyrics) {
			return null;
		}
		return lyrics;
	}

	function getTranslation(list) {
		const lyricStr = list?.tlyric?.lyric;

		if (!lyricStr) {
			return null;
		}

		const lines = lyricStr.split(/\r?\n/).map((line) => line.trim());
		const translation = lines
			.map((line) => {
				const { time, text } = parseTimestamp(line);
				if (!time || !text) return null;

				const [key, value] = time.split(":") || [];
				const [min, sec] = [Number.parseFloat(key), Number.parseFloat(value)];
				if (!Number.isNaN(min) && !Number.isNaN(sec) && !containCredits(text)) {
					return {
						startTime: (min * 60 + sec) * 1000,
						text: text || "",
					};
				}
				return null;
			})
			.filter(Boolean);

		if (!translation.length) {
			return null;
		}
		return translation;
	}

	function getUnsynced(list) {
		const lyricStr = list?.lrc?.lyric;
		let noLyrics = false;

		if (!lyricStr) {
			return null;
		}

		const lines = lyricStr.split(/\r?\n/).map((line) => line.trim());
		const lyrics = lines
			.map((line) => {
				const parsed = parseTimestamp(line);
				if (parsed.text === "纯音乐, 请欣赏") noLyrics = true;
				if (!parsed.text || containCredits(parsed.text)) return null;
				return parsed;
			})
			.filter(Boolean);

		if (!lyrics.length || noLyrics) {
			return null;
		}
		return lyrics;
	}

	return { findLyrics, getKaraoke, getSynced, getUnsynced, getTranslation };
})();


================================================
FILE: CustomApps/lyrics-plus/Providers.js
================================================
const Providers = {
	spotify: async (info) => {
		const result = {
			uri: info.uri,
			karaoke: null,
			synced: null,
			unsynced: null,
			provider: "Spotify",
			copyright: null,
		};

		const baseURL = "https://spclient.wg.spotify.com/color-lyrics/v2/track/";
		const id = info.uri.split(":")[2];
		let body;
		try {
			body = await Spicetify.CosmosAsync.get(`${baseURL + id}?format=json&vocalRemoval=false&market=from_token`);
		} catch {
			return { error: "Request error", uri: info.uri };
		}

		const lyrics = body.lyrics;
		if (!lyrics) {
			return { error: "No lyrics", uri: info.uri };
		}

		const lines = lyrics.lines;
		if (lyrics.syncType === "LINE_SYNCED") {
			result.synced = lines.map((line) => ({
				startTime: line.startTimeMs,
				text: line.words,
			}));
			result.unsynced = result.synced;
		} else {
			result.unsynced = lines.map((line) => ({
				text: line.words,
			}));
		}

		/**
		 * to distinguish it from the existing Musixmatch, the provider will remain as Spotify.
		 * if Spotify official lyrics support multiple providers besides Musixmatch in the future, please uncomment the under section. */
		// result.provider = lyrics.provider;

		return result;
	},
	musixmatch: async (info) => {
		const result = {
			error: null,
			uri: info.uri,
			karaoke: null,
			synced: null,
			unsynced: null,
			musixmatchTranslation: null,
			musixmatchAvailableTranslations: [],
			musixmatchTrackId: null,
			musixmatchTranslationLanguage: null,
			provider: "Musixmatch",
			copyright: null,
		};

		let list;
		try {
			list = await ProviderMusixmatch.findLyrics(info);
			if (list.error) {
				throw "";
			}
		} catch {
			result.error = "No lyrics";
			return result;
		}

		const karaoke = await ProviderMusixmatch.getKaraoke(list);
		if (karaoke) {
			result.karaoke = karaoke;
			result.copyright = list["track.lyrics.get"].message?.body?.lyrics?.lyrics_copyright?.trim();
		}
		const synced = ProviderMusixmatch.getSynced(list);
		if (synced) {
			result.synced = synced;
			result.copyright = list["track.subtitles.get"].message?.body?.subtitle_list?.[0]?.subtitle.lyrics_copyright.trim();
		}
		const unsynced = synced || ProviderMusixmatch.getUnsynced(list);
		if (unsynced) {
			result.unsynced = unsynced;
			result.copyright = list["track.lyrics.get"].message?.body?.lyrics?.lyrics_copyright?.trim();
		}
		result.musixmatchAvailableTranslations = Array.isArray(list.__musixmatchTranslationStatus) ? list.__musixmatchTranslationStatus : [];
		result.musixmatchTrackId = list.__musixmatchTrackId ?? null;

		const selectedLanguage = CONFIG.visual["musixmatch-translation-language"];
		const canRequestTranslation =
			selectedLanguage && selectedLanguage !== "none" && result.musixmatchAvailableTranslations.includes(selectedLanguage);

		const translation = canRequestTranslation ? await ProviderMusixmatch.getTranslation(result.musixmatchTrackId) : null;
		if ((synced || unsynced) && Array.isArray(translation) && translation.length) {
			const normalizeLyrics =
				typeof Utils !== "undefined" && typeof Utils.processLyrics === "function"
					? (value) => Utils.processLyrics(value ?? "")
					: (value) =>
							typeof value === "string" ? value.replace(/ | /g, "").replace(/[!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~?!,。、《》【】「」]/g, "") : "";

			const translationMap = new Map();
			for (const entry of translation) {
				const normalizedMatched = normalizeLyrics(entry.matchedLine);
				if (!translationMap.has(normalizedMatched)) {
					translationMap.set(normalizedMatched, entry.translation);
				}
			}

			const baseLyrics = synced ?? unsynced;
			result.musixmatchTranslation = baseLyrics.map((line) => {
				const originalText = line.text;
				const normalizedOriginal = normalizeLyrics(originalText);
				return {
					...line,
					text: translationMap.get(normalizedOriginal) ?? line.text,
					originalText,
				};
			});
			result.musixmatchTranslationLanguage = selectedLanguage;
		}

		return result;
	},
	netease: async (info) => {
		const result = {
			uri: info.uri,
			karaoke: null,
			synced: null,
			unsynced: null,
			neteaseTranslation: null,
			provider: "Netease",
			copyright: null,
		};

		let list;
		try {
			list = await ProviderNetease.findLyrics(info);
		} catch {
			result.error = "No lyrics";
			return result;
		}

		const karaoke = ProviderNetease.getKaraoke(list);
		if (karaoke) {
			result.karaoke = karaoke;
		}
		const synced = ProviderNetease.getSynced(list);
		if (synced) {
			result.synced = synced;
		}
		const unsynced = synced || ProviderNetease.getUnsynced(list);
		if (unsynced) {
			result.unsynced = unsynced;
		}
		const translation = ProviderNetease.getTranslation(list);
		if ((synced || unsynced) && Array.isArray(translation)) {
			const baseLyrics = synced ?? unsynced;
			result.neteaseTranslation = baseLyrics.map((line) => ({
				...line,
				text: translation.find((t) => t.startTime === line.startTime)?.text ?? line.text,
				originalText: line.text,
			}));
		}

		return result;
	},
	lrclib: async (info) => {
		const result = {
			uri: info.uri,
			karaoke: null,
			synced: null,
			unsynced: null,
			provider: "lrclib",
			copyright: null,
		};

		let list;
		try {
			list = await ProviderLRCLIB.findLyrics(info);
		} catch {
			result.error = "No lyrics";
			return result;
		}

		const synced = ProviderLRCLIB.getSynced(list);
		if (synced) {
			result.synced = synced;
		}

		const unsynced = synced || ProviderLRCLIB.getUnsynced(list);

		if (unsynced) {
			result.unsynced = unsynced;
		}

		return result;
	},
	genius: async (info) => {
		const { lyrics, versions } = await ProviderGenius.fetchLyrics(info);

		let versionIndex2 = 0;
		let genius2 = lyrics;
		if (CONFIG.visual["dual-genius"] && versions.length > 1) {
			genius2 = await ProviderGenius.fetchLyricsVersion(versions, 1);
			versionIndex2 = 1;
		}

		return {
			uri: info.uri,
			genius: lyrics,
			provider: "Genius",
			karaoke: null,
			synced: null,
			unsynced: null,
			copyright: null,
			error: null,
			versions,
			versionIndex: 0,
			genius2,
			versionIndex2,
		};
	},
	local: (info) => {
		let result = {
			uri: info.uri,
			karaoke: null,
			synced: null,
			unsynced: null,
			provider: "local",
		};

		try {
			const savedLyrics = JSON.parse(localStorage.getItem("lyrics-plus:local-lyrics"));
			const lyrics = savedLyrics[info.uri];
			if (!lyrics) {
				throw "";
			}

			result = {
				...result,
				...lyrics,
			};
		} catch {
			result.error = "No lyrics";
		}

		return result;
	},
};


================================================
FILE: CustomApps/lyrics-plus/README.md
================================================
# Spicetify Custom App

### Lyrics Plus

Show current track lyrics. Current lyrics providers:

- Internal Spotify lyrics service.
- Netease: From Chinese developers and users. Provides karaoke and synced lyrics.
- Musixmatch: A company from Italy. Provided synced lyrics.
- 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).

![kara](./kara.png)

![genius](./genius.png)

Different 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.

Right 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

![lockin](./lockin.png)

Lyrics 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.

![search](./search.png)

Choose between different option of displaying Japanese lyrics. (Furigana, Romaji, Hiragana, Katakana)

![conversion](./conversion.png)

Customise colors, change providers' priorities in config menu. Config menu locates in Profile Menu (top right button with your user name).

To install, run:

```bash
spicetify config custom_apps lyrics-plus
spicetify apply
```

### Credits

- 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.
- 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.
- The algorithm for converting Japanese lyrics is based on [Hexenq's Kuroshiro](https://github.com/hexenq/kuroshiro).
- 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).
- The algorithm for converting Korean lyrics is based on [fujaru's aromanize-js](https://github.com/fujaru/aromanize-js)
- The algorithm for detecting Simplified Chinese is adapted from [nickdrewe's traditional-or-simplified](https://github.com/nickdrewe/traditional-or-simplified).


================================================
FILE: CustomApps/lyrics-plus/Settings.js
================================================
const ButtonSVG = ({ icon, active = true, onClick }) => {
	return react.createElement(
		"button",
		{
			className: `switch${active ? "" : " disabled"}`,
			onClick,
		},
		react.createElement("svg", {
			width: 16,
			height: 16,
			viewBox: "0 0 16 16",
			fill: "currentColor",
			dangerouslySetInnerHTML: {
				__html: icon,
			},
		})
	);
};

const SwapButton = ({ icon, disabled, onClick }) => {
	return react.createElement(
		"button",
		{
			className: "switch small",
			onClick,
			disabled,
		},
		react.createElement("svg", {
			width: 10,
			height: 10,
			viewBox: "0 0 16 16",
			fill: "currentColor",
			dangerouslySetInnerHTML: {
				__html: icon,
			},
		})
	);
};

const CacheButton = () => {
	let lyrics = {};

	try {
		const localLyrics = JSON.parse(localStorage.getItem("lyrics-plus:local-lyrics"));
		if (!localLyrics || typeof localLyrics !== "object") {
			throw "";
		}
		lyrics = localLyrics;
	} catch {
		lyrics = {};
	}

	const [count, setCount] = useState(Object.keys(lyrics).length);
	const text = count ? "Clear all cached lyrics" : "No cached lyrics";

	return react.createElement(
		"button",
		{
			className: "btn",
			onClick: () => {
				localStorage.removeItem("lyrics-plus:local-lyrics");
				setCount(0);
			},
			disabled: !count,
		},
		text
	);
};

const RefreshTokenButton = ({ setTokenCallback }) => {
	const [buttonText, setButtonText] = useState("Refresh token");

	useEffect(() => {
		if (buttonText === "Refreshing token...") {
			Spicetify.CosmosAsync.get("https://apic-desktop.musixmatch.com/ws/1.1/token.get?app_id=web-desktop-app-v1.0", null, {
				authority: "apic-desktop.musixmatch.com",
			})
				.then(({ message: response }) => {
					if (response.header.status_code === 200 && response.body.user_token) {
						setTokenCallback(response.body.user_token);
						setButtonText("Token refreshed");
					} else if (response.header.status_code === 401) {
						setButtonText("Too many attempts");
					} else {
						setButtonText("Failed to refresh token");
						console.error("Failed to refresh token", response);
					}
				})
				.catch((error) => {
					setButtonText("Failed to refresh token");
					console.error("Failed to refresh token", error);
				});
		}
	}, [buttonText]);

	return react.createElement(
		"button",
		{
			className: "btn",
			onClick: () => {
				setButtonText("Refreshing token...");
			},
			disabled: buttonText !== "Refresh token",
		},
		buttonText
	);
};

const ConfigButton = ({ name, text, onChange = () => {} }) => {
	return react.createElement(
		"div",
		{
			className: "setting-row",
		},
		react.createElement(
			"label",
			{
				className: "col description",
			},
			name
		),
		react.createElement(
			"div",
			{
				className: "col action",
			},
			react.createElement(
				"button",
				{
					className: "btn",
					onClick: onChange,
				},
				text
			)
		)
	);
};

const ConfigSlider = ({ name, defaultValue, onChange = () => {} }) => {
	const [active, setActive] = useState(defaultValue);

	useEffect(() => {
		setActive(defaultValue);
	}, [defaultValue]);

	const toggleState = useCallback(() => {
		const state = !active;
		setActive(state);
		onChange(state);
	}, [active]);

	return react.createElement(
		"div",
		{
			className: "setting-row",
		},
		react.createElement(
			"label",
			{
				className: "col description",
			},
			name
		),
		react.createElement(
			"div",
			{
				className: "col action",
			},
			react.createElement(ButtonSVG, {
				icon: Spicetify.SVGIcons.check,
				active,
				onClick: toggleState,
			})
		)
	);
};

const ConfigSelection = ({ name, defaultValue, options, onChange = () => {} }) => {
	const [value, setValue] = useState(defaultValue);

	const setValueCallback = useCallback(
		(event) => {
			let value = event.target.value;
			if (!Number.isNaN(Number(value))) {
				value = Number.parseInt(value);
			}
			setValue(value);
			onChange(value);
		},
		[value, options]
	);

	useEffect(() => {
		setValue(defaultValue);
	}, [defaultValue]);

	if (!Object.keys(options).length) return null;

	return react.createElement(
		"div",
		{
			className: "setting-row",
		},
		react.createElement(
			"label",
			{
				className: "col description",
			},
			name
		),
		react.createElement(
			"div",
			{
				className: "col action",
			},
			react.createElement(
				"select",
				{
					className: "main-dropDown-dropDown",
					value,
					onChange: setValueCallback,
				},
				Object.keys(options).map((item) =>
					react.createElement(
						"option",
						{
							value: item,
						},
						options[item]
					)
				)
			)
		)
	);
};

const ConfigInput = ({ name, defaultValue, onChange = () => {} }) => {
	const [value, setValue] = useState(defaultValue);

	const setValueCallback = useCallback(
		(event) => {
			const value = event.target.value;
			setValue(value);
			onChange(value);
		},
		[value]
	);

	return react.createElement(
		"div",
		{
			className: "setting-row",
		},
		react.createElement(
			"label",
			{
				className: "col description",
			},
			name
		),
		react.createElement(
			"div",
			{
				className: "col action",
			},
			react.createElement("input", {
				value,
				onChange: setValueCallback,
			})
		)
	);
};

const ConfigAdjust = ({ name, defaultValue, step, min, max, onChange = () => {} }) => {
	const [value, setValue] = useState(defaultValue);

	function adjust(dir) {
		let temp = value + dir * step;
		if (temp < min) {
			temp = min;
		} else if (temp > max) {
			temp = max;
		}
		setValue(temp);
		onChange(temp);
	}
	return react.createElement(
		"div",
		{
			className: "setting-row",
		},
		react.createElement(
			"label",
			{
				className: "col description",
			},
			name
		),
		react.createElement(
			"div",
			{
				className: "col action",
			},
			react.createElement(SwapButton, {
				icon: `<path d="M2 7h12v2H0z"/>`,
				onClick: () => adjust(-1),
				disabled: value === min,
			}),
			react.createElement(
				"p",
				{
					className: "adjust-value",
				},
				value
			),
			react.createElement(SwapButton, {
				icon: Spicetify.SVGIcons.plus2px,
				onClick: () => adjust(1),
				disabled: value === max,
			})
		)
	);
};

const ConfigHotkey = ({ name, defaultValue, onChange = () => {} }) => {
	const [value, setValue] = useState(defaultValue);
	const [trap] = useState(new Spicetify.Mousetrap());

	function record() {
		trap.handleKey = (character, modifiers, e) => {
			if (e.type === "keydown") {
				const sequence = [...new Set([...modifiers, character])];
				if (sequence.length === 1 && sequence[0] === "esc") {
					onChange("");
					setValue("");
					return;
				}
				setValue(sequence.join("+"));
			}
		};
	}

	function finishRecord() {
		trap.handleKey = () => {};
		onChange(value);
	}

	return react.createElement(
		"div",
		{
			className: "setting-row",
		},
		react.createElement(
			"label",
			{
				className: "col description",
			},
			name
		),
		react.createElement(
			"div",
			{
				className: "col action",
			},
			react.createElement("input", {
				value,
				onFocus: record,
				onBlur: finishRecord,
			})
		)
	);
};

const ServiceAction = ({ item, setTokenCallback }) => {
	switch (item.name) {
		case "local":
			return react.createElement(CacheButton);
		case "musixmatch":
			return react.createElement(RefreshTokenButton, { setTokenCallback });
		default:
			return null;
	}
};

const ServiceOption = ({ item, onToggle, onSwap, isFirst = false, isLast = false, onTokenChange = null }) => {
	const [token, setToken] = useState(item.token);
	const [active, setActive] = useState(item.on);

	const setTokenCallback = useCallback(
		(token) => {
			setToken(token);
			onTokenChange(item.name, token);
		},
		[item.token]
	);

	const toggleActive = useCallback(() => {
		if (item.name === "genius" && spotifyVersion >= "1.2.31") return;
		const state = !active;
		setActive(state);
		onToggle(item.name, state);
	}, [active]);

	return react.createElement(
		"div",
		null,
		react.createElement(
			"div",
			{
				className: "setting-row",
			},
			react.createElement(
				"h3",
				{
					className: "col description",
				},
				item.name
			),
			react.createElement(
				"div",
				{
					className: "col action",
				},
				react.createElement(ServiceAction, {
					item,
					setTokenCallback,
				}),
				react.createElement(SwapButton, {
					icon: Spicetify.SVGIcons["chart-up"],
					onClick: () => onSwap(item.name, -1),
					disabled: isFirst,
				}),
				react.createElement(SwapButton, {
					icon: Spicetify.SVGIcons["chart-down"],
					onClick: () => onSwap(item.name, 1),
					disabled: isLast,
				}),
				react.createElement(ButtonSVG, {
					icon: Spicetify.SVGIcons.check,
					active,
					onClick: toggleActive,
				})
			)
		),
		react.createElement("span", {
			dangerouslySetInnerHTML: {
				__html: item.desc,
			},
		}),
		item.token !== undefined &&
			react.createElement("input", {
				placeholder: `Place your ${item.name} token here`,
				value: token,
				onChange: (event) => setTokenCallback(event.target.value),
			})
	);
};

const ServiceList = ({ itemsList, onListChange = () => {}, onToggle = () => {}, onTokenChange = () => {} }) => {
	const [items, setItems] = useState(itemsList);
	const maxIndex = items.length - 1;

	const onSwap = useCallback(
		(name, direction) => {
			const curPos = items.findIndex((val) => val === name);
			const newPos = curPos + direction;
			[items[curPos], items[newPos]] = [items[newPos], items[curPos]];
			onListChange(items);
			setItems([...items]);
		},
		[items]
	);

	return items.map((key, index) => {
		const item = CONFIG.providers[key];
		item.name = key;
		return react.createElement(ServiceOption, {
			item,
			key,
			isFirst: index === 0,
			isLast: index === maxIndex,
			onSwap,
			onTokenChange,
			onToggle,
		});
	});
};

const corsProxyTemplate = () => {
	const [proxyValue, setProxyValue] = react.useState(localStorage.getItem("spicetify:corsProxyTemplate") || "https://cors-proxy.spicetify.app/{url}");

	return react.createElement("input", {
		placeholder: "CORS Proxy Template",
		value: proxyValue,
		onChange: (event) => {
			const value = event.target.value;
			setProxyValue(value);

			if (value === "" || !value) return localStorage.removeItem("spicetify:corsProxyTemplate");
			localStorage.setItem("spicetify:corsProxyTemplate", value);
		},
	});
};

const OptionList = ({ type, items, onChange }) => {
	const [itemList, setItemList] = useState(items);
	const [, forceUpdate] = useState();

	useEffect(() => {
		if (!type) return;

		const eventListener = (event) => {
			if (event.detail?.type !== type) return;
			setItemList(event.detail.items);
		};
		document.addEventListener("lyrics-plus", eventListener);

		return () => document.removeEventListener("lyrics-plus", eventListener);
	}, []);

	return itemList.map((item) => {
		if (!item || (item.when && !item.when())) {
			return;
		}

		const onChangeItem = item.onChange || onChange;

		return react.createElement(
			"div",
			null,
			react.createElement(item.type, {
				...item,
				name: item.desc,
				defaultValue: CONFIG.visual[item.key],
				onChange: (value) => {
					onChangeItem(item.key, value);
					forceUpdate({});
				},
			}),
			item.info &&
				react.createElement("span", {
					dangerouslySetInnerHTML: {
						__html: item.info,
					},
				})
		);
	});
};

function openConfig() {
	const configContainer = react.createElement(
		"div",
		{
			id: `${APP_NAME}-config-container`,
		},
		react.createElement("h2", null, "Options"),
		react.createElement(OptionList, {
			items: [
				{
					desc: "Playbar button",
					key: "playbar-button",
					info: "Replace Spotify's lyrics button with Lyrics Plus.",
					type: ConfigSlider,
				},
				{
					desc: "Global delay",
					info: "Offset (in ms) across all tracks.",
					key: "global-delay",
					type: ConfigAdjust,
					min: -10000,
					max: 10000,
					step: 250,
				},
				{
					desc: "Font size",
					info: "(or Ctrl + Mouse scroll in main app)",
					key: "font-size",
					type: ConfigAdjust,
					min: fontSizeLimit.min,
					max: fontSizeLimit.max,
					step: fontSizeLimit.step,
				},
				{
					desc: "Alignment",
					key: "alignment",
					type: ConfigSelection,
					options: {
						left: "Left",
						center: "Center",
						right: "Right",
					},
				},
				{
					desc: "Fullscreen hotkey",
					key: "fullscreen-key",
					type: ConfigHotkey,
				},
				{
					desc: "Compact synced: Lines to show before",
					key: "lines-before",
					type: ConfigSelection,
					options: [0, 1, 2, 3, 4],
				},
				{
					desc: "Compact synced: Lines to show after",
					key: "lines-after",
					type: ConfigSelection,
					options: [0, 1, 2, 3, 4],
				},
				{
					desc: "Compact synced: Fade-out blur",
					key: "fade-blur",
					type: ConfigSlider,
				},
				{
					desc: "Noise overlay",
					key: "noise",
					type: ConfigSlider,
				},
				{
					desc: "Colorful background",
					key: "colorful",
					type: ConfigSlider,
				},
				{
					desc: "Background color",
					key: "background-color",
					type: ConfigInput,
					when: () => !CONFIG.visual.colorful,
				},
				{
					desc: "Active text color",
					key: "active-color",
					type: ConfigInput,
					when: () => !CONFIG.visual.colorful,
				},
				{
					desc: "Inactive text color",
					key: "inactive-color",
					type: ConfigInput,
					when: () => !CONFIG.visual.colorful,
				},
				{
					desc: "Highlight text background",
					key: "highlight-color",
					type: ConfigInput,
					when: () => !CONFIG.visual.colorful,
				},
				{
					desc: "Text convertion: Japanese Detection threshold (Advanced)",
					info: "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.",
					key: "ja-detect-threshold",
					type: ConfigAdjust,
					min: thresholdSizeLimit.min,
					max: thresholdSizeLimit.max,
					step: thresholdSizeLimit.step,
				},
				{
					desc: "Text convertion: Traditional-Simplified Detection threshold (Advanced)",
					info: "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.",
					key: "hans-detect-threshold",
					type: ConfigAdjust,
					min: thresholdSizeLimit.min,
					max: thresholdSizeLimit.max,
					step: thresholdSizeLimit.step,
				},
				{
					desc: "Clear Memory Cache",
					info: "Loaded lyrics are cached in memory for faster reloading. Press this button to clear the cached lyrics from memory without restarting Spotify.",
					key: "clear-memore-cache",
					text: "Clear memory cache",
					type: ConfigButton,
					onChange: () => {
						reloadLyrics?.();
					},
				},
			],
			onChange: (name, value) => {
				CONFIG.visual[name] = value;
				localStorage.setItem(`${APP_NAME}:visual:${name}`, value);
				lyricContainerUpdate?.();

				const configChange = new CustomEvent("lyrics-plus", {
					detail: {
						type: "config",
						name: name,
						value: value,
					},
				});
				window.dispatchEvent(configChange);
			},
		}),
		react.createElement("h2", null, "Providers"),
		react.createElement(ServiceList, {
			itemsList: CONFIG.providersOrder,
			onListChange: (list) => {
				CONFIG.providersOrder = list;
				localStorage.setItem(`${APP_NAME}:services-order`, JSON.stringify(list));
				reloadLyrics?.();
			},
			onToggle: (name, value) => {
				CONFIG.providers[name].on = value;
				localStorage.setItem(`${APP_NAME}:provider:${name}:on`, value);
				reloadLyrics?.();
			},
			onTokenChange: (name, value) => {
				CONFIG.providers[name].token = value;
				localStorage.setItem(`${APP_NAME}:provider:${name}:token`, value);
				reloadLyrics?.();
			},
		}),
		react.createElement("h2", null, "CORS Proxy Template"),
		react.createElement("span", {
			dangerouslySetInnerHTML: {
				__html:
					"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.",
			},
		}),
		react.createElement(corsProxyTemplate),
		react.createElement("span", {
			dangerouslySetInnerHTML: {
				__html: "Spotify will reload its webview after applying. Leave empty to restore default: <code>https://cors-proxy.spicetify.app/{url}</code>",
			},
		})
	);

	Spicetify.PopupModal.display({
		title: "Lyrics Plus",
		content: configContainer,
		isLarge: true,
	});
}


================================================
FILE: CustomApps/lyrics-plus/TabBar.js
================================================
class TabBarItem extends react.Component {
	onSelect(event) {
		event.preventDefault();
		this.props.switchTo(this.props.item.key);
	}
	onLock(event) {
		event.preventDefault();
		this.props.lockIn(this.props.item.key);
	}
	render() {
		return react.createElement(
			"li",
			{
				className: "lyrics-tabBar-headerItem",
				onClick: this.onSelect.bind(this),
				onDoubleClick: this.onLock.bind(this),
				onContextMenu: this.onLock.bind(this),
			},
			react.createElement(
				"a",
				{
					"aria-current": "page",
					className: `lyrics-tabBar-headerItemLink ${this.props.item.active ? "lyrics-tabBar-active" : ""}`,
					draggable: "false",
					href: "",
				},
				react.createElement(
					"span",
					{
						className: "main-type-mestoBold",
					},
					this.props.item.value
				)
			)
		);
	}
}

const TabBarMore = react.memo(({ items, switchTo, lockIn }) => {
	const activeItem = items.find((item) => item.active);

	function onLock(event) {
		event.preventDefault();
		if (activeItem) {
			lockIn(activeItem.key);
		}
	}
	return react.createElement(
		"li",
		{
			className: `lyrics-tabBar-headerItem ${activeItem ? "lyrics-tabBar-active" : ""}`,
			onDoubleClick: onLock,
			onContextMenu: onLock,
		},
		react.createElement(OptionsMenu, {
			options: items,
			onSelect: switchTo,
			selected: activeItem,
			defaultValue: "More",
			bold: true,
		})
	);
});

const TopBarContent = ({ links, activeLink, lockLink, switchCallback, lockCallback }) => {
	const resizeHost = document.querySelector(
		".Root__main-view .os-resize-observer-host, .Root__main-view .os-size-observer, .Root__main-view .main-view-container__scroll-node"
	);
	const [windowSize, setWindowSize] = useState(resizeHost.clientWidth);
	const resizeHandler = () => setWindowSize(resizeHost.clientWidth);

	useEffect(() => {
		const observer = new ResizeObserver(resizeHandler);
		observer.observe(resizeHost);
		return () => {
			observer.disconnect();
		};
	}, [resizeHandler]);

	return react.createElement(
		TabBarContext,
		null,
		react.createElement(TabBar, {
			className: "queue-queueHistoryTopBar-tabBar",
			links,
			activeLink,
			lockLink,
			switchCallback,
			lockCallback,
			windowSize,
		})
	);
};

const TabBarContext = ({ children }) => {
	return Spicetify.ReactDOM.createPortal(
		react.createElement(
			"div",
			{
				className: "main-topBar-topbarContent",
			},
			children
		),
		document.querySelector(".main-topBar-topbarContentWrapper")
	);
};

const TabBar = react.memo(({ links, activeLink, lockLink, switchCallback, lockCallback, windowSize = Number.POSITIVE_INFINITY }) => {
	const tabBarRef = react.useRef(null);
	const [childrenSizes, setChildrenSizes] = useState([]);
	const [availableSpace, setAvailableSpace] = useState(0);
	const [droplistItem, setDroplistItems] = useState([]);

	const options = [];
	for (let i = 0; i < links.length; i++) {
		const key = links[i];
		if (spotifyVersion >= "1.2.31" && key === "genius") continue;
		let value = key[0].toUpperCase() + key.slice(1);
		if (key === lockLink) value = `• ${value}`;
		const active = key === activeLink;
		options.push({ key, value, active });
	}

	useEffect(() => {
		if (!tabBarRef.current) return;
		setAvailableSpace(tabBarRef.current.clientWidth);
	}, [windowSize]);

	useEffect(() => {
		if (!tabBarRef.current) return;

		const tabbarItemSizes = [];
		for (const child of tabBarRef.current.children) {
			tabbarItemSizes.push(child.clientWidth);
		}

		setChildrenSizes(tabbarItemSizes);
	}, [links]);

	useEffect(() => {
		if (!tabBarRef.current) return;

		const totalSize = childrenSizes.reduce((a, b) => a + b, 0);

		// Can we render everything?
		if (totalSize <= availableSpace) {
			setDroplistItems([]);
			return;
		}

		// The `More` button can be set to _any_ of the children. So we
		// reserve space for the largest item instead of always taking
		// the last item.
		const viewMoreButtonSize = Math.max(...childrenSizes);

		// Figure out how many children we can render while also showing
		// the More button
		const itemsToHide = [];
		let stopWidth = viewMoreButtonSize;

		childrenSizes.forEach((childWidth, i) => {
			if (availableSpace >= stopWidth + childWidth) {
				stopWidth += childWidth;
			} else {
				// First elem is edit button
				itemsToHide.push(i);
			}
		});

		setDroplistItems(itemsToHide);
	}, [availableSpace, childrenSizes]);

	return react.createElement(
		"nav",
		{
			className: "lyrics-tabBar lyrics-tabBar-nav",
		},
		react.createElement(
			"ul",
			{
				className: "lyrics-tabBar-header",
				ref: tabBarRef,
			},
			react.createElement("li", {
				className: "lyrics-tabBar-headerItem",
			}),
			options
				.filter((_, id) => !droplistItem.includes(id))
				.map((item) =>
					react.createElement(TabBarItem, {
						item,
						switchTo: switchCallback,
						lockIn: lockCallback,
					})
				),
			droplistItem.length || childrenSizes.length === 0
				? react.createElement(TabBarMore, {
						items: droplistItem.map((i) => options[i]).filter(Boolean),
						switchTo: switchCallback,
						lockIn: lockCallback,
					})
				: null
		)
	);
});


================================================
FILE: CustomApps/lyrics-plus/Translator.js
================================================
const kuroshiroPath = "https://cdn.jsdelivr.net/npm/kuroshiro@1.2.0/dist/kuroshiro.min.js";
const kuromojiPath = "https://cdn.jsdelivr.net/npm/kuroshiro-analyzer-kuromoji@1.1.0/dist/kuroshiro-analyzer-kuromoji.min.js";
const aromanize = "https://cdn.jsdelivr.net/npm/aromanize@0.1.5/aromanize.min.js";
const openCCPath = "https://cdn.jsdelivr.net/npm/opencc-js@1.0.5/dist/umd/full.min.js";

const dictPath = "https:/cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict";

class Translator {
	constructor(lang, isUsingNetease = false) {
		this.finished = {
			ja: false,
			ko: false,
			zh: false,
		};
		this.isUsingNetease = isUsingNetease;

		this.applyKuromojiFix();
		this.injectExternals(lang);
		this.createTranslator(lang);
	}

	includeExternal(url) {
		if ((CONFIG.visual.translate || this.isUsingNetease) && !document.querySelector(`script[src="${url}"]`)) {
			const script = document.createElement("script");
			script.setAttribute("type", "text/javascript");
			script.setAttribute("src", url);
			document.head.appendChild(script);
		}
	}

	injectExternals(lang) {
		switch (lang?.slice(0, 2)) {
			case "ja":
				this.includeExternal(kuromojiPath);
				this.includeExternal(kuroshiroPath);
				break;
			case "ko":
				this.includeExternal(aromanize);
				break;
			case "zh":
				this.includeExternal(openCCPath);
				break;
		}
	}

	async awaitFinished(language) {
		return new Promise((resolve) => {
			const interval = setInterval(() => {
				this.injectExternals(language);
				this.createTranslator(language);

				const lan = language.slice(0, 2);
				if (this.finished[lan]) {
					clearInterval(interval);
					resolve();
				}
			}, 100);
		});
	}

	/**
	 * Fix an issue with kuromoji when loading dict from external urls
	 * Adapted from: https://github.com/mobilusoss/textlint-browser-runner/pull/7
	 */
	applyKuromojiFix() {
		if (typeof XMLHttpRequest.prototype.realOpen !== "undefined") return;
		XMLHttpRequest.prototype.realOpen = XMLHttpRequest.prototype.open;
		XMLHttpRequest.prototype.open = function (method, url, bool) {
			if (url.indexOf(dictPath.replace("https://", "https:/")) === 0) {
				this.realOpen(method, url.replace("https:/", "https://"), bool);
			} else {
				this.realOpen(method, url, bool);
			}
		};
	}

	async createTranslator(lang) {
		switch (lang.slice(0, 2)) {
			case "ja":
				if (this.kuroshiro) return;
				if (typeof Kuroshiro === "undefined" || typeof KuromojiAnalyzer === "undefined") {
					await Translator.#sleep(50);
					return this.createTranslator(lang);
				}

				this.kuroshiro = new Kuroshiro.default();
				this.kuroshiro.init(new KuromojiAnalyzer({ dictPath })).then(
					function () {
						this.finished.ja = true;
					}.bind(this)
				);

				break;
			case "ko":
				if (this.Aromanize) return;
				if (typeof Aromanize === "undefined") {
					await Translator.#sleep(50);
					return this.createTranslator(lang);
				}

				this.Aromanize = Aromanize;
				this.finished.ko = true;
				break;
			case "zh":
				if (this.OpenCC) return;
				if (typeof OpenCC === "undefined") {
					await Translator.#sleep(50);
					return this.createTranslator(lang);
				}

				this.OpenCC = OpenCC;
				this.finished.zh = true;
				break;
		}
	}

	async romajifyText(text, target = "romaji", mode = "spaced") {
		if (!this.finished.ja) {
			await Translator.#sleep(100);
			return this.romajifyText(text, target, mode);
		}

		return this.kuroshiro.convert(text, {
			to: target,
			mode: mode,
		});
	}

	async convertToRomaja(text, target) {
		if (!this.finished.ko) {
			await Translator.#sleep(100);
			return this.convertToRomaja(text, target);
		}

		if (target === "hangul") return text;
		return Aromanize.hangulToLatin(text, "rr-translit");
	}

	async convertChinese(text, from, target) {
		if (!this.finished.zh) {
			await Translator.#sleep(100);
			return this.convertChinese(text, from, target);
		}

		const converter = this.OpenCC.Converter({
			from: from,
			to: target,
		});

		return converter(text);
	}

	/**
	 * Async wrapper of `setTimeout`.
	 *
	 * @param {number} ms
	 * @returns {Promise<void>}
	 */
	static async #sleep(ms) {
		return new Promise((resolve) => setTimeout(resolve, ms));
	}
}


================================================
FILE: CustomApps/lyrics-plus/Utils.js
================================================
const Utils = {
	addQueueListener(callback) {
		Spicetify.Player.origin._events.addListener("queue_update", callback);
	},
	removeQueueListener(callback) {
		Spicetify.Player.origin._events.removeListener("queue_update", callback);
	},
	convertIntToRGB(colorInt, div = 1) {
		const rgb = {
			r: Math.round(((colorInt >> 16) & 0xff) / div),
			g: Math.round(((colorInt >> 8) & 0xff) / div),
			b: Math.round((colorInt & 0xff) / div),
		};
		return `rgb(${rgb.r},${rgb.g},${rgb.b})`;
	},
	/**
	 * @param {string} s
	 * @param {boolean} emptySymbol
	 * @returns {string}
	 */
	normalize(s, emptySymbol = true) {
		let result = s
			.replace(/(/g, "(")
			.replace(/)/g, ")")
			.replace(/【/g, "[")
			.replace(/】/g, "]")
			.replace(/。/g, ". ")
			.replace(/;/g, "; ")
			.replace(/:/g, ": ")
			.replace(/?/g, "? ")
			.replace(/!/g, "! ")
			.replace(/、|,/g, ", ")
			.replace(/‘|’|′|'/g, "'")
			.replace(/“|”/g, '"')
			.replace(/〜/g, "~")
			.replace(/·|・/g, "•");
		if (emptySymbol) {
			result = result.replace(/-/g, " ").replace(/\//g, " ");
		}
		return result.replace(/\s+/g, " ").trim();
	},
	/**
	 * Check if the specified string contains Han character.
	 *
	 * @param {string} s
	 * @returns {boolean}
	 */
	containsHanCharacter(s) {
		const hanRegex = /\p{Script=Han}/u;
		return hanRegex.test(s);
	},
	/**
	 * Singleton Translator instance for {@link toSimplifiedChinese}.
	 *
	 * @type {Translator | null}
	 */
	set translator(translator) {
		this._translatorInstance = translator;
	},
	_translatorInstance: null,
	/**
	 * Convert all Han characters to Simplified Chinese.
	 *
	 * Choosing Simplified Chinese makes the converted result more accurate,
	 * as the conversion from SC to TC may have multiple possibilities,
	 * while the conversion from TC to SC usually has only one possibility.
	 *
	 * @param {string} s
	 * @returns {Promise<string>}
	 */
	async toSimplifiedChinese(s) {
		// create a singleton Translator instance
		if (!this._translatorInstance) this.translator = new Translator("zh", true);

		// translate to Simplified Chinese
		// as Traditional Chinese differs between HK and TW, forcing to use OpenCC standard
		return this._translatorInstance.convertChinese(s, "t", "cn");
	},
	removeSongFeat(s) {
		return (
			s
				.replace(/-\s+(feat|with|prod).*/i, "")
				.replace(/(\(|\[)(feat|with|prod)\.?\s+.*(\)|\])$/i, "")
				.trim() || s
		);
	},
	removeExtraInfo(s) {
		return s.replace(/\s-\s.*/, "");
	},
	capitalize(s) {
		return s.replace(/^(\w)/, ($1) => $1.toUpperCase());
	},
	detectLanguage(lyrics) {
		if (!Array.isArray(lyrics)) return;

		// Should return IETF BCP 47 language tags.
		// This should detect the song's main language.
		// Remember there is a possibility of a song referencing something in another language and the lyrics show it in that native language!
		const rawLyrics = lyrics[0].originalText ? lyrics.map((line) => line.originalText).join(" ") : lyrics.map((line) => line.text).join(" ");

		const kanaRegex = /[\u3001-\u3003]|[\u3005\u3007]|[\u301d-\u301f]|[\u3021-\u3035]|[\u3038-\u303a]|[\u3040-\u30ff]|[\uff66-\uff9f]/gu;
		const hangulRegex = /(\S*[\u3131-\u314e|\u314f-\u3163|\uac00-\ud7a3]+\S*)/g;
		const simpRegex =
			/[万与丑专业丛东丝丢两严丧个丬丰临为丽举么义乌乐乔习乡书买乱争于亏云亘亚产亩亲亵亸亿仅从仑仓仪们价众优伙会伛伞伟传伤伥伦伧伪伫体余佣佥侠侣侥侦侧侨侩侪侬俣俦俨俩俪俭债倾偬偻偾偿傥傧储傩儿兑兖党兰关兴兹养兽冁内冈册写军农冢冯冲决况冻净凄凉凌减凑凛几凤凫凭凯击凼凿刍划刘则刚创删别刬刭刽刿剀剂剐剑剥剧劝办务劢动励劲劳势勋勐勚匀匦匮区医华协单卖卢卤卧卫却卺厂厅历厉压厌厍厕厢厣厦厨厩厮县参叆叇双发变叙叠叶号叹叽吁后吓吕吗吣吨听启吴呒呓呕呖呗员呙呛呜咏咔咙咛咝咤咴咸哌响哑哒哓哔哕哗哙哜哝哟唛唝唠唡唢唣唤唿啧啬啭啮啰啴啸喷喽喾嗫呵嗳嘘嘤嘱噜噼嚣嚯团园囱围囵国图圆圣圹场坂坏块坚坛坜坝坞坟坠垄垅垆垒垦垧垩垫垭垯垱垲垴埘埙埚埝埯堑堕塆墙壮声壳壶壸处备复够头夸夹夺奁奂奋奖奥妆妇妈妩妪妫姗姜娄娅娆娇娈娱娲娴婳婴婵婶媪嫒嫔嫱嬷孙学孪宁宝实宠审宪宫宽宾寝对寻导寿将尔尘尧尴尸尽层屃屉届属屡屦屿岁岂岖岗岘岙岚岛岭岳岽岿峃峄峡峣峤峥峦崂崃崄崭嵘嵚嵛嵝嵴巅巩巯币帅师帏帐帘帜带帧帮帱帻帼幂幞干并广庄庆庐庑库应庙庞废庼廪开异弃张弥弪弯弹强归当录彟彦彻径徕御忆忏忧忾怀态怂怃怄怅怆怜总怼怿恋恳恶恸恹恺恻恼恽悦悫悬悭悯惊惧惨惩惫惬惭惮惯愍愠愤愦愿慑慭憷懑懒懔戆戋戏戗战戬户扎扑扦执扩扪扫扬扰抚抛抟抠抡抢护报担拟拢拣拥拦拧拨择挂挚挛挜挝挞挟挠挡挢挣挤挥挦捞损捡换捣据捻掳掴掷掸掺掼揸揽揿搀搁搂搅携摄摅摆摇摈摊撄撑撵撷撸撺擞攒敌敛数斋斓斗斩断无旧时旷旸昙昼昽显晋晒晓晔晕晖暂暧札术朴机杀杂权条来杨杩杰极构枞枢枣枥枧枨枪枫枭柜柠柽栀栅标栈栉栊栋栌栎栏树栖样栾桊桠桡桢档桤桥桦桧桨桩梦梼梾检棂椁椟椠椤椭楼榄榇榈榉槚槛槟槠横樯樱橥橱橹橼檐檩欢欤欧歼殁殇残殒殓殚殡殴毁毂毕毙毡毵氇气氢氩氲汇汉污汤汹沓沟没沣沤沥沦沧沨沩沪沵泞泪泶泷泸泺泻泼泽泾洁洒洼浃浅浆浇浈浉浊测浍济浏浐浑浒浓浔浕涂涌涛涝涞涟涠涡涢涣涤润涧涨涩淀渊渌渍渎渐渑渔渖渗温游湾湿溃溅溆溇滗滚滞滟滠满滢滤滥滦滨滩滪漤潆潇潋潍潜潴澜濑濒灏灭灯灵灾灿炀炉炖炜炝点炼炽烁烂烃烛烟烦烧烨烩烫烬热焕焖焘煅煳熘爱爷牍牦牵牺犊犟状犷犸犹狈狍狝狞独狭狮狯狰狱狲猃猎猕猡猪猫猬献獭玑玙玚玛玮环现玱玺珉珏珐珑珰珲琎琏琐琼瑶瑷璇璎瓒瓮瓯电画畅畲畴疖疗疟疠疡疬疮疯疱疴痈痉痒痖痨痪痫痴瘅瘆瘗瘘瘪瘫瘾瘿癞癣癫癯皑皱皲盏盐监盖盗盘眍眦眬着睁睐睑瞒瞩矫矶矾矿砀码砖砗砚砜砺砻砾础硁硅硕硖硗硙硚确硷碍碛碜碱碹磙礼祎祢祯祷祸禀禄禅离秃秆种积称秽秾稆税稣稳穑穷窃窍窑窜窝窥窦窭竖竞笃笋笔笕笺笼笾筑筚筛筜筝筹签简箓箦箧箨箩箪箫篑篓篮篱簖籁籴类籼粜粝粤粪粮糁糇紧絷纟纠纡红纣纤纥约级纨纩纪纫纬纭纮纯纰纱纲纳纴纵纶纷纸纹纺纻纼纽纾线绀绁绂练组绅细织终绉绊绋绌绍绎经绐绑绒结绔绕绖绗绘给绚绛络绝绞统绠绡绢绣绤绥绦继绨绩绪绫绬续绮绯绰绱绲绳维绵绶绷绸绹绺绻综绽绾绿缀缁缂缃缄缅缆缇缈缉缊缋缌缍缎缏缐缑缒缓缔缕编缗缘缙缚缛缜缝缞缟缠缡缢缣缤缥缦缧缨缩缪缫缬缭缮缯缰缱缲缳缴缵罂网罗罚罢罴羁羟羡翘翙翚耢耧耸耻聂聋职聍联聩聪肃肠肤肷肾肿胀胁胆胜胧胨胪胫胶脉脍脏脐脑脓脔脚脱脶脸腊腌腘腭腻腼腽腾膑臜舆舣舰舱舻艰艳艹艺节芈芗芜芦苁苇苈苋苌苍苎苏苘苹茎茏茑茔茕茧荆荐荙荚荛荜荞荟荠荡荣荤荥荦荧荨荩荪荫荬荭荮药莅莜莱莲莳莴莶获莸莹莺莼萚萝萤营萦萧萨葱蒇蒉蒋蒌蓝蓟蓠蓣蓥蓦蔷蔹蔺蔼蕲蕴薮藁藓虏虑虚虫虬虮虽虾虿蚀蚁蚂蚕蚝蚬蛊蛎蛏蛮蛰蛱蛲蛳蛴蜕蜗蜡蝇蝈蝉蝎蝼蝾螀螨蟏衅衔补衬衮袄袅袆袜袭袯装裆裈裢裣裤裥褛褴襁襕见观觃规觅视觇览觉觊觋觌觍觎觏觐觑觞触觯詟誉誊讠计订讣认讥讦讧讨让讪讫训议讯记讱讲讳讴讵讶讷许讹论讻讼讽设访诀证诂诃评诅识诇诈诉诊诋诌词诎诏诐译诒诓诔试诖诗诘诙诚诛诜话诞诟诠诡询诣诤该详诧诨诩诪诫诬语诮误诰诱诲诳说诵诶请诸诹诺读诼诽课诿谀谁谂调谄谅谆谇谈谊谋谌谍谎谏谐谑谒谓谔谕谖谗谘谙谚谛谜谝谞谟谠谡谢谣谤谥谦谧谨谩谪谫谬谭谮谯谰谱谲谳谴谵谶谷豮贝贞负贠贡财责贤败账货质贩贪贫贬购贮贯贰贱贲贳贴贵贶贷贸费贺贻贼贽贾贿赀赁赂赃资赅赆赇赈赉赊赋赌赍赎赏赐赑赒赓赔赕赖赗赘赙赚赛赜赝赞赟赠赡赢赣赪赵赶趋趱趸跃跄跖跞践跶跷跸跹跻踊踌踪踬踯蹑蹒蹰蹿躏躜躯车轧轨轩轪轫转轭轮软轰轱轲轳轴轵轶轷轸轹轺轻轼载轾轿辀辁辂较辄辅辆辇辈辉辊辋辌辍辎辏辐辑辒输辔辕辖辗辘辙辚辞辩辫边辽达迁过迈运还这进远违连迟迩迳迹适选逊递逦逻遗遥邓邝邬邮邹邺邻郁郄郏郐郑郓郦郧郸酝酦酱酽酾酿释里鉅鉴銮錾钆钇针钉钊钋钌钍钎钏钐钑钒钓钔钕钖钗钘钙钚钛钝钞钟钠钡钢钣钤钥钦钧钨钩钪钫钬钭钮钯钰钱钲钳钴钵钶钷钸钹钺钻钼钽钾钿铀铁铂铃铄铅铆铈铉铊铋铍铎铏铐铑铒铕铗铘铙铚铛铜铝铞铟铠铡铢铣铤铥铦铧铨铪铫铬铭铮铯铰铱铲铳铴铵银铷铸铹铺铻铼铽链铿销锁锂锃锄锅锆锇锈锉锊锋锌锍锎锏锐锑锒锓锔锕锖锗错锚锜锞锟锠锡锢锣锤锥锦锨锩锫锬锭键锯锰锱锲锳锴锵锶锷锸锹锺锻锼锽锾锿镀镁镂镃镆镇镈镉镊镌镍镎镏镐镑镒镕镖镗镙镚镛镜镝镞镟镠镡镢镣镤镥镦镧镨镩镪镫镬镭镮镯镰镱镲镳镴镶长门闩闪闫闬闭问闯闰闱闲闳间闵闶闷闸闹闺闻闼闽闾闿阀阁阂阃阄阅阆阇阈阉阊阋阌阍阎阏阐阑阒阓阔阕阖阗阘阙阚阛队阳阴阵阶际陆陇陈陉陕陧陨险随隐隶隽难雏雠雳雾霁霉霭靓静靥鞑鞒鞯鞴韦韧韨韩韪韫韬韵页顶顷顸项顺须顼顽顾顿颀颁颂颃预颅领颇颈颉颊颋颌颍颎颏颐频颒颓颔颕颖颗题颙颚颛颜额颞颟颠颡颢颣颤颥颦颧风飏飐飑飒飓飔飕飖飗飘飙飚飞飨餍饤饥饦饧饨饩饪饫饬饭饮饯饰饱饲饳饴饵饶饷饸饹饺饻饼饽饾饿馀馁馂馃馄馅馆馇馈馉馊馋馌馍馎馏馐馑馒馓馔馕马驭驮驯驰驱驲驳驴驵驶驷驸驹驺驻驼驽驾驿骀骁骂骃骄骅骆骇骈骉骊骋验骍骎骏骐骑骒骓骔骕骖骗骘骙骚骛骜骝骞骟骠骡骢骣骤骥骦骧髅髋髌鬓魇魉鱼鱽鱾鱿鲀鲁鲂鲄鲅鲆鲇鲈鲉鲊鲋鲌鲍鲎鲏鲐鲑鲒鲓鲔鲕鲖鲗鲘鲙鲚鲛鲜鲝鲞鲟鲠鲡鲢鲣鲤鲥鲦鲧鲨鲩鲪鲫鲬鲭鲮鲯鲰鲱鲲鲳鲴鲵鲶鲷鲸鲹鲺鲻鲼鲽鲾鲿鳀鳁鳂鳃鳄鳅鳆鳇鳈鳉鳊鳋鳌鳍鳎鳏鳐鳑鳒鳓鳔鳕鳖鳗鳘鳙鳛鳜鳝鳞鳟鳠鳡鳢鳣鸟鸠鸡鸢鸣鸤鸥鸦鸧鸨鸩鸪鸫鸬鸭鸮鸯鸰鸱鸲鸳鸴鸵鸶鸷鸸鸹鸺鸻鸼鸽鸾鸿鹀鹁鹂鹃鹄鹅鹆鹇鹈鹉鹊鹋鹌鹍鹎鹏鹐鹑鹒鹓鹔鹕鹖鹗鹘鹚鹛鹜鹝鹞鹟鹠鹡鹢鹣鹤鹥鹦鹧鹨鹩鹪鹫鹬鹭鹯鹰鹱鹲鹳鹴鹾麦麸黄黉黡黩黪黾鼋鼌鼍鼗鼹齄齐齑齿龀龁龂龃龄龅龆龇龈龉龊龋龌龙龚龛龟志制咨只里系范松没尝尝闹面准钟别闲干尽脏拼]/gu;
		const tradRegex =
			/[萬與醜專業叢東絲丟兩嚴喪個爿豐臨為麗舉麼義烏樂喬習鄉書買亂爭於虧雲亙亞產畝親褻嚲億僅從侖倉儀們價眾優夥會傴傘偉傳傷倀倫傖偽佇體餘傭僉俠侶僥偵側僑儈儕儂俁儔儼倆儷儉債傾傯僂僨償儻儐儲儺兒兌兗黨蘭關興茲養獸囅內岡冊寫軍農塚馮衝決況凍淨淒涼淩減湊凜幾鳳鳧憑凱擊氹鑿芻劃劉則剛創刪別剗剄劊劌剴劑剮劍剝劇勸辦務勱動勵勁勞勢勳猛勩勻匭匱區醫華協單賣盧鹵臥衛卻巹廠廳曆厲壓厭厙廁廂厴廈廚廄廝縣參靉靆雙發變敘疊葉號歎嘰籲後嚇呂嗎唚噸聽啟吳嘸囈嘔嚦唄員咼嗆嗚詠哢嚨嚀噝吒噅鹹呱響啞噠嘵嗶噦嘩噲嚌噥喲嘜嗊嘮啢嗩唕喚呼嘖嗇囀齧囉嘽嘯噴嘍嚳囁嗬噯噓嚶囑嚕劈囂謔團園囪圍圇國圖圓聖壙場阪壞塊堅壇壢壩塢墳墜壟壟壚壘墾坰堊墊埡墶壋塏堖塒塤堝墊垵塹墮壪牆壯聲殼壺壼處備複夠頭誇夾奪奩奐奮獎奧妝婦媽嫵嫗媯姍薑婁婭嬈嬌孌娛媧嫻嫿嬰嬋嬸媼嬡嬪嬙嬤孫學孿寧寶實寵審憲宮寬賓寢對尋導壽將爾塵堯尷屍盡層屭屜屆屬屢屨嶼歲豈嶇崗峴嶴嵐島嶺嶽崠巋嶨嶧峽嶢嶠崢巒嶗崍嶮嶄嶸嶔崳嶁脊巔鞏巰幣帥師幃帳簾幟帶幀幫幬幘幗冪襆幹並廣莊慶廬廡庫應廟龐廢廎廩開異棄張彌弳彎彈強歸當錄彠彥徹徑徠禦憶懺憂愾懷態慫憮慪悵愴憐總懟懌戀懇惡慟懨愷惻惱惲悅愨懸慳憫驚懼慘懲憊愜慚憚慣湣慍憤憒願懾憖怵懣懶懍戇戔戲戧戰戩戶紮撲扡執擴捫掃揚擾撫拋摶摳掄搶護報擔擬攏揀擁攔擰撥擇掛摯攣掗撾撻挾撓擋撟掙擠揮撏撈損撿換搗據撚擄摑擲撣摻摜摣攬撳攙擱摟攪攜攝攄擺搖擯攤攖撐攆擷擼攛擻攢敵斂數齋斕鬥斬斷無舊時曠暘曇晝曨顯晉曬曉曄暈暉暫曖劄術樸機殺雜權條來楊榪傑極構樅樞棗櫪梘棖槍楓梟櫃檸檉梔柵標棧櫛櫳棟櫨櫟欄樹棲樣欒棬椏橈楨檔榿橋樺檜槳樁夢檮棶檢欞槨櫝槧欏橢樓欖櫬櫚櫸檟檻檳櫧橫檣櫻櫫櫥櫓櫞簷檁歡歟歐殲歿殤殘殞殮殫殯毆毀轂畢斃氈毿氌氣氫氬氳彙漢汙湯洶遝溝沒灃漚瀝淪滄渢溈滬濔濘淚澩瀧瀘濼瀉潑澤涇潔灑窪浹淺漿澆湞溮濁測澮濟瀏滻渾滸濃潯濜塗湧濤澇淶漣潿渦溳渙滌潤澗漲澀澱淵淥漬瀆漸澠漁瀋滲溫遊灣濕潰濺漵漊潷滾滯灩灄滿瀅濾濫灤濱灘澦濫瀠瀟瀲濰潛瀦瀾瀨瀕灝滅燈靈災燦煬爐燉煒熗點煉熾爍爛烴燭煙煩燒燁燴燙燼熱煥燜燾煆糊溜愛爺牘犛牽犧犢強狀獷獁猶狽麅獮獰獨狹獅獪猙獄猻獫獵獼玀豬貓蝟獻獺璣璵瑒瑪瑋環現瑲璽瑉玨琺瓏璫琿璡璉瑣瓊瑤璦璿瓔瓚甕甌電畫暢佘疇癤療瘧癘瘍鬁瘡瘋皰屙癰痙癢瘂癆瘓癇癡癉瘮瘞瘺癟癱癮癭癩癬癲臒皚皺皸盞鹽監蓋盜盤瞘眥矓著睜睞瞼瞞矚矯磯礬礦碭碼磚硨硯碸礪礱礫礎硜矽碩硤磽磑礄確鹼礙磧磣堿镟滾禮禕禰禎禱禍稟祿禪離禿稈種積稱穢穠穭稅穌穩穡窮竊竅窯竄窩窺竇窶豎競篤筍筆筧箋籠籩築篳篩簹箏籌簽簡籙簀篋籜籮簞簫簣簍籃籬籪籟糴類秈糶糲粵糞糧糝餱緊縶糸糾紆紅紂纖紇約級紈纊紀紉緯紜紘純紕紗綱納紝縱綸紛紙紋紡紵紖紐紓線紺絏紱練組紳細織終縐絆紼絀紹繹經紿綁絨結絝繞絰絎繪給絢絳絡絕絞統綆綃絹繡綌綏絛繼綈績緒綾緓續綺緋綽緔緄繩維綿綬繃綢綯綹綣綜綻綰綠綴緇緙緗緘緬纜緹緲緝縕繢緦綞緞緶線緱縋緩締縷編緡緣縉縛縟縝縫縗縞纏縭縊縑繽縹縵縲纓縮繆繅纈繚繕繒韁繾繰繯繳纘罌網羅罰罷羆羈羥羨翹翽翬耮耬聳恥聶聾職聹聯聵聰肅腸膚膁腎腫脹脅膽勝朧腖臚脛膠脈膾髒臍腦膿臠腳脫腡臉臘醃膕齶膩靦膃騰臏臢輿艤艦艙艫艱豔艸藝節羋薌蕪蘆蓯葦藶莧萇蒼苧蘇檾蘋莖蘢蔦塋煢繭荊薦薘莢蕘蓽蕎薈薺蕩榮葷滎犖熒蕁藎蓀蔭蕒葒葤藥蒞蓧萊蓮蒔萵薟獲蕕瑩鶯蓴蘀蘿螢營縈蕭薩蔥蕆蕢蔣蔞藍薊蘺蕷鎣驀薔蘞藺藹蘄蘊藪槁蘚虜慮虛蟲虯蟣雖蝦蠆蝕蟻螞蠶蠔蜆蠱蠣蟶蠻蟄蛺蟯螄蠐蛻蝸蠟蠅蟈蟬蠍螻蠑螿蟎蠨釁銜補襯袞襖嫋褘襪襲襏裝襠褌褳襝褲襇褸襤繈襴見觀覎規覓視覘覽覺覬覡覿覥覦覯覲覷觴觸觶讋譽謄訁計訂訃認譏訐訌討讓訕訖訓議訊記訒講諱謳詎訝訥許訛論訩訟諷設訪訣證詁訶評詛識詗詐訴診詆謅詞詘詔詖譯詒誆誄試詿詩詰詼誠誅詵話誕詬詮詭詢詣諍該詳詫諢詡譸誡誣語誚誤誥誘誨誑說誦誒請諸諏諾讀諑誹課諉諛誰諗調諂諒諄誶談誼謀諶諜謊諫諧謔謁謂諤諭諼讒諮諳諺諦謎諞諝謨讜謖謝謠謗諡謙謐謹謾謫譾謬譚譖譙讕譜譎讞譴譫讖穀豶貝貞負貟貢財責賢敗賬貨質販貪貧貶購貯貫貳賤賁貰貼貴貺貸貿費賀貽賊贄賈賄貲賃賂贓資賅贐賕賑賚賒賦賭齎贖賞賜贔賙賡賠賧賴賵贅賻賺賽賾贗讚贇贈贍贏贛赬趙趕趨趲躉躍蹌蹠躒踐躂蹺蹕躚躋踴躊蹤躓躑躡蹣躕躥躪躦軀車軋軌軒軑軔轉軛輪軟轟軲軻轤軸軹軼軤軫轢軺輕軾載輊轎輈輇輅較輒輔輛輦輩輝輥輞輬輟輜輳輻輯轀輸轡轅轄輾轆轍轔辭辯辮邊遼達遷過邁運還這進遠違連遲邇逕跡適選遜遞邐邏遺遙鄧鄺鄔郵鄒鄴鄰鬱郤郟鄶鄭鄆酈鄖鄲醞醱醬釅釃釀釋裏钜鑒鑾鏨釓釔針釘釗釙釕釷釺釧釤鈒釩釣鍆釹鍚釵鈃鈣鈈鈦鈍鈔鍾鈉鋇鋼鈑鈐鑰欽鈞鎢鉤鈧鈁鈥鈄鈕鈀鈺錢鉦鉗鈷缽鈳鉕鈽鈸鉞鑽鉬鉭鉀鈿鈾鐵鉑鈴鑠鉛鉚鈰鉉鉈鉍鈹鐸鉶銬銠鉺銪鋏鋣鐃銍鐺銅鋁銱銦鎧鍘銖銑鋌銩銛鏵銓鉿銚鉻銘錚銫鉸銥鏟銃鐋銨銀銣鑄鐒鋪鋙錸鋱鏈鏗銷鎖鋰鋥鋤鍋鋯鋨鏽銼鋝鋒鋅鋶鐦鐧銳銻鋃鋟鋦錒錆鍺錯錨錡錁錕錩錫錮鑼錘錐錦鍁錈錇錟錠鍵鋸錳錙鍥鍈鍇鏘鍶鍔鍤鍬鍾鍛鎪鍠鍰鎄鍍鎂鏤鎡鏌鎮鎛鎘鑷鐫鎳鎿鎦鎬鎊鎰鎔鏢鏜鏍鏰鏞鏡鏑鏃鏇鏐鐔钁鐐鏷鑥鐓鑭鐠鑹鏹鐙鑊鐳鐶鐲鐮鐿鑔鑣鑞鑲長門閂閃閆閈閉問闖閏闈閑閎間閔閌悶閘鬧閨聞闥閩閭闓閥閣閡閫鬮閱閬闍閾閹閶鬩閿閽閻閼闡闌闃闠闊闋闔闐闒闕闞闤隊陽陰陣階際陸隴陳陘陝隉隕險隨隱隸雋難雛讎靂霧霽黴靄靚靜靨韃鞽韉韝韋韌韍韓韙韞韜韻頁頂頃頇項順須頊頑顧頓頎頒頌頏預顱領頗頸頡頰頲頜潁熲頦頤頻頮頹頷頴穎顆題顒顎顓顏額顳顢顛顙顥纇顫顬顰顴風颺颭颮颯颶颸颼颻飀飄飆飆飛饗饜飣饑飥餳飩餼飪飫飭飯飲餞飾飽飼飿飴餌饒餉餄餎餃餏餅餑餖餓餘餒餕餜餛餡館餷饋餶餿饞饁饃餺餾饈饉饅饊饌饢馬馭馱馴馳驅馹駁驢駔駛駟駙駒騶駐駝駑駕驛駘驍罵駰驕驊駱駭駢驫驪騁驗騂駸駿騏騎騍騅騌驌驂騙騭騤騷騖驁騮騫騸驃騾驄驏驟驥驦驤髏髖髕鬢魘魎魚魛魢魷魨魯魴魺鮁鮃鯰鱸鮋鮓鮒鮊鮑鱟鮍鮐鮭鮚鮳鮪鮞鮦鰂鮜鱠鱭鮫鮮鮺鯗鱘鯁鱺鰱鰹鯉鰣鰷鯀鯊鯇鮶鯽鯒鯖鯪鯕鯫鯡鯤鯧鯝鯢鯰鯛鯨鯵鯴鯔鱝鰈鰏鱨鯷鰮鰃鰓鱷鰍鰒鰉鰁鱂鯿鰠鼇鰭鰨鰥鰩鰟鰜鰳鰾鱈鱉鰻鰵鱅鰼鱖鱔鱗鱒鱯鱤鱧鱣鳥鳩雞鳶鳴鳲鷗鴉鶬鴇鴆鴣鶇鸕鴨鴞鴦鴒鴟鴝鴛鴬鴕鷥鷙鴯鴰鵂鴴鵃鴿鸞鴻鵐鵓鸝鵑鵠鵝鵒鷳鵜鵡鵲鶓鵪鶤鵯鵬鵮鶉鶊鵷鷫鶘鶡鶚鶻鶿鶥鶩鷊鷂鶲鶹鶺鷁鶼鶴鷖鸚鷓鷚鷯鷦鷲鷸鷺鸇鷹鸌鸏鸛鸘鹺麥麩黃黌黶黷黲黽黿鼂鼉鞀鼴齇齊齏齒齔齕齗齟齡齙齠齜齦齬齪齲齷龍龔龕龜誌製谘隻裡係範鬆冇嚐嘗鬨麵準鐘彆閒乾儘臟拚]/gu;
		const hanziRegex = /\p{Script=Han}/gu;

		const cjkMatch = rawLyrics.match(
			new RegExp(`${kanaRegex.source}|${hanziRegex.source}|${hangulRegex.source}|${/\p{Unified_Ideograph}/gu.source}`, "gu")
		);

		if (!cjkMatch) return;

		const kanaCount = cjkMatch.filter((glyph) => kanaRegex.test(glyph)).length;
		const hanziCount = cjkMatch.filter((glyph) => hanziRegex.test(glyph)).length;
		const simpCount = cjkMatch.filter((glyph) => simpRegex.test(glyph)).length;
		const tradCount = cjkMatch.filter((glyph) => tradRegex.test(glyph)).length;

		const kanaPercentage = kanaCount / cjkMatch.length;
		const hanziPercentage = hanziCount / cjkMatch.length;
		const simpPercentage = simpCount / cjkMatch.length;
		const tradPercentage = tradCount / cjkMatch.length;

		if (cjkMatch.filter((glyph) => hangulRegex.test(glyph)).length !== 0) {
			return "ko";
		}

		if (((kanaPercentage - hanziPercentage + 1) / 2) * 100 >= CONFIG.visual["ja-detect-threshold"]) {
			return "ja";
		}

		return ((simpPercentage - tradPercentage + 1) / 2) * 100 >= CONFIG.visual["hans-detect-threshold"] ? "zh-hans" : "zh-hant";
	},
	processTranslatedLyrics(translated, original) {
		return original.map((lyric, index) => ({
			startTime: lyric.startTime || 0,
			text: this.rubyTextToReact(translated[index]),
			originalText: lyric.text,
		}));
	},
	/** It seems that this function is not being used, but I'll keep it just in case it’s needed in the future.*/
	processTranslatedOriginalLyrics(lyrics, synced) {
		const data = [];
		const dataSouce = {};

		for (const item of lyrics) {
			dataSouce[item.startTime] = { translate: item.text };
		}

		for (const time in synced) {
			dataSouce[item.startTime] = {
				...dataSouce[item.startTime],
				text: item.text,
			};
		}

		for (const time in dataSouce) {
			const item = dataSouce[time];
			const lyric = {
				startTime: time || 0,
				text: this.rubyTextToOriginalReact(item.translate || item.text, item.text || item.translate),
			};
			data.push(lyric);
		}

		return data;
	},
	rubyTextToOriginalReact(translated, syncedText) {
		const react = Spicetify.React;
		return react.createElement("p1", null, [react.createElement("ruby", {}, syncedText, react.createElement("rt", null, translated))]);
	},
	rubyTextToReact(s) {
		const react = Spicetify.React;
		const rubyElems = s.split("<ruby>");
		const reactChildren = [];

		reactChildren.push(rubyElems[0]);
		for (let i = 1; i < rubyElems.length; i++) {
			const kanji = rubyElems[i].split("<rp>")[0];
			const furigana = rubyElems[i].split("<rt>")[1].split("</rt>")[0];
			reactChildren.push(react.createElement("ruby", null, kanji, react.createElement("rt", null, furigana)));

			reactChildren.push(rubyElems[i].split("</ruby>")[1]);
		}
		return react.createElement("p1", null, reactChildren);
	},
	formatTime(timestamp) {
		if (Number.isNaN(timestamp)) return timestamp.toString();
		let minutes = Math.trunc(timestamp / 60000);
		let seconds = ((timestamp - minutes * 60000) / 1000).toFixed(2);

		if (minutes < 10) minutes = `0${minutes}`;
		if (seconds < 10) seconds = `0${seconds}`;

		return `${minutes}:${seconds}`;
	},
	formatTextWithTimestamps(text, startTime = 0) {
		if (text.props?.children) {
			return text.props.children
				.map((child) => {
					if (typeof child === "string") {
						return child;
					}
					if (child.props?.children) {
						return child.props?.children[0];
					}
				})
				.join("");
		}
		if (Array.isArray(text)) {
			let wordTime = startTime;
			return text
				.map((word) => {
					wordTime += word.time;
					return `${word.word}<${this.formatTime(wordTime)}>`;
				})
				.join("");
		}
		return text;
	},
	convertParsedToLRC(lyrics, isBelow) {
		let original = "";
		let conver = "";

		if (isBelow) {
			for (const line of lyrics) {
				original += `[${this.formatTime(line.startTime)}]${this.formatTextWithTimestamps(line.originalText, line.startTime)}\n`;
				conver += `[${this.formatTime(line.startTime)}]${this.formatTextWithTimestamps(line.text, line.startTime)}\n`;
			}
		} else {
			for (const line of lyrics) {
				original += `[${this.formatTime(line.startTime)}]${this.formatTextWithTimestamps(line.text, line.startTime)}\n`;
			}
		}

		return {
			original,
			conver,
		};
	},
	convertParsedToUnsynced(lyrics, isBelow) {
		let original = "";
		let conver = "";

		if (isBelow) {
			for (const line of lyrics) {
				if (typeof line.originalText === "object") {
					original += `${line.originalText?.props?.children?.[0]}\n`;
				} else {
					original += `${line.originalText}\n`;
				}

				if (typeof line.text === "object") {
					conver += `${line.text?.props?.children?.[0]}\n`;
				} else {
					conver += `${line.text}\n`;
				}
			}
		} else {
			for (const line of lyrics) {
				if (typeof line.text === "object") {
					original += `${line.text?.props?.children?.[0]}\n`;
				} else {
					original += `${line.text}\n`;
				}
			}
		}

		return {
			original,
			conver,
		};
	},
	parseLocalLyrics(lyrics) {
		// Preprocess lyrics by removing [tags] and empty lines
		const lines = lyrics
			.replaceAll(/\[[a-zA-Z]+:.+\]/g, "")
			.trim()
			.split("\n");

		const syncedTimestamp = /\[([0-9:.]+)\]/;
		const karaokeTimestamp = /<([0-9:.]+)>/;

		const unsynced = [];

		const isSynced = lines[0].match(syncedTimestamp);
		const synced = isSynced ? [] : null;

		const isKaraoke = lines[0].match(karaokeTimestamp);
		const karaoke = isKaraoke ? [] : null;

		function timestampToMs(timestamp) {
			const [minutes, seconds] = timestamp.replace(/\[\]<>/, "").split(":");
			return Number(minutes) * 60 * 1000 + Number(seconds) * 1000;
		}

		function parseKaraokeLine(line, startTime) {
			let wordTime = timestampToMs(startTime);
			const karaokeLine = [];
			const karaoke = line.matchAll(/(\S+ ?)<([0-9:.]+)>/g);
			for (const match of karaoke) {
				const word = match[1];
				const time = match[2];
				karaokeLine.push({ word, time: timestampToMs(time) - wordTime });
				wordTime = timestampToMs(time);
			}
			return karaokeLine;
		}

		for (const [i, line] of lines.entries()) {
			const time = line.match(syncedTimestamp)?.[1];
			let lyricContent = line.replace(syncedTimestamp, "").trim();
			const lyric = lyricContent.replaceAll(/<([0-9:.]+)>/g, "").trim();

			if (line.trim() !== "") {
				if (isKaraoke) {
					if (!lyricContent.endsWith(">")) {
						// For some reason there are a variety of formats for karaoke lyrics, Wikipedia is also inconsisent in their examples
						const endTime = lines[i + 1]?.match(syncedTimestamp)?.[1] || this.formatTime(Number(Spicetify.Player.data.item.metadata.duration));
						lyricContent += `<${endTime}>`;
					}
					const karaokeLine = parseKaraokeLine(lyricContent, time);
					karaoke.push({ text: karaokeLine, startTime: timestampToMs(time) });
				}
				isSynced && time && synced.push({ text: lyric || "♪", startTime: timestampToMs(time) });
				unsynced.push({ text: lyric || "♪" });
			}
		}

		return { synced, unsynced, karaoke };
	},
	processLyrics(lyrics) {
		return lyrics
			.replace(/ | /g, "") // Remove space
			.replace(/[!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~?!,。、《》【】「」]/g, ""); // Remove punctuation
	},
};


================================================
FILE: CustomApps/lyrics-plus/index.js
================================================
// Run "npm i @types/react" to have this type package available in workspace
/// <reference types="react" />
/// <reference path="../../globals.d.ts" />

/** @type {React} */
const react = Spicetify.React;
const { useState, useEffect, useCallback, useMemo, useRef } = react;
/** @type {import("react").ReactDOM} */
const spotifyVersion = Spicetify.Platform.version;

// Define a function called "render" to specify app entry point
// This function will be used to mount app to main view.
function render() {
	return react.createElement(LyricsContainer, null);
}

function getConfig(name, defaultVal = true) {
	const value = localStorage.getItem(name);
	return value ? value === "true" : defaultVal;
}

const APP_NAME = "lyrics-plus";
const MUSIXMATCH_TRANSLATION_PREFIX_DEFAULT = "musixmatchTranslation:";
const MUSIXMATCH_TRANSLATION_PREFIX_GLOBAL_KEY = "__lyricsPlusMusixmatchTranslationPrefix";
const MUSIXMATCH_TRANSLATION_FETCH_MESSAGE = "Fetching translation...";
const MUSIXMATCH_TRANSLATION_FETCH_FAILED_MESSAGE = "Failed to fetch translation, please try again in a few minutes";
const MUSIXMATCH_TRANSLATION_PREFIX =
	typeof window !== "undefined" && typeof window[MUSIXMATCH_TRANSLATION_PREFIX_GLOBAL_KEY] === "string"
		? window[MUSIXMATCH_TRANSLATION_PREFIX_GLOBAL_KEY]
		: MUSIXMATCH_TRANSLATION_PREFIX_DEFAULT;

if (typeof window !== "undefined") {
	window[MUSIXMATCH_TRANSLATION_PREFIX_GLOBAL_KEY] = MUSIXMATCH_TRANSLATION_PREFIX;
}

const KARAOKE = 0;
const SYNCED = 1;
const UNSYNCED = 2;
const GENIUS = 3;

const CONFIG = {
	visual: {
		"playbar-button": getConfig("lyrics-plus:visual:playbar-button", false),
		colorful: getConfig("lyrics-plus:visual:colorful"),
		noise: getConfig("lyrics-plus:visual:noise"),
		"background-color": localStorage.getItem("lyrics-plus:visual:background-color") || "var(--spice-main)",
		"active-color": localStorage.getItem("lyrics-plus:visual:active-color") || "var(--spice-text)",
		"inactive-color": localStorage.getItem("lyrics-plus:visual:inactive-color") || "rgba(var(--spice-rgb-subtext),0.5)",
		"highlight-color": localStorage.getItem("lyrics-plus:visual:highlight-color") || "var(--spice-button)",
		alignment: localStorage.getItem("lyrics-plus:visual:alignment") || "center",
		"lines-before": localStorage.getItem("lyrics-plus:visual:lines-before") || "0",
		"lines-after": localStorage.getItem("lyrics-plus:visual:lines-after") || "2",
		"font-size": localStorage.getItem("lyrics-plus:visual:font-size") || "32",
		"translate:translated-lyrics-source": localStorage.getItem("lyrics-plus:visual:translate:translated-lyrics-source") || "none",
		"translate:display-mode": localStorage.getItem("lyrics-plus:visual:translate:display-mode") || "replace",
		"translate:detect-language-override": localStorage.getItem("lyrics-plus:visual:translate:detect-language-override") || "off",
		"translation-mode:japanese": localStorage.getItem("lyrics-plus:visual:translation-mode:japanese") || "furigana",
		"translation-mode:korean": localStorage.getItem("lyrics-plus:visual:translation-mode:korean") || "romaja",
		"translation-mode:chinese": localStorage.getItem("lyrics-plus:visual:translation-mode:chinese") || "cn",
		translate: getConfig("lyrics-plus:visual:translate", false),
		"ja-detect-threshold": localStorage.getItem("lyrics-plus:visual:ja-detect-threshold") || "40",
		"hans-detect-threshold": localStorage.getItem("lyrics-plus:visual:hans-detect-threshold") || "40",
		"musixmatch-translation-language": localStorage.getItem("lyrics-plus:visual:musixmatch-translation-language") || "none",
		"fade-blur": getConfig("lyrics-plus:visual:fade-blur"),
		"fullscreen-key": localStorage.getItem("lyrics-plus:visual:fullscreen-key") || "f12",
		"show-performers": getConfig("lyrics-plus:visual:show-performers", true),
		"synced-compact": getConfig("lyrics-plus:visual:synced-compact"),
		"dual-genius": getConfig("lyrics-plus:visual:dual-genius"),
		"global-delay": Number(localStorage.getItem("lyrics-plus:visual:global-delay")) || 0,
		delay: 0,
	},
	providers: {
		lrclib: {
			on: getConfig("lyrics-plus:provider:lrclib:on"),
			desc: "Lyrics sourced from lrclib.net. Supports both synced and unsynced lyrics. LRCLIB is a free and open-source lyrics provider.",
			modes: [SYNCED, UNSYNCED],
		},
		musixmatch: {
			on: getConfig("lyrics-plus:provider:musixmatch:on"),
			desc: "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.",
			token: localStorage.getItem("lyrics-plus:provider:musixmatch:token") || "21051986b9886beabe1ce01c3ce94c96319411f8f2c122676365e3",
			modes: [KARAOKE, SYNCED, UNSYNCED],
		},
		spotify: {
			on: getConfig("lyrics-plus:provider:spotify:on"),
			desc: "Lyrics sourced from official Spotify API.",
			modes: [SYNCED, UNSYNCED],
		},
		netease: {
			on: getConfig("lyrics-plus:provider:netease:on", false),
			desc: "Crowdsourced lyrics provider ran by Chinese developers and users.",
			modes: [KARAOKE, SYNCED, UNSYNCED],
		},
		genius: {
			on: spotifyVersion >= "1.2.31" ? false : getConfig("lyrics-plus:provider:genius:on"),
			desc: "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.",
			modes: [GENIUS],
		},
		local: {
			on: getConfig("lyrics-plus:provider:local:on"),
			desc: "Provide lyrics from cache/local files loaded from previous Spotify sessions.",
			modes: [KARAOKE, SYNCED, UNSYNCED],
		},
	},
	providersOrder: localStorage.getItem("lyrics-plus:services-order"),
	modes: ["karaoke", "synced", "unsynced", "genius"],
	locked: localStorage.getItem("lyrics-plus:lock-mode") || "-1",
};

try {
	CONFIG.providersOrder = JSON.parse(CONFIG.providersOrder);
	if (!Array.isArray(CONFIG.providersOrder) || Object.keys(CONFIG.providers).length !== CONFIG.providersOrder.length) {
		throw "";
	}
} catch {
	CONFIG.providersOrder = Object.keys(CONFIG.providers);
	localStorage.setItem("lyrics-plus:services-order", JSON.stringify(CONFIG.providersOrder));
}

CONFIG.locked = Number.parseInt(CONFIG.locked);
CONFIG.visual["lines-before"] = Number.parseInt(CONFIG.visual["lines-before"]);
CONFIG.visual["lines-after"] = Number.parseInt(CONFIG.visual["lines-after"]);
CONFIG.visual["font-size"] = Number.parseInt(CONFIG.visual["font-size"]);
CONFIG.visual["ja-detect-threshold"] = Number.parseInt(CONFIG.visual["ja-detect-threshold"]);
CONFIG.visual["hans-detect-threshold"] = Number.parseInt(CONFIG.visual["hans-detect-threshold"]);

if (CONFIG.visual["translate:translated-lyrics-source"] === "musixmatchTranslation") {
	const language = CONFIG.visual["musixmatch-translation-language"];
	const normalizedLanguage = language && language !== "none" ? language : "none";
	const upgradedValue = normalizedLanguage !== "none" ? `${MUSIXMATCH_TRANSLATION_PREFIX}${normalizedLanguage}` : "none";
	CONFIG.visual["translate:translated-lyrics-source"] = upgradedValue;
	localStorage.setItem(`${APP_NAME}:visual:translate:translated-lyrics-source`, upgradedValue);
}

if (typeof CONFIG.visual["translate:translated-lyrics-source"] === "string") {
	const sourceValue = CONFIG.visual["translate:translated-lyrics-source"];
	if (sourceValue.startsWith(MUSIXMATCH_TRANSLATION_PREFIX)) {
		const language = sourceValue.slice(MUSIXMATCH_TRANSLATION_PREFIX.length) || "none";
		if (CONFIG.visual["musixmatch-translation-language"] !== language) {
			CONFIG.visual["musixmatch-translation-language"] = language;
			localStorage.setItem(`${APP_NAME}:visual:musixmatch-translation-language`, language);
		}
	}
}

if (
	CONFIG.visual.translate &&
	typeof CONFIG.visual["translate:translated-lyrics-source"] === "string" &&
	CONFIG.visual["translate:translated-lyrics-source"] !== "none"
) {
	CONFIG.visual.translate = false;
	localStorage.setItem(`${APP_NAME}:visual:translate`, "false");
}

let CACHE = {};

const emptyState = {
	karaoke: null,
	synced: null,
	unsynced: null,
	genius: null,
	genius2: null,
	currentLyrics: null,
	musixmatchAvailableTranslations: null,
	musixmatchTrackId: null,
	musixmatchTranslationLanguage: null,
};

let lyricContainerUpdate;
let reloadLyrics;
let refreshMusixmatchTranslation;

const fontSizeLimit = { min: 16, max: 256, step: 4 };

const thresholdSizeLimit = { min: 0, max: 100, step: 5 };

function resolveTranslationSource(source) {
	if (typeof source !== "string") {
		return { key: source, language: null };
	}

	if (source.startsWith(MUSIXMATCH_TRANSLATION_PREFIX)) {
		const language = source.slice(MUSIXMATCH_TRANSLATION_PREFIX.length) || null;
		return { key: "musixmatchTranslation", language };
	}

	return { key: source, language: null };
}

class LyricsContainer extends react.Component {
	constructor() {
		super();
		this.state = {
			karaoke: null,
			synced: null,
			unsynced: null,
			genius: null,
			genius2: null,
			currentLyrics: null,
			romaji: null,
			furigana: null,
			hiragana: null,
			hangul: null,
			romaja: null,
			katakana: null,
			cn: null,
			hk: null,
			tw: null,
			musixmatchTranslation: null,
			musixmatchTranslationLanguage: null,
			musixmatchAvailableTranslations: [],
			musixmatchTrackId: null,
			neteaseTranslation: null,
			uri: "",
			provider: "",
			colors: {
				background: "",
				inactive: "",
			},
			tempo: "0.25s",
			explicitMode: -1,
			lockMode: CONFIG.locked,
			mode: -1,
			isLoading: false,
			versionIndex: 0,
			versionIndex2: 0,
			isFullscreen: false,
			isFADMode: false,
			isCached: false,
			language: null,
		};
		this.currentTrackUri = "";
		this.nextTrackUri = "";
		this.availableModes = [];
		this.styleVariables = {};
		this.fullscreenContainer = document.createElement("div");
		this.fullscreenContainer.id = "lyrics-fullscreen-container";
		this.mousetrap = null;
		this.containerRef = react.createRef(null);
		this.translator = null;
		this.initMoustrap();
		// Cache last state
		this.languageOverride = CONFIG.visual["translate:detect-language-override"];
		this.translate = CONFIG.visual.translate;
		this.reRenderLyricsPage = false;
		this.displayMode = null;
		this.currentMusixmatchLanguage = CONFIG.visual["musixmatch-translation-language"];
		this._musixmatchTranslationRequestId = null;
	}

	infoFromTrack(track) {
		const meta = track?.metadata;
		if (!meta) {
			return null;
		}
		return {
			duration: Number(meta.duration),
			album: meta.album_title,
			artist: meta.artist_name,
			title: meta.title,
			uri: track.uri,
			image: meta.image_url,
		};
	}

	async fetchColors(uri) {
		let vibrant = 0;
		try {
			try {
				const { fetchExtractedColorForTrackEntity } = Spicetify.GraphQL.Definitions;
				const { data } = await Spicetify.GraphQL.Request(fetchExtractedColorForTrackEntity, { uri });
				const { hex } = data.trackUnion.albumOfTrack.coverArt.extractedColors.colorDark;
				vibrant = Number.parseInt(hex.replace("#", ""), 16);
			} catch {
				const colors = await Spicetify.CosmosAsync.get(`https://spclient.wg.spotify.com/colorextractor/v1/extract-presets?uri=${uri}&format=json`);
				vibrant = colors.entries[0].color_swatches.find((color) => color.preset === "VIBRANT_NON_ALARMING").color;
			}
		} catch {
			vibrant = 8747370;
		}

		this.setState({
			colors: {
				background: Utils.convertIntToRGB(vibrant),
				inactive: Utils.convertIntToRGB(vibrant, 3),
			},
		});
	}

	async fetchTempo(uri) {
		const audio = await Spicetify.CosmosAsync.get(
			`https://spclient.wg.spotify.com/audio-attributes/v1/audio-features/${uri.split(":")[2]}?format=json`
		);
		let tempo = audio.tempo;

		const MIN_TEMPO = 60;
		const MAX_TEMPO = 150;
		const MAX_PERIOD = 0.4;
		if (!tempo) tempo = 105;
		if (tempo < MIN_TEMPO) tempo = MIN_TEMPO;
		if (tempo > MAX_TEMPO) tempo = MAX_TEMPO;

		let period = MAX_PERIOD - ((tempo - MIN_TEMPO) / (MAX_TEMPO - MIN_TEMPO)) * MAX_PERIOD;
		period = Math.round(period * 100) / 100;

		this.setState({
			tempo: `${String(period)}s`,
		});
	}

	async refreshMusixmatchTranslation() {
		const selectedLanguage = CONFIG.visual["musixmatch-translation-language"] || "none";
		const availableTranslations = this.state.musixmatchAvailableTranslations || [];
		const trackId = this.state.musixmatchTrackId;
		const currentUri = this.state.uri;
		const currentRequestId = Symbol("musixmatchTranslationRequest");
		this._musixmatchTranslationRequestId = currentRequestId;
		const isLatestRequest = () => this._musixmatchTranslationRequestId === currentRequestId;
		const finishRequest = () => {
			if (isLatestRequest()) {
				this._musixmatchTranslationRequestId = null;
			}
		};

		const clearTranslation = () => {
			if (this.state.musixmatchTranslation !== null || this.state.musixmatchTranslationLanguage !== null) {
				this.setState({
					musixmatchTranslation: null,
					musixmatchTranslationLanguage: null,
				});
			}
			if (CACHE[currentUri]) {
				CACHE[currentUri].musixmatchTranslation = null;
				CACHE[currentUri].musixmatchTranslationLanguage = null;
			}
		};

		if (!trackId || !selectedLanguage || selectedLanguage === "none") {
			clearTranslation();
			finishRequest();
			return;
		}

		if (!availableTranslations.includes(selectedLanguage)) {
			clearTranslation();
			finishRequest();
			return;
		}

		const baseLyrics = this.state.synced ?? this.state.unsynced;
		if (!baseLyrics) {
			finishRequest();
			return;
		}

		const currentLanguage = selectedLanguage;

		Spicetify.showNotification(MUSIXMATCH_TRANSLATION_FETCH_MESSAGE, false, 1000);

		this.setState({
			musixmatchTranslation: null,
			musixmatchTranslationLanguage: null,
		});

		let translation;
		try {
			translation = await ProviderMusixmatch.getTranslation(trackId);
		} catch (error) {
			console.error(error);
			if (isLatestRequest()) {
				Spicetify.showNotification(MUSIXMATCH_TRANSLATION_FETCH_FAILED_MESSAGE, true, 3000);
				if (CACHE[currentUri]) {
					CACHE[currentUri].musixmatchTranslation = null;
					CACHE[currentUri].musixmatchTranslationLanguage = null;
				}
			}
			finishRequest();
			return;
		}

		if (!translation) {
			if (isLatestRequest()) {
				Spicetify.showNotification(MUSIXMATCH_TRANSLATION_FETCH_FAILED_MESSAGE, true, 3000);
				if (CACHE[currentUri]) {
					CACHE[currentUri].musixmatchTranslation = null;
					CACHE[currentUri].musixmatchTranslationLanguage = null;
				}
			}
			finishRequest();
			return;
		}

		if (
			currentLanguage !== CONFIG.visual["musixmatch-translation-language"] ||
			trackId !== this.state.musixmatchTrackId ||
			currentUri !== this.state.uri ||
			!isLatestRequest()
		) {
			finishRequest();
			return;
		}

		const latestBaseLyrics = this.state.synced ?? this.state.unsynced;
		if (!latestBaseLyrics) {
			finishRequest();
			return;
		}

		const mappedTranslation = latestBaseLyrics.map((line) => {
			const originalText = line.originalText ?? line.text;
			const matched = translation.find((entry) => Utils.processLyrics(entry.matchedLine) === Utils.processLyrics(originalText));

			return {
				...line,
				text: matched?.translation ?? line.text,
				originalText,
			};
		});

		if (!isLatestRequest()) {
			finishRequest();
			return;
		}

		this.setState({
			musixmatchTranslation: mappedTranslation,
			musixmatchTranslationLanguage: currentLanguage,
		});
		if (CACHE[currentUri]) {
			CACHE[currentUri].musixmatchTranslation = mappedTranslation;
			CACHE[currentUri].musixmatchTranslationLanguage = currentLanguage;
		}
		finishRequest();
	}

	async tryServices(trackInfo, mode = -1) {
		const currentMode = CONFIG.modes[mode] || "";
		let finalData = { ...emptyState, uri: trackInfo.uri };
		for (const id of CONFIG.providersOrder) {
			const service = CONFIG.providers[id];
			if (spotifyVersion >= "1.2.31" && id === "genius") continue;
			if (!service.on) continue;
			if (mode !== -1 && !service.modes.includes(mode)) continue;

			let data;
			try {
				data = await Providers[id](trackInfo);
			} catch (e) {
				console.error(e);
				continue;
			}

			if (data.error || (!data.karaoke && !data.synced && !data.unsynced && !data.genius)) continue;
			if (mode === -1) {
				finalData = data;
				return finalData;
			}

			if (!data[currentMode]) {
				for (const key in data) {
					if (!finalData[key]) {
						finalData[key] = data[key];
					}
				}
				continue;
			}

			for (const key in data) {
				if (!finalData[key]) {
					finalData[key] = data[key];
				}
			}

			if (data.provider !== "local" && finalData.provider && finalData.provider !== data.provider) {
				const styledMode = currentMode.charAt(0).toUpperCase() + currentMode.slice(1);
				finalData.copyright = `${styledMode} lyrics provided by ${data.provider}\n${finalData.copyright || ""}`.trim();
			}

			if (finalData.musixmatchTranslation && typeof finalData.musixmatchTranslation[0].startTime === "undefined" && finalData.synced) {
				finalData.musixmatchTranslation = finalData.synced.map((line) => ({
					...line,
					text:
						finalData.musixmatchTranslation.find((l) => Utils.processLyrics(l.originalText) === Utils.processLyrics(line.text))?.text ?? line.text,
				}));
			}

			return finalData;
		}

		return finalData;
	}

	async fetchLyrics(track, mode = -1, refresh = false) {
		const info = this.infoFromTrack(track);
		if (!info) {
			this.setState({ error: "No track info" });
			return;
		}

		let isCached = this.lyricsSaved(info.uri);

		if (CONFIG.visual.colorful) {
			this.fetchColors(info.uri);
		}

		this.fetchTempo(info.uri);
		this.resetDelay();

		let tempState;
		// if lyrics are cached
		if ((mode === -1 && CACHE[info.uri]) || CACHE[info.uri]?.[CONFIG.modes?.[mode]]) {
			tempState = { ...emptyState, ...CACHE[info.uri], isCached };
			if (CACHE[info.uri]?.mode) {
				this.state.explicitMode = CACHE[info.uri]?.mode;
				tempState = { ...tempState, mode: CACHE[info.uri]?.mode };
			}
		} else {
			this.setState({ ...emptyState, isLoading: true, isCached: false });

			const resp = await this.tryServices(info, mode);
			if (resp.provider) {
				// Cache lyrics
				CACHE[resp.uri] = resp;
			}

			// This True when the user presses the Cache Lyrics button and saves it to localStorage.
			isCached = this.lyricsSaved(resp.uri);

			// In case user skips tracks too fast and multiple callbacks
			// set wrong lyrics to current track.
			if (resp.uri === this.currentTrackUri) {
				tempState = { ...emptyState, ...resp, isLoading: false, isCached };
			} else {
				return;
			}
		}

		const selectedMusixmatchLanguage = CONFIG.visual["musixmatch-translation-language"] || "none";
		const shouldRefreshMusixmatchTranslation =
			tempState.musixmatchTrackId &&
			selectedMusixmatchLanguage !== "none" &&
			Array.isArray(tempState.musixmatchAvailableTranslations) &&
			tempState.musixmatchAvailableTranslations.includes(selectedMusixmatchLanguage) &&
			(tempState.musixmatchTranslationLanguage !== selectedMusixmatchLanguage || !tempState.musixmatchTranslation);
		if (
			selectedMusixmatchLanguage !== "none" &&
			(!Array.isArray(tempState.musixmatchAvailableTranslations) || !tempState.musixmatchAvailableTranslations.includes(selectedMusixmatchLanguage))
		) {
			if (
				typeof CONFIG.visual["translate:translated-lyrics-source"] === "string" &&
				CONFIG.visual["translate:translated-lyrics-source"].startsWith(MUSIXMATCH_TRANSLATION_PREFIX)
			) {
				CONFIG.visual["translate:translated-lyrics-source"] = "none";
				localStorage.setItem(`${APP_NAME}:visual:translate:translated-lyrics-source`, "none");
			}
			CONFIG.visual["musixmatch-translation-language"] = "none";
			localStorage.setItem(`${APP_NAME}:visual:musixmatch-translation-language`, "none");
		}
		const translationOverrides = shouldRefreshMusixmatchTranslation ? { musixmatchTranslation: null, musixmatchTranslationLanguage: null } : {};

		let finalMode = mode;
		if (mode === -1) {
			if (this.state.explicitMode !== -1) {
				finalMode = this.state.explicitMode;
			} else if (this.state.lockMode !== -1) {
				finalMode = this.state.lockMode;
			} else {
				// Auto switch
				if (tempState.karaoke) {
					finalMode = KARAOKE;
				} else if (tempState.synced) {
					finalMode = SYNCED;
				} else if (tempState.unsynced) {
					finalMode = UNSYNCED;
				} else if (tempState.genius) {
					finalMode = GENIUS;
				}
			}
		}

		this.lyricsSource(tempState, finalMode);

		// if song changed one time
		if (tempState.uri !== this.state.uri || refresh) {
			// when a song starts for the first time and language-override is selected, the lyrics are converted to the specified language.
			// however, when switching it off again, the detected language needs to be known, so defaultLanguage has been introduced.
			const defaultLanguage = Utils.detectLanguage(this.state.currentLyrics);
			const language =
				CONFIG.visual["translate:detect-language-override"] !== "off" ? CONFIG.visual["translate:detect-language-override"] : defaultLanguage;
			const friendlyLanguage = language && new Intl.DisplayNames(["en"], { type: "language" }).of(language.split("-")[0])?.toLowerCase();
			const targetConvert = CONFIG.visual[`translation-mode:${friendlyLanguage}`];

			const isMemorey = CACHE[tempState.uri]?.[targetConvert];
			if (CONFIG.visual.translate && defaultLanguage && !isMemorey) {
				this.translateLyrics(language, this.state.currentLyrics, targetConvert).then((translated) => {
					const res = { [targetConvert]: translated };
					// Cache translated lyrics
					CACHE[tempState.uri] = { ...CACHE[tempState.uri], ...res };
					this.setState({ ...res });
				});
			}

			// reset and apply
			this.setState(
				{
					furigana: null,
					romaji: null,
					hiragana: null,
					katakana: null,
					hangul: null,
					romaja: null,
					cn: null,
					hk: null,
					tw: null,
					neteaseTranslation: null,
					...tempState,
					...translationOverrides,
					language: defaultLanguage,
				},
				() => {
					this.currentMusixmatchLanguage = CONFIG.visual["musixmatch-translation-language"];
					if (shouldRefreshMusixmatchTranslation) {
						this.refreshMusixmatchTranslation();
					}
				}
			);
			return;
		}

		this.setState({ ...tempState, ...translationOverrides }, () => {
			this.currentMusixmatchLanguage = CONFIG.visual["musixmatch-translation-language"];
			if (shouldRefreshMusixmatchTranslation) {
				this.refreshMusixmatchTranslation();
			}
		});
	}

	lyricsSource(lyricsState, mode) {
		if (!lyricsState) return;

		const lang = this.provideLanguageCode(this.state.currentLyrics);
		const friendlyLanguage = lang && new Intl.DisplayNames(["en"], { type: "language" }).of(lang.split("-")[0])?.toLowerCase();

		if (!this.displayMode) {
			this.displayMode = CONFIG.visual[`translation-mode:${friendlyLanguage}`];
		}

		// get original Lyrics
		const lyrics = lyricsState[CONFIG.modes[mode]];
		const translationSourceConfig = resolveTranslationSource(CONFIG.visual["translate:translated-lyrics-source"]);

		if (translationSourceConfig.language) {
			const translationLanguageKey = `${APP_NAME}:visual:musixmatch-translation-language`;
			const storedLanguage = localStorage.getItem(translationLanguageKey);

			if (storedLanguage !== translationSourceConfig.language) {
				localStorage.setItem(translationLanguageKey, translationSourceConfig.language);
			}

			if (CONFIG.visual["musixmatch-translation-language"] !== translationSourceConfig.language) {
				CONFIG.visual["musixmatch-translation-language"] = translationSourceConfig.language;
			}
		}

		if (CONFIG.visual.translate) {
			this.state.currentLyrics = lyricsState[CONFIG.visual[`translation-mode:${friendlyLanguage}`]] ?? lyrics;
		} else {
			this.state.currentLyrics = lyricsState[translationSourceConfig.key] ?? lyrics;
		}

		// Convert Mode re-fresh
		if (
			this.translate !== CONFIG.visual.translate ||
			this.languageOverride !== CONFIG.visual["translate:detect-language-override"] ||
			this.displayMode !== CONFIG.visual[`translation-mode:${friendlyLanguage}`]
		) {
			this.translate = CONFIG.visual.translate;
			this.languageOverride = CONFIG.visual["translate:detect-language-override"];
			this.displayMode = CONFIG.visual[`translation-mode:${friendlyLanguage}`];

			if (CONFIG.visual.translate) {
				const targetConvert = CONFIG.visual[`translation-mode:${friendlyLanguage}`];
				const isCached = CACHE[lyricsState.uri]?.[targetConvert];

				if (!isCached) {
					this.translateLyrics(lang, lyrics, targetConvert).then((translated) => {
						const res = { [targetConvert]: translated };
						// Cache translated lyrics
						CACHE[lyricsState.uri] = { ...CACHE[lyricsState.uri], ...res };
						this.setState({ ...this.state, ...res });
					});
				}
			} else {
				const resetCache = { furigana: null, romaji: null, hiragana: null, katakana: null, hangul: null, romaja: null, cn: null, hk: null, tw: null };
				CACHE[lyricsState.uri] = { ...CACHE[lyricsState.uri], ...resetCache };
			}
		}
	}

	provideLanguageCode(lyrics) {
		if (!lyrics) return;

		if (CONFIG.visual["translate:detect-language-override"] !== "off") {
			return CONFIG.visual["translate:detect-language-override"];
		}
		if (this.state.language) {
			return this.state.language;
		}
		return Utils.detectLanguage(lyrics);
	}

	async translateLyrics(language, lyrics, targetConvert) {
		if (!language) return;

		Spicetify.showNotification("Converting...", false, 1000);
		if (!this.translator) {
			this.translator = new Translator(language);
		}
		await this.translator.awaitFinished(language);

		let result;
		try {
			if (language === "ja") {
				// Japanese
				const map = {
					romaji: { target: "romaji", mode: "spaced" },
					furigana: { target: "hiragana", mode: "furigana" },
					hiragana: { target: "hiragana", mode: "normal" },
					katakana: { target: "katakana", mode: "normal" },
				};

				result = await Promise.all(
					lyrics.map(async (lyric) => await this.translator.romajifyText(lyric.text, map[targetConvert].target, map[targetConvert].mode))
				);
			} else if (language === "ko") {
				// Korean
				result = await Promise.all(lyrics.map(async (lyric) => await this.translator.convertToRomaja(lyric.text, "romaji")));
			} else if (language === "zh-hans") {
				// Chinese (Simplified)
				const map = {
					cn: { from: "cn", target: "cn" },
					tw: { from: "cn", target: "tw" },
					hk: { from: "cn", target: "hk" },
				};

				// prevent conversion between the same language.
				if (targetConvert === "cn") {
					Spicetify.showNotification("No conversion is needed", false, 1000);
					return lyrics;
				}

				result = await Promise.all(
					lyrics.map(async (lyric) => await this.translator.convertChinese(lyric.text, map[targetConvert].from, map[targetConvert].target))
				);
			} else if (language === "zh-hant") {
				// Chinese (Traditional)
				const map = {
					cn: { from: "t", target: "cn" },
					hk: { from: "t", target: "hk" },
					tw: { from: "t", target: "tw" },
				};

				// prevent conversion between the same language.
				if (targetConvert === "tw") {
					Spicetify.showNotification("No conversion is needed", false, 1000);
					return lyrics;
				}

				result = await Promise.all(
					lyrics.map(async (lyric) => await this.translator.convertChinese(lyric.text, map[targetConvert].from, map[targetConvert].target))
				);
			}

			const res = Utils.processTranslatedLyrics(result, lyrics);
			Spicetify.showNotification("Converting...", false, 0);
			return res;
		} catch (error) {
			Spicetify.showNotification("Convert Error!", true);
			console.error(error);
		}
	}

	resetDelay() {
		CONFIG.visual.delay = Number(localStorage.getItem(`lyrics-delay:${Spicetify.Player.data.item.uri}`)) || 0;
	}

	async onVersionChange(items, index) {
		if (this.state.mode === GENIUS) {
			this.setState({
				...emptyLine,
				genius2: this.state.genius2,
				isLoading: true,
			});
			const lyrics = await ProviderGenius.fetchLyricsVersion(items, index);
			this.setState({
				genius: lyrics,
				versionIndex: index,
				isLoading: false,
			});
		}
	}

	async onVersionChange2(items, index) {
		if (this.state.mode === GENIUS) {
			this.setState({
				...emptyLine,
				genius: this.state.genius,
				isLoading: true,
			});
			const lyrics = await ProviderGenius.fetchLyricsVersion(items, index);
			this.setState({
				genius2: lyrics,
				versionIndex2: index,
				isLoading: false,
			});
		}
	}

	saveLocalLyrics(uri, lyrics) {
		if (lyrics.genius) {
			lyrics.unsynced = lyrics.genius.split("<br>").map((lyc) => {
				return {
					text: lyc.replace(/<[^>]*>/g, ""),
				};
			});
			lyrics.genius = null;
		}

		const localLyrics = JSON.parse(localStorage.getItem(`${APP_NAME}:local-lyrics`)) || {};
		localLyrics[uri] = lyrics;
		localStorage.setItem(`${APP_NAME}:local-lyrics`, JSON.stringify(localLyrics));
		this.setState({ isCached: true });
	}

	deleteLocalLyrics(uri) {
		const localLyrics = JSON.parse(localStorage.getItem(`${APP_NAME}:local-lyrics`)) || {};
		delete localLyrics[uri];
		localStorage.setItem(`${APP_NAME}:local-lyrics`, JSON.stringify(localLyrics));
		console.log(localLyrics);
		this.setState({ isCached: false });
	}

	lyricsSaved(uri) {
		const localLyrics = JSON.parse(localStorage.getItem(`${APP_NAME}:local-lyrics`)) || {};
		return !!localLyrics[uri];
	}

	processLyricsFromFile(event) {
		const file = event.target.files;
		if (!file.length) return;
		const reader = new FileReader();

		if (file[0].size > 1024 * 1024) {
			Spicetify.showNotification("File too large", true);
			return;
		}

		reader.onload = (e) => {
			try {
				const localLyrics = Utils.parseLocalLyrics(e.target.result);
				const parsedKeys = Object.keys(localLyrics)
					.filter((key) => localLyrics[key])
					.map((key) => key[0].toUpperCase() + key.slice(1))
					.map((key) => `<strong>${key}</strong>`);

				if (!parsedKeys.length) {
					Spicetify.showNotification("Nothing to load", true);
					return;
				}

				this.setState({ ...localLyrics, provider: "local" });
				CACHE[this.currentTrackUri] = { ...localLyrics, provider: "local", uri: this.currentTrackUri };
				this.saveLocalLyrics(this.currentTrackUri, localLyrics);

				Spicetify.showNotification(`Loaded ${parsedKeys.join(", ")} lyrics from file`);
			} catch (e) {
				console.error(e);
				Spicetify.showNotification("Failed to load lyrics", true);
			}
		};

		reader.onerror = (e) => {
			console.error(e);
			Spicetify.showNotification("Failed to read file", true);
		};

		reader.readAsText(file[0]);
		event.target.value = "";
	}
	initMoustrap() {
		if (!this.mousetrap && Spicetify.Mousetrap) {
			this.mousetrap = new Spicetify.Mousetrap();
		}
	}

	componentDidMount() {
		this.onQueueChange = async ({ data: queue }) => {
			this.state.explicitMode = this.state.lockMode;
			this.currentTrackUri = queue.current.uri;
			this.fetchLyrics(queue.current, this.state.explicitMode);
			this.viewPort.scrollTo(0, 0);

			// Fetch next track
			const nextTrack = queue.queued?.[0] || queue.nextUp?.[0];
			const nextInfo = this.infoFromTrack(nextTrack);
			// Debounce next track fetch
			if (!nextInfo || nextInfo.uri === this.nextTrackUri) return;
			this.nextTrackUri = nextInfo.uri;
			this.tryServices(nextInfo, this.state.explicitMode).then((resp) => {
				if (resp.provider) {
					// Cache lyrics
					CACHE[resp.uri] = resp;
				}
			});
		};

		if (Spicetify.Player?.data?.item) {
			this.state.explicitMode = this.state.lockMode;
			this.currentTrackUri = Spicetify.Player.data.item.uri;
			this.fetchLyrics(Spicetify.Player.data.item, this.state.explicitMode);
		}

		this.updateVisualOnConfigChange();
		Utils.addQueueListener(this.onQueueChange);

		lyricContainerUpdate = () => {
			this.reRenderLyricsPage = !this.reRenderLyricsPage;
			this.updateVisualOnConfigChange();
			this.forceUpdate();

			if (this.currentMusixmatchLanguage !== CONFIG.visual["musixmatch-translation-language"]) {
				this.currentMusixmatchLanguage = CONFIG.visual["musixmatch-translation-language"];
				this.refreshMusixmatchTranslation();
			}
		};

		refreshMusixmatchTranslation = this.refreshMusixmatchTranslation.bind(this);

		reloadLyrics = () => {
			CACHE = {};
			this.updateVisualOnConfigChange();
			this.forceUpdate();
			this.fetchLyrics(Spicetify.Player.data.item, this.state.explicitMode, true);
		};

		this.viewPort =
			document.querySelector(".Root__main-view .os-viewport") ?? document.querySelector(".Root__main-view .main-view-container__scroll-node");

		this.configButton = new Spicetify.Menu.Item("Lyrics Plus config", false, openConfig, "lyrics");
		this.configButton.register();

		this.onFontSizeChange = (event) => {
			if (!event.ctrlKey) return;
			const dir = event.deltaY < 0 ? 1 : -1;
			let temp = CONFIG.visual["font-size"] + dir * fontSizeLimit.step;
			if (temp < fontSizeLimit.min) {
				temp = fontSizeLimit.min;
			} else if (temp > fontSizeLimit.max) {
				temp = fontSizeLimit.max;
			}
			CONFIG.visual["font-size"] = temp;
			localStorage.setItem("lyrics-plus:visual:font-size", temp);
			lyricContainerUpdate();
		};

		this.toggleFullscreen = () => {
			const isEnabled = !this.state.isFullscreen;
			if (isEnabled) {
				document.body.append(this.fullscreenContainer);
				document.documentElement.requestFullscreen();
				this.mousetrap.bind("esc", this.toggleFullscreen);
			} else {
				this.fullscreenContainer.remove();
				document.exitFullscreen();
				this.mousetrap.unbind("esc");
			}

			this.setState({
				isFullscreen: isEnabled,
			});
		};
		this.mousetrap.reset();
		this.mousetrap.bind(CONFIG.visual["fullscreen-key"], this.toggleFullscreen);
		window.addEventListener("fad-request", lyricContainerUpdate);
	}

	componentWillUnmount() {
		Utils.removeQueueListener(this.onQueueChange);
		this.configButton.deregister();
		this.mousetrap.reset();
		window.removeEventListener("fad-request", lyricContainerUpdate);
		refreshMusixmatchTranslation = null;
	}

	updateVisualOnConfigChange() {
		this.availableModes = CONFIG.modes.filter((_, id) => {
			return Object.values(CONFIG.providers).some((p) => p.on && p.modes.includes(id));
		});

		if (!CONFIG.visual.colorful) {
			this.styleVariables = {
				"--lyrics-color-active": CONFIG.visual["active-color"],
				"--lyrics-color-inactive": CONFIG.visual["inactive-color"],
				"--lyrics-color-background": CONFIG.visual["background-color"],
				"--lyrics-highlight-background": CONFIG.visual["highlight-color"],
				"--lyrics-background-noise": CONFIG.visual.noise ? "var(--background-noise)" : "unset",
			};
		}

		this.styleVariables = {
			...this.styleVariables,
			"--lyrics-align-text": CONFIG.visual.alignment,
			"--lyrics-font-size": `${CONFIG.visual["font-size"]}px`,
			"--animation-tempo": this.state.tempo,
		};

		this.mousetrap.reset();
		this.mousetrap.bind(CONFIG.visual["fullscreen-key"], this.toggleFullscreen);
	}

	render() {
		const fadLyricsContainer = document.getElementById("fad-lyrics-plus-container");
		this.state.isFADMode = !!fadLyricsContainer;

		if (this.state.isFADMode) {
			// Text colors will be set by FAD extension
			this.styleVariables = {};
		} else if (CONFIG.visual.colorful) {
			this.styleVariables = {
				"--lyrics-color-active": "white",
				"--lyrics-color-inactive": this.state.colors.inactive,
				"--lyrics-color-background": this.state.colors.background || "transparent",
				"--lyrics-highlight-background": this.state.colors.inactive,
				"--lyrics-background-noise": CONFIG.visual.noise ? "var(--background-noise)" : "unset",
			};
		}

		this.styleVariables = {
			...this.styleVariables,
			"--lyrics-align-text": CONFIG.visual.alignment,
			"--lyrics-font-size": `${CONFIG.visual["font-size"]}px`,
			"--animation-tempo": this.state.tempo,
		};

		let mode = -1;
		if (this.state.explicitMode !== -1) {
			mode = this.state.explicitMode;
		} else if (this.state.lockMode !== -1) {
			mode = this.state.lockMode;
		} else {
			// Auto switch
			if (this.state.karaoke) {
				mode = KARAOKE;
			} else if (this.state.synced) {
				mode = SYNCED;
			} else if (this.state.unsynced) {
				mode = UNSYNCED;
			} else if (this.state.genius) {
				mode = GENIUS;
			}
		}

		let activeItem;
		let showTranslationButton;

		this.lyricsSource(this.state, mode);
		const lang = this.provideLanguageCode(this.state.currentLyrics);
		const friendlyLanguage = lang && new Intl.DisplayNames(["en"], { type: "language" }).of(lang.split("-")[0])?.toLowerCase();
		const hasMusixmatchLanguages = Array.isArray(this.state.musixmatchAvailableTranslations) && this.state.musixmatchAvailableTranslations.length > 0;
		const hasTranslation = this.state.neteaseTranslation !== null || this.state.musixmatchTranslation !== null || hasMusixmatchLanguages;
		const hasPerformer = !!this.state.currentLyrics?.some((line) => line.performer);

		if (mode !== -1) {
			showTranslationButton = (friendlyLanguage || hasTranslation) && (mode === SYNCED || mode === UNSYNCED);

			if (mode === KARAOKE && this.state.karaoke) {
				activeItem = react.createElement(CONFIG.visual["synced-compact"] ? SyncedLyricsPage : SyncedExpandedLyricsPage, {
					isKara: true,
					trackUri: this.state.uri,
					lyrics: this.state.karaoke,
					provider: this.state.provider,
					copyright: this.state.copyright,
					reRenderLyricsPage: this.reRenderLyricsPage,
				});
			} else if (mode === SYNCED && this.state.synced) {
				activeItem = react.createElement(CONFIG.visual["synced-compact"] ? SyncedLyricsPage : SyncedExpandedLyricsPage, {
					trackUri: this.state.uri,
					lyrics: this.state.currentLyrics,
					provider: this.state.provider,
					copyright: this.state.copyright,
					reRenderLyricsPage: this.reRenderLyricsPage,
				});
			} else if (mode === UNSYNCED && this.state.unsynced) {
				activeItem = react.createElement(UnsyncedLyricsPage, {
					trackUri: this.state.uri,
					lyrics: this.state.currentLyrics,
					provider: this.state.provider,
					copyright: this.state.copyright,
					reRenderLyricsPage: this.reRenderLyricsPage,
				});
			} else if (mode === GENIUS && this.state.genius) {
				activeItem = react.createElement(GeniusPage, {
					isSplitted: CONFIG.visual["dual-genius"],
					trackUri: this.state.uri,
					lyrics: this.state.genius,
					provider: this.state.provider,
					copyright: this.state.copyright,
					versions: this.state.versions,
					versionIndex: this.state.versionIndex,
					onVersionChange: this.onVersionChange.bind(this),
					lyrics2: this.state.genius2,
					versionIndex2: this.state.versionIndex2,
					onVersionChange2: this.onVersionChange2.bind(this),
					reRenderLyricsPage: this.reRenderLyricsPage,
				});
			}
		}

		if (!activeItem) {
			activeItem = react.createElement(
				"div",
				{
					className: "lyrics-lyricsContainer-LyricsUnavailablePage",
				},
				react.createElement(
					"span",
					{
						className: "lyrics-lyricsContainer-LyricsUnavailableMessage",
					},
					this.state.isLoading ? LoadingIcon : "(• _ • )"
				)
			);
		}

		this.state.mode = mode;

		const out = react.createElement(
			"div",
			{
				className: `lyrics-lyricsContainer-LyricsContainer${CONFIG.visual["fade-blur"] ? " blur-enabled" : ""}${
					fadLyricsContainer ? " fad-enabled" : ""
				}`,
				style: this.styleVariables,
				ref: (el) => {
					if (!el) return;
					el.onmousewheel = this.onFontSizeChange;
				},
			},
			react.createElement("div", {
				className: "lyrics-lyricsContainer-LyricsBackground",
			}),
			react.createElement(
				"div",
				{
					className: "lyrics-config-button-container",
				},
				showTranslationButton &&
					react.createElement(TranslationMenu, {
						friendlyLanguage,
						hasTranslation: {
							musixmatch: this.state.musixmatchTranslation !== null,
							netease: this.state.neteaseTranslation !== null,
						},
						musixmatchLanguages: this.state.musixmatchAvailableTranslations || [],
						musixmatchSelectedLanguage: this.state.musixmatchTranslationLanguage || CONFIG.visual["musixmatch-translation-language"],
					}),
				react.createElement(AdjustmentsMenu, { mode, hasPerformer }),
				react.createElement(
					Spicetify.ReactComponent.TooltipWrapper,
					{
						label: this.state.isCached ? "Lyrics cached" : "Cache lyrics",
					},
					react.createElement(
						"button",
						{
							className: "lyrics-config-button",
							onClick: () => {
								const { synced, unsynced, karaoke, genius } = this.state;
								if (!synced && !unsynced && !karaoke && !genius) {
									Spicetify.showNotification("No lyrics to cache", true);
									return;
								}

								if (this.state.isCached) {
									this.deleteLocalLyrics(this.currentTrackUri);
									Spicetify.showNotification("Delete lyrics cache");
								} else {
									this.saveLocalLyrics(this.currentTrackUri, { synced, unsynced, karaoke, genius });
									Spicetify.showNotification("Lyrics cached");
								}
							},
						},
						react.createElement("svg", {
							width: 16,
							height: 16,
							viewBox: "0 0 16 16",
							fill: "currentColor",
							dangerouslySetInnerHTML: {
								__html: Spicetify.SVGIcons[this.state.isCached ? "downloaded" : "download"],
							},
						})
					)
				),
				react.createElement(
					Spicetify.ReactComponent.TooltipWrapper,
					{
						label: "Load lyrics from file",
					},
					react.createElement(
						"button",
						{
							className: "lyrics-config-button",
							onClick: () => {
								document.getElementById("lyrics-file-input").click();
							},
						},
						react.createElement("input", {
							type: "file",
							id: "lyrics-file-input",
							accept: ".lrc,.txt",
							onChange: this.processLyricsFromFile.bind(this),
							style: {
								display: "none",
							},
						}),
						react.createElement("svg", {
							width: 16,
							height: 16,
							viewBox: "0 0 16 16",
							fill: "currentColor",
							dangerouslySetInnerHTML: {
								__html: Spicetify.SVGIcons["plus-alt"],
							},
						})
					)
				)
			),
			activeItem,
			!!document.querySelector(".main-topBar-topbarContentWrapper") &&
				react.createElement(TopBarContent, {
					links: this.availableModes,
					activeLink: CONFIG.modes[mode],
					lockLink: CONFIG.modes[this.state.lockMode],
					switchCallback: (label) => {
						const mode = CONFIG.modes.findIndex((a) => a === label);
						if (mode !== this.state.mode) {
							// If explicitMode is not set, moving the topBar will apply the default mode value for the selected song.
							const info = this.infoFromTrack(Spicetify.Player.data.item);
							if (info?.uri && CACHE[info?.uri]) {
								CACHE[info.uri].mode = mode;
							}

							this.setState({ explicitMode: mode });
							this.state.provider !== "local" && this.fetchLyrics(Spicetify.Player.data.item, mode);
						}
					},
					lockCallback: (label) => {
						let mode = CONFIG.modes.findIndex((a) => a === label);
						if (mode === this.state.lockMode) {
							mode = -1;
						}
						this.setState({ explicitMode: mode, lockMode: mode });
						this.fetchLyrics(Spicetify.Player.data.item, mode);
						CONFIG.locked = mode;
						localStorage.setItem("lyrics-plus:lock-mode", mode);
					},
				})
		);

		if (this.state.isFullscreen) return Spicetify.ReactDOM.createPortal(out, this.fullscreenContainer);
		if (fadLyricsContainer) return Spicetify.ReactDOM.createPortal(out, fadLyricsContainer);
		return out;
	}
}


================================================
FILE: CustomApps/lyrics-plus/manifest.json
================================================
{
	"name": {
		"ms": "Lyrics",
		"gu": "Lyrics",
		"ko": "Lyrics",
		"pa-IN": "Lyrics",
		"az": "Lyrics",
		"ru": "Текст",
		"uk": "Lyrics",
		"nb": "Lyrics",
		"sv": "Låttext",
		"sw": "Lyrics",
		"ur": "Lyrics",
		"bho": "Lyrics",
		"pa-PK": "Lyrics",
		"te": "Lyrics",
		"ro": "Lyrics",
		"vi": "Lời bài hát",
		"am": "Lyrics",
		"bn": "Lyrics",
		"en": "Lyrics",
		"id": "Lirik",
		"bg": "Lyrics",
		"da": "Lyrics",
		"es-419": "Letras",
		"mr": "Lyrics",
		"ml": "Lyrics",
		"th": "เนื้อเพลง",
		"tr": "Şarkı Sözleri",
		"is": "Lyrics",
		"fa": "Lyrics",
		"or": "Lyrics",
		"he": "Lyrics",
		"hi": "Lyrics",
		"zh-TW": "歌詞",
		"sr": "Lyrics",
		"pt-BR": "Letra",
		"zu": "Lyrics",
		"nl": "Songteksten",
		"es": "Letra",
		"lt": "Lyrics",
		"ja": "歌詞",
		"st": "Lyrics",
		"it": "Lyrics",
		"el": "Στίχοι",
		"pt-PT": "Lyrics",
		"kn": "Lyrics",
		"de": "Songtext",
		"fr": "Paroles",
		"ne": "Lyrics",
		"ar": "الكلمات",
		"af": "Lyrics",
		"et": "Lyrics",
		"pl": "Tekst",
		"ta": "Lyrics",
		"sl": "Lyrics",
		"pk": "Lyrics",
		"hr": "Lyrics",
		"sk": "Lyrics",
		"fi": "Sanat",
		"lv": "Lyrics",
		"fil": "Lyrics",
		"fr-CA": "Paroles",
		"cs": "Text",
		"zh-CN": "歌词",
		"hu": "Dalszöveg"
	},
	"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>",
	"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>",
	"subfiles": [
		"ProviderNetease.js",
		"ProviderMusixmatch.js",
		"ProviderGenius.js",
		"ProviderLRCLIB.js",
		"Providers.js",
		"Pages.js",
		"OptionsMenu.js",
		"TabBar.js",
		"Utils.js",
		"Settings.js",
		"Translator.js"
	],
	"subfiles_extension": ["PlaybarButton.js"]
}


================================================
FILE: CustomApps/lyrics-plus/style.css
================================================
/*!
 * Bootstrap v3.3.7 (http://getbootstrap.com)
 * Copyright 2011-2016 Twitter,
 Inc.
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 */
/*!
 * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=35378cd201a131f69c68a64bc4438544)
 * Config saved to config.json and https://gist.github.com/35378cd201a131f69c68a64bc4438544
 */
@media (min-width: 768px) {
	.container {
		width: 750px;
	}
}

@media (min-width: 992px) {
	.container {
		width: 970px;
	}
}

@media (min-width: 1200px) {
	.container {
		width: 1170px;
	}
}

@media (min-width: 1500px) {
	.container {
		width: 1450px;
	}
}

.row {
	margin-left: -16px;
	margin-right: -16px;
}

.container:after,
.row:after {
	clear: both;
}

.hide {
	display: none !important;
}

.show {
	display: block !important;
}

.hidden {
	display: none !important;
}

.lyrics-lyricsContainer-LyricsContainer {
	display: grid;
	grid-template-rows: 1fr;
	position: absolute;
	height: 100%;
	width: 100%;
	top: 0;
}

.lyrics-lyricsContainer-Loading {
	align-self: center;
	grid-area: 1 / 1 / -1 / -1;
}

.lyrics-lyricsContainer-LyricsUnavailablePage {
	align-items: center;
	color: var(--lyrics-color-inactive);
	display: flex;
	grid-area: 1 / 1 / -1 / -1;
	height: 100%;
	justify-content: center;
	padding: 20px;
	font-size: 88px;
	letter-spacing: 0.1em;
	font-weight: 700;
}

.lyrics-lyricsContainer-UnsyncedLyricsPage {
	grid-area: 1 / 1 / -1 / -1;
	grid-template-rows: 1fr 20px;
	user-select: text;
	text-align: var(--lyrics-align-text);
}

.lyrics-lyricsContainer-LyricsUnsyncedPadding {
	display: flex;
	/* 2 padding blocks & 1 line height & Provider block */
	height: calc(50vh - 91px - 8px - var(--lyrics-font-size));
}
.lyrics-lyricsContainer-UnsyncedLyricsPage:has(.lyrics-versionSelector, .lyrics-lyricsContainer-LyricsLine:nth-child(4))
	.lyrics-lyricsContainer-LyricsUnsyncedPadding {
	height: 10vh;
}

.lyrics-lyricsContainer-SyncedLyricsPage {
	display: grid;
	grid-area: 1 / 1 / -1 / -1;
	grid-template-rows: 1fr 30px;
	overflow: hidden;
	text-align: var(--lyrics-align-text);
	user-select: text;
}

.lyrics-lyricsContainer-LyricsBackground {
	background-color: var(--lyrics-color-background);
	background-image: var(--lyrics-background-noise);
	grid-area: 1 / 1 / -1 / -1;
	transition: background-color 0.25s ease-out;
}

.lyrics-lyricsContainer-Provider {
	align-self: end;
	color: var(--lyrics-color-inactive);
	grid-area: 2 / 1 / -1 / -1;
	justify-self: stretch;
	height: 25px;
	overflow: hidden;
	background: linear-gradient(0deg, var(--lyrics-color-background) 30%, transparent);
	z-index: 1;
	padding: 60px 20px 30px;
	pointer-events: none;
}

.lyrics-lyricsContainer-SyncedLyrics {
	--lyrics-line-height: calc(4px + var(--lyrics-font-size));
	grid-area: 1 / 1 / -2 / -1;
	height: 0;
}

.lyrics-lyricsContainer-SyncedLyrics .lyrics-lyricsContainer-LyricsLine {
	transform: translateY(calc(var(--position-index) * var(--lyrics-line-height) + var(--offset)));
	transform-origin: var(--lyrics-align-text);
	transition-timing-function: cubic-bezier(0, 0, 0.58, 1);
	transition-duration: calc(var(--animation-index) * var(--animation-tempo) + 0.1s);
	transition-property: transform, color, opacity;
}

.lyrics-lyricsContainer-LyricsContainer.blur-enabled .lyrics-lyricsContainer-SyncedLyrics .lyrics-lyricsContainer-LyricsLine {
	filter: blur(calc(var(--blur-index) * 1.5px));
}

.lyrics-lyricsContainer-UnsyncedLyricsPage .lyrics-lyricsContainer-LyricsLine a {
	color: var(--lyrics-color-active);
}

.lyrics-lyricsContainer-UnsyncedLyricsPage .lyrics-lyricsContainer-LyricsLine {
	color: var(--lyrics-color-inactive);
	transition: color 0.25s cubic-bezier(0, 0, 0.58, 1);
}

.lyrics-lyricsContainer-UnsyncedLyricsPage .lyrics-lyricsContainer-LyricsLine-active {
	color: var(--lyrics-color-active);
}

.lyrics-lyricsContainer-UnsyncedLyricsPage .lyrics-lyricsContainer-LyricsLine:hover {
	color: var(--lyrics-color-active);
}

.lyrics-lyricsContainer-SyncedLyrics .lyrics-lyricsContainer-LyricsLine {
	color: var(--lyrics-color-inactive);
}

.lyrics-lyricsContainer-SyncedLyrics .lyrics-lyricsContainer-LyricsLine:hover {
	color: var(--lyrics-color-active);
}

.lyrics-lyricsContainer-SyncedLyrics .lyrics-lyricsContainer-LyricsLine.lyrics-lyricsContainer-LyricsLine-active {
	color: var(--lyrics-color-active);
	opacity: 1;
	transform: translateY(calc(var(--position-index) * var(--lyrics-line-height) + var(--offset))) scale(1.1);
	filter: none !important;
}

.lyrics-lyricsContainer-SyncedLyrics > .lyrics-lyricsContainer-LyricsLine-paddingLine {
	opacity: 0;
	pointer-events: none;
}

.lyrics-lyricsContainer-LyricsLine,
.lyrics-versionSelector {
	margin-left: 100px;
	margin-right: 100px;
}

@media (min-width: 1024px) {
	.lyrics-lyricsContainer-LyricsLine,
	.lyrics-versionSelector {
		margin-left: 150px;
		margin-right: 150px;
	}
}

@media (min-width: 1280px) {
	.lyrics-lyricsContainer-LyricsLine,
	.lyrics-versionSelector {
		margin-left: 200px;
		margin-right: 200px;
	}
}

.lyrics-lyricsContainer-UnsyncedLyricsPage .lyrics-lyricsContainer-LyricsLine {
	font-size: var(--lyrics-font-size);
	font-weight: 700;
	letter-spacing: -0.04em;
	line-height: calc(12px + var(--lyrics-font-size));
}

.lyrics-lyricsContainer-SyncedLyrics .lyrics-lyricsContainer-LyricsLine {
	font-size: var(--lyrics-font-size);
	font-weight: 700;
	letter-spacing: -0.04em;
	line-height: var(--lyrics-line-height);
}

@media (min-width: 1280px) {
	.lyrics-lyricsContainer-SyncedLyrics .lyrics-lyricsContainer-LyricsLine {
		font-weight: 900;
	}
}

.lyrics-tabBar-headerItem {
	-webkit-app-region: no-drag;
	display: inline-block;
	pointer-events: auto;
}

.lyrics-tabBar-headerItemLink {
	margin: 0 8px 0 0;
}

.lyrics-tabBar-active {
	background-color: var(--spice-tab-active);
	border-radius: 4px;
}

.lyrics-tabBar-headerItemLink {
	border-radius: 4px;
	color: var(--spice-text);
	display: inline-block;
	margin: 0 8px;
	padding: 8px 16px;
	position: relative;
	text-decoration: none !important;
	cursor: pointer;
}

.lyrics-tabBar-headerItemLink .main-type-mestoBold {
	text-transform: capitalize;
}

.lyrics-tabBar-headerItemLink-locked::before {
	content: "• ";
}

.lyrics-tabBar-nav {
	-webkit-app-region: drag;
	pointer-events: none;
	width: 100%;
}

.lyrics-tabBar-header {
	display: flex;
	flex-direction: row;
	flex-wrap: nowrap;
	align-items: center;
	justify-content: flex-start;
}

.lyrics-tabBar-headerItem .optionsMenu-dropBox {
	color: var(--spice-text);
	border: 0;
	max-width: 150px;
	height: 42px;
	padding: 0 30px 0 12px;
	background-color: initial;
	cursor: pointer;
	appearance: none;
}

.lyrics-tabBar-headerItem .optionsMenu-dropBox svg {
	position: absolute;
	margin-left: 8px;
}

#lyrics-plus-config-container option {
	background-color: var(--spice-button);
}

div.lyrics-tabBar-headerItemLink {
	padding: 0;
}

.lyrics-tabBar-header button.switch {
	margin-inline-end: 12px;
	margin-inline-start: 0;
}

.lyrics-lyricsContainer-Karaoke-WordActive {
	color: var(--lyrics-color-active) !important;
	background-position: top left !important;
}

.lyrics-lyricsContainer-LyricsLine:hover .lyrics-lyricsContainer-Karaoke-Word {
	background-position: top left;
}

.lyrics-lyricsContainer-Karaoke-Word {
	color: var(--lyrics-color-inactive);
	background-image: linear-gradient(
		to right,
		var(--lyrics-color-active),
		var(--lyrics-color-active) 45%,
		var(--lyrics-color-inactive) 55%,
		var(--lyrics-color-inactive)
	);
	-webkit-background-clip: text;
	-webkit-text-fill-color: transparent;
	background-size: 225% 100%;
	background-position: top left 100%;
	transition-property: color, background-position;
	transition-duration: calc(var(--word-duration) + 0.05s);
	transition-timing-function: linear;
}

.lyrics-lyricsContainer-LyricsLine a {
	background-color: transparent;
	transition: background-color 0.25s cubic-bezier(0, 0, 0, 1);
}

.lyrics-lyricsContainer-LyricsLine a.fetched {
	background-color: var(--lyrics-highlight-background);
}

.lyrics-lyricsContainer-LyricsLine a,
.lyrics-lyricsContainer-LyricsLine a:hover {
	text-decoration: none !important;
}

.lyrics-lyricsContainer-LyricsLine a:hover {
	border-bottom: 2px solid var(--lyrics-color-active);
}

.lyrics-Genius-noteTextContainer {
	font-size: 18px;
	font-weight: 400;
	letter-spacing: normal;
	line-height: 24px;
	text-transform: none;

	padding: 25px;
	background-color: var(--lyrics-color-active);
	border-radius: 3px;
	color: var(--lyrics-highlight-background);
	box-shadow: 0 10px 15px 5px rgb(0, 0, 0, 0.2);
	cursor: default;
	text-align: left;
}

.lyrics-Genius-divider {
	/* border-bottom: 3px solid var(--lyrics-color-active); */
	line-height: 0;
	margin-left: var(--link-left);
}

.lyrics-Searchbar {
	position: sticky;
	width: 300px;
	height: 40px;
	bottom: 10px;
	display: flex;
	background-color: var(--lyrics-color-active) !important;
	color: var(--lyrics-highlight-background);
	margin-left: 10px;
	border-radius: 3px;
	box-shadow: 0 10px 15px 5px rgb(0, 0, 0, 0.2);
}

.lyrics-Searchbar input {
	width: 300px;
	height: 40px;
	bottom: 10px;
	border: 0;
	color: var(--lyrics-highlight-background) !important;
	padding: 0 36px;
}

.lyrics-Searchbar svg {
	position: absolute;
	left: 0;
	height: 40px;
	margin-left: 10px;
}

.lyrics-Searchbar span {
	position: relative;
	right: 0;
	line-height: 40px;
	margin-right: 10px;
	font-weight: 400;
	font-size: 16px;
	letter-spacing: 0.2em;
}

.lyrics-Searchbar-highlight {
	position: fixed;
	width: 100%;
	height: var(--search-highlight-height);
	left: 0;
	top: var(--search-highlight-top);
	background-color: var(--lyrics-highlight-background);
	opacity: 0.5;
	pointer-events: none;
}

.lyrics-versionSelector {
	max-width: 500px;
	border-radius: 4px;
	display: inline-block;
	position: relative;
	cursor: pointer;
	box-shadow: 0 10px 15px 5px rgb(0, 0, 0, 0.2);
	background-color: var(--lyrics-highlight-background);
	margin-bottom: 75px;
}

.lyrics-versionSelector select {
	border: 0;
	border-radius: 4px;
	max-width: 500px;
	height: 42px;
	padding: 0 30px 0 12px;
	cursor: pointer;
	appearance: none;
	font-size: 18px;
	background-color: var(--lyrics-color-active);
	color: var(--lyrics-highlight-background);
}
.lyrics-versionSelector option {
	background-color: var(--lyrics-color-active);
}

.lyrics-versionSelector svg {
	position: absolute;
	height: 42px;
	right: 10px;
	pointer-events: none;
	fill: var(--lyrics-highlight-background);
}

/** Setting menu */
.lyrics-tooltip-wrapper .setting-row::after,
#lyrics-plus-config-container .setting-row::after {
	content: "";
	display: table;
	clear: both;
}
.lyrics-tooltip-wrapper .setting-row .col,
#lyrics-plus-config-container .setting-row .col {
	padding: 16px 0 4px;
	align-items: center;
}
.lyrics-tooltip-wrapper .setting-row .col.description,
#lyrics-plus-config-container .setting-row .col.description {
	float: left;
	padding-right: 15px;
	cursor: default;
}
.lyrics-tooltip-wrapper .setting-row .col.action,
#lyrics-plus-config-container .setting-row .col.action {
	float: right;
	display: flex;
	justify-content: flex-end;
	align-items: center;
}
.lyrics-tooltip-wrapper button.switch,
#lyrics-plus-config-container button.switch {
	align-items: center;
	border: 0px;
	border-radius: 50%;
	background-color: rgba(var(--spice-rgb-shadow), 0.7);
	color: var(--spice-text);
	cursor: pointer;
	margin-inline-start: 12px;
	padding: 8px;
	width: 32px;
	height: 32px;
}
.lyrics-tooltip-wrapper button.switch.disabled,
.lyrics-tooltip-wrapper button.switch[disabled],
#lyrics-plus-config-container button.switch.disabled,
#lyrics-plus-config-container button.switch[disabled] {
	color: rgba(var(--spice-rgb-text), 0.3);
}
.lyrics-tooltip-wrapper button.switch.small,
#lyrics-plus-config-container button.switch.small {
	width: 22px;
	height: 22px;
	padding: 3px;
}

.lyrics-tooltip-wrapper input,
#lyrics-plus-config-container input {
	width: 100%;
	margin-top: 10px;
	padding: 0 5px;
	height: 32px;
	border: 0;
	color: var(--spice-text);
	background-color: initial;
	border-bottom: 1px solid var(--spice-text);
}
.lyrics-tooltip-wrapper .col.action .adjust-value,
#lyrics-plus-config-container .col.action .adjust-value {
	margin-inline-start: 12px;
	min-width: 22px;
	text-align: center;
}

.lyrics-tooltip-wrapper .col.action span,
#lyrics-plus-config-container .col.action span {
	font-size: 14px;
	opacity: 0.8;
}
.lyrics-tooltip-wrapper .col.action .btn,
#lyrics-plus-config-container .col.action .btn {
	font-weight: 700;
	background-color: transparent;
	border-radius: 500px;
	transition-duration: 33ms;
	transition-property: background-color, border-color, color, box-shadow, filter, transform;
	padding-inline: 15px;
	border: 1px solid #727272;
	color: var(--spice-text);
	min-block-size: 32px;
	cursor: pointer;
}

.lyrics-tooltip-wrapper .col.action .btn:hover,
#lyrics-plus-config-container .col.action .btn:hover {
	transform: scale(1.04);
	border-color: var(--spice-text);
}

.lyrics-tooltip-wrapper .col.action .btn:disabled,
#lyrics-plus-config-container .col.action .btn:disabled {
	opacity: 0.5;
	cursor: not-allowed;
}
.lyrics-tooltip-wrapper .col.action .main-dropDown-dropDown,
.lyrics-tooltip-wrapper .col.action input,
#lyrics-plus-config-container .col.action .main-dropDown-dropDown,
#lyrics-plus-config-container .col.action input {
	width: 150px;
}

#lyrics-fullscreen-container {
	position: fixed;
	width: 100vw;
	height: 100vh;
	cursor: default;
	left: 0;
	top: 0;
}

#lyrics-fullscreen-container .lyrics-lyricsContainer-LyricsContainer {
	height: 100vh;
	margin-bottom: 0;
	margin-top: 0;
	overflow-y: auto;
}

#lyrics-fullscreen-container .lyrics-lyricsContainer-LyricsContainer::-webkit-scrollbar {
	background-color: var(--lyrics-color-background);
}

.lyrics-lyricsContainer-LyricsContainer.fad-enabled {
	height: 100vh;
	margin-top: 0;
	margin-bottom: 0;
	overflow-y: scroll;
}

.lyrics-lyricsContainer-LyricsContainer.fad-enabled .lyrics-lyricsContainer-LyricsLine {
	margin-left: 100px;
	margin-right: 100px;
}

.lyrics-lyricsContainer-LyricsContainer.fad-enabled .lyrics-lyricsContainer-LyricsBackground,
.lyrics-lyricsContainer-LyricsContainer.fad-enabled .lyrics-lyricsContainer-Provider {
	display: none;
}

.lyrics-lyricsContainer-LyricsContainer.fad-enabled .lyrics-lyricsContainer-SyncedLyricsPage {
	width: 100%;
}

.lyrics-lyricsContainer-LyricsContainer.fad-enabled .lyrics-config-button-container {
	opacity: 0;
	transition: opacity 0.2s cubic-bezier(0, 0, 0.58, 1);
}

.lyrics-lyricsContainer-LyricsContainer.fad-enabled:hover .lyrics-config-button-container {
	opacity: 1;
}

.lyrics-idling-indicator {
	display: inline-block;
	opacity: 1;
	transition: opacity 0.2s cubic-bezier(0, 0, 0.58, 1);
}

.lyrics-idling-indicator-hidden {
	opacity: 0;
}

.lyrics-idling-indicator__circle {
	background-color: var(--lyrics-color-active);
	border-radius: 50%;
	display: inline-block;
	opacity: 0.5;
	margin-right: calc(var(--lyrics-font-size) / 4);

	transform-origin: center;
	transition-timing-function: linear;
	transition-duration: var(--indicator-delay);
	transition-property: transform, opacity;
	height: var(--lyrics-font-size);
	width: var(--lyrics-font-size);
	transform: scale(0.5);
}
.lyrics-idling-indicator__circle.active {
	opacity: 1;
	transform: scale(0.7);
}

.lyrics-config-button-container {
	-webkit-margin-end: 32px;
	-webkit-box-pack: end;
	pointer-events: none;
	bottom: 32px;
	display: flex;
	justify-content: flex-end;
	margin: -52px 0 0;
	margin-inline-end: 32px;
	position: sticky;
	z-index: 2;
}

.lyrics-config-button-container > * {
	pointer-events: auto;
}

@-webkit-keyframes spin {
	0% {
		-webkit-transform: rotate(0deg);
	}
	100% {
		-webkit-transform: rotate(360deg);
	}
}

.lyrics-config-button {
	align-items: center;
	background-color: rgba(0, 0, 0, 0.5);
	border: 0;
	margin: 5px;
	border-radius: 4px;
	color: #eee;
	cursor: pointer;
	display: flex;
	gap: 8px;
	justify-content: center;
	padding: 12px;
	height: 40px;
	width: 40px;
}

.lyrics-config-button-container .main-contextMenu-menu {
	color: var(--spice-text);
	padding: 12px 12px 6px;
}

.lyrics-lyricsContainer-UnsyncedLyricsPage .split {
	display: flex;
}
.lyrics-lyricsContainer-UnsyncedLyricsPage .split > div {
	flex: 50%;
}
.lyrics-lyricsContainer-UnsyncedLyricsPage .split > div > div:not(.lyrics-versionSelector) {
	margin-left: 0;
	margin-right: 0;
}
.split .lyrics-lyricsContainer-LyricsLine {
	padding-left: 50px;
	padding-right: 50px;
}
.split .lyrics-versionSelector {
	margin-right: 50px;
	margin-left: 50px;
}
.split .lyrics-versionSelector select {
	width: 100%;
}
.main-content-view {
	height: 100%;
}

@media (min-width: 1024px) {
	.split .lyrics-lyricsContainer-LyricsLine {
		padding-left: 75px;
		padding-right: 75px;
	}
	.split .lyrics-versionSelector {
		margin-right: 75px;
		margin-left: 75px;
	}
}

@media (min-width: 1280px) {
	.split .lyrics-lyricsContainer-LyricsLine {
		padding-left: 100px;
		padding-right: 100px;
	}
	.split .lyrics-versionSelector {
		margin-right: 100px;
		margin-left: 100px;
	}
}

.lyrics-lyricsContainer-Performer {
	display: block;
	font-size: 0.6em;
	opacity: 0.7;
	line-height: 1.2em;
	/* color: var(--lyrics-color-inactive); */
}


================================================
FILE: CustomApps/new-releases/Card.js
================================================
function DraggableComponent({ uri, title, children }) {
	const dragHandler = Spicetify.ReactHook.DragHandler?.([uri], title);
	return dragHandler
		? react.cloneElement(children, {
				onDragStart: dragHandler,
				draggable: "true",
			})
		: children;
}

class Card extends react.Component {
	constructor(props) {
		super(props);
		Object.assign(this, props);
		this.href = URI.fromString(this.uri).toURLPath(true);
		this.artistHref = URI.fromString(this.artist.uri).toURLPath(true);
		const uriType = Spicetify.URI.fromString(this.uri)?.type;
		switch (uriType) {
			case Spicetify.URI.Type.ALBUM:
			case Spicetify.URI.Type.TRACK:
				this.menuType = Spicetify.ReactComponent.AlbumMenu;
				break;
		}
		this.menuType = this.menuType || "div";
	}

	play(event) {
		Spicetify.Player.playUri(this.uri, this.context);
		event.stopPropagation();
	}

	closeButtonClicked(event) {
		event.stopPropagation();

		removeCards(this.props.uri);

		Spicetify.Snackbar.enqueueCustomSnackbar
			? Spicetify.Snackbar.enqueueCustomSnackbar("dismissed-release", {
					keyPrefix: "dismissed-release",
					children: Spicetify.ReactComponent.Snackbar.wrapper({
						children: Spicetify.ReactComponent.Snackbar.simpleLayout({
							leading: Spicetify.ReactComponent.Snackbar.styledImage({
								src: this.props.imageURL,
								imageHeight: "24px",
								imageWidth: "24px",
							}),
							center: Spicetify.React.createElement("div", {
								dangerouslySetInnerHTML: {
									__html: `Dismissed <b>${this.title}</b>.`,
								},
							}),
							trailing: Spicetify.ReactComponent.Snackbar.ctaText({
								ctaText: "Undo",
								onCtaClick: () => removeCards(this.props.uri, "undo"),
							}),
						}),
					}),
				})
			: Spicetify.showNotification(`Dismissed <b>${this.title}</b> from <br>${this.artist.name}</b>`);
	}

	render() {
		const detail = [];
		this.visual.type && detail.push(this.type);
		if (this.visual.count && this.trackCount) {
			detail.push(Spicetify.Locale.get("tracklist-header.songs-counter", this.trackCount));
		}

		return react.createElement(
			Spicetify.ReactComponent.RightClickMenu || "div",
			{
				menu: react.createElement(this.menuType, { uri: this.uri }),
			},
			react.createElement(
				"div",
				{
					className: "main-card-card",
					onClick: (event) => {
						History.push(this.href);
						event.preventDefault();
					},
				},
				react.createElement(
					DraggableComponent,
					{
						uri: this.uri,
						title: this.title,
					},
					react.createElement(
						"div",
						{
							className: "main-card-draggable",
						},
						react.createElement(
							"div",
							{
								className: "main-card-imageContainer",
							},
							react.createElement(
								"div",
								{
									className: "main-cardImage-imageWrapper",
								},
								react.createElement(
									"div",
									{},
									react.createElement("img", {
										"aria-hidden": "false",
										draggable: "false",
										loading: "lazy",
										src: this.imageURL,
										className: "main-image-image main-cardImage-image",
									})
								)
							),
							react.createElement(
								"div",
								{
									className: "main-card-PlayButtonContainer",
								},
								react.createElement(
									"div",
									{
										className: "main-playButton-PlayButton main-playButton-primary",
										"aria-label": Spicetify.Locale.get("play"),
										style: { "--size": "40px" },
										onClick: this.play.bind(this),
									},
									react.createElement(
										"button",
										null,
										react.createElement(
											"span",
											null,
											react.createElement(
												"svg",
												{
													height: "24",
													role: "img",
													wi
Download .txt
gitextract_d9xm56qv/

├── .coderabbit.yaml
├── .github/
│   ├── dependabot.yml
│   ├── labeler.yml
│   └── workflows/
│       ├── build.yml
│       ├── labeler.yml
│       ├── linter.yml
│       └── lintpr.yml
├── .gitignore
├── .vscode/
│   ├── extensions.json
│   └── settings.json
├── CONTRIBUTING.md
├── CustomApps/
│   ├── lyrics-plus/
│   │   ├── OptionsMenu.js
│   │   ├── Pages.js
│   │   ├── PlaybarButton.js
│   │   ├── ProviderGenius.js
│   │   ├── ProviderLRCLIB.js
│   │   ├── ProviderMusixmatch.js
│   │   ├── ProviderNetease.js
│   │   ├── Providers.js
│   │   ├── README.md
│   │   ├── Settings.js
│   │   ├── TabBar.js
│   │   ├── Translator.js
│   │   ├── Utils.js
│   │   ├── index.js
│   │   ├── manifest.json
│   │   └── style.css
│   ├── new-releases/
│   │   ├── Card.js
│   │   ├── Icons.js
│   │   ├── Settings.js
│   │   ├── index.js
│   │   ├── manifest.json
│   │   └── style.css
│   └── reddit/
│       ├── Card.js
│       ├── Icons.js
│       ├── OptionsMenu.js
│       ├── Settings.js
│       ├── SortBox.js
│       ├── TabBar.js
│       ├── index.js
│       ├── manifest.json
│       └── style.css
├── Extensions/
│   ├── autoSkipExplicit.js
│   ├── autoSkipVideo.js
│   ├── bookmark.js
│   ├── fullAppDisplay.js
│   ├── keyboardShortcut.js
│   ├── loopyLoop.js
│   ├── popupLyrics.js
│   ├── shuffle+.js
│   ├── trashbin.js
│   └── webnowplaying.js
├── LICENSE
├── README.md
├── Themes/
│   └── SpicetifyDefault/
│       ├── color.ini
│       └── user.css
├── biome.json
├── css-map.json
├── globals.d.ts
├── go.mod
├── go.sum
├── install.ps1
├── install.sh
├── jsHelper/
│   ├── expFeatures.js
│   ├── homeConfig.js
│   ├── sidebarConfig.js
│   └── spicetifyWrapper.js
├── manifest.json
├── spicetify.go
└── src/
    ├── apply/
    │   └── apply.go
    ├── backup/
    │   └── backup.go
    ├── cmd/
    │   ├── apply.go
    │   ├── auto.go
    │   ├── backup.go
    │   ├── block-updates.go
    │   ├── cmd.go
    │   ├── color.go
    │   ├── config-dir.go
    │   ├── config.go
    │   ├── devtools.go
    │   ├── patch.go
    │   ├── path.go
    │   ├── restart.go
    │   ├── update.go
    │   └── watch.go
    ├── preprocess/
    │   └── preprocess.go
    ├── status/
    │   ├── backup/
    │   │   └── backup.go
    │   └── spotify/
    │       └── spotify.go
    └── utils/
        ├── color.go
        ├── config.go
        ├── file-utils.go
        ├── isAdmin/
        │   ├── unix.go
        │   └── windows.go
        ├── path-utils.go
        ├── print.go
        ├── scanner.go
        ├── show-dir.go
        ├── utils.go
        ├── vcs.go
        └── watcher.go
Download .txt
SYMBOL INDEX (688 symbols across 67 files)

FILE: CustomApps/lyrics-plus/OptionsMenu.js
  function getMusixmatchTranslationPrefix (line 89) | function getMusixmatchTranslationPrefix() {

FILE: CustomApps/lyrics-plus/Pages.js
  class SearchBar (line 255) | class SearchBar extends react.Component {
    method constructor (line 256) | constructor() {
    method componentDidMount (line 266) | componentDidMount() {
    method componentWillUnmount (line 309) | componentWillUnmount() {
    method getNodeFromInput (line 317) | getNodeFromInput(event) {
    method render (line 355) | render() {
  function isInViewport (line 402) | function isInViewport(element) {
  function showNote (line 646) | function showNote(parent, note) {

FILE: CustomApps/lyrics-plus/PlaybarButton.js
  function setPlaybarButton (line 39) | function setPlaybarButton() {
  function removePlaybarButton (line 44) | function removePlaybarButton() {

FILE: CustomApps/lyrics-plus/ProviderGenius.js
  function getChildDeep (line 2) | function getChildDeep(parent, isDeep = false) {
  function getNote (line 22) | async function getNote(id) {
  function fetchHTML (line 50) | function fetchHTML(url) {
  function fetchLyricsVersion (line 66) | async function fetchLyricsVersion(results, index) {
  function fetchLyrics (line 96) | async function fetchLyrics(info) {

FILE: CustomApps/lyrics-plus/ProviderLRCLIB.js
  function findLyrics (line 2) | async function findLyrics(info) {
  function getUnsynced (line 32) | function getUnsynced(body) {
  function getSynced (line 42) | function getSynced(body) {

FILE: CustomApps/lyrics-plus/ProviderMusixmatch.js
  function findTranslationStatus (line 7) | function findTranslationStatus(body) {
  function findLyrics (line 37) | async function findLyrics(info) {
  function parsePerformerData (line 94) | function parsePerformerData(meta) {
  function matchSequential (line 165) | function matchSequential(lyricsLines, snippetQueue, getTextCallback = (l...
  function getKaraoke (line 218) | async function getKaraoke(body) {
  function getSynced (line 294) | function getSynced(body) {
  function getUnsynced (line 334) | function getUnsynced(body) {
  function getTranslation (line 372) | async function getTranslation(trackId) {
  function getLanguages (line 408) | async function getLanguages() {

FILE: CustomApps/lyrics-plus/ProviderNetease.js
  function findLyrics (line 6) | async function findLyrics(info) {
  function containCredits (line 38) | function containCredits(text) {
  function parseTimestamp (line 42) | function parseTimestamp(line) {
  function breakdownLine (line 67) | function breakdownLine(text) {
  function getKaraoke (line 82) | function getKaraoke(list) {
  function getSynced (line 116) | function getSynced(list) {
  function getTranslation (line 149) | function getTranslation(list) {
  function getUnsynced (line 180) | function getUnsynced(list) {

FILE: CustomApps/lyrics-plus/Settings.js
  function adjust (line 277) | function adjust(dir) {
  function record (line 329) | function record() {
  function finishRecord (line 343) | function finishRecord() {
  function openConfig (line 550) | function openConfig() {

FILE: CustomApps/lyrics-plus/TabBar.js
  class TabBarItem (line 1) | class TabBarItem extends react.Component {
    method onSelect (line 2) | onSelect(event) {
    method onLock (line 6) | onLock(event) {
    method render (line 10) | render() {
  function onLock (line 42) | function onLock(event) {

FILE: CustomApps/lyrics-plus/Translator.js
  class Translator (line 8) | class Translator {
    method constructor (line 9) | constructor(lang, isUsingNetease = false) {
    method includeExternal (line 22) | includeExternal(url) {
    method injectExternals (line 31) | injectExternals(lang) {
    method awaitFinished (line 46) | async awaitFinished(language) {
    method applyKuromojiFix (line 65) | applyKuromojiFix() {
    method createTranslator (line 77) | async createTranslator(lang) {
    method romajifyText (line 117) | async romajifyText(text, target = "romaji", mode = "spaced") {
    method convertToRomaja (line 129) | async convertToRomaja(text, target) {
    method convertChinese (line 139) | async convertChinese(text, from, target) {
    method #sleep (line 159) | static async #sleep(ms) {

FILE: CustomApps/lyrics-plus/Utils.js
  method addQueueListener (line 2) | addQueueListener(callback) {
  method removeQueueListener (line 5) | removeQueueListener(callback) {
  method convertIntToRGB (line 8) | convertIntToRGB(colorInt, div = 1) {
  method normalize (line 21) | normalize(s, emptySymbol = true) {
  method containsHanCharacter (line 48) | containsHanCharacter(s) {
  method translator (line 57) | set translator(translator) {
  method toSimplifiedChinese (line 71) | async toSimplifiedChinese(s) {
  method removeSongFeat (line 79) | removeSongFeat(s) {
  method removeExtraInfo (line 87) | removeExtraInfo(s) {
  method capitalize (line 90) | capitalize(s) {
  method detectLanguage (line 93) | detectLanguage(lyrics) {
  method processTranslatedLyrics (line 135) | processTranslatedLyrics(translated, original) {
  method processTranslatedOriginalLyrics (line 143) | processTranslatedOriginalLyrics(lyrics, synced) {
  method rubyTextToOriginalReact (line 169) | rubyTextToOriginalReact(translated, syncedText) {
  method rubyTextToReact (line 173) | rubyTextToReact(s) {
  method formatTime (line 188) | formatTime(timestamp) {
  method formatTextWithTimestamps (line 198) | formatTextWithTimestamps(text, startTime = 0) {
  method convertParsedToLRC (line 222) | convertParsedToLRC(lyrics, isBelow) {
  method convertParsedToUnsynced (line 242) | convertParsedToUnsynced(lyrics, isBelow) {
  method parseLocalLyrics (line 275) | parseLocalLyrics(lyrics) {
  method processLyrics (line 333) | processLyrics(lyrics) {

FILE: CustomApps/lyrics-plus/index.js
  function render (line 13) | function render() {
  function getConfig (line 17) | function getConfig(name, defaultVal = true) {
  constant APP_NAME (line 22) | const APP_NAME = "lyrics-plus";
  constant MUSIXMATCH_TRANSLATION_PREFIX_DEFAULT (line 23) | const MUSIXMATCH_TRANSLATION_PREFIX_DEFAULT = "musixmatchTranslation:";
  constant MUSIXMATCH_TRANSLATION_PREFIX_GLOBAL_KEY (line 24) | const MUSIXMATCH_TRANSLATION_PREFIX_GLOBAL_KEY = "__lyricsPlusMusixmatch...
  constant MUSIXMATCH_TRANSLATION_FETCH_MESSAGE (line 25) | const MUSIXMATCH_TRANSLATION_FETCH_MESSAGE = "Fetching translation...";
  constant MUSIXMATCH_TRANSLATION_FETCH_FAILED_MESSAGE (line 26) | const MUSIXMATCH_TRANSLATION_FETCH_FAILED_MESSAGE = "Failed to fetch tra...
  constant MUSIXMATCH_TRANSLATION_PREFIX (line 27) | const MUSIXMATCH_TRANSLATION_PREFIX =
  constant KARAOKE (line 36) | const KARAOKE = 0;
  constant SYNCED (line 37) | const SYNCED = 1;
  constant UNSYNCED (line 38) | const UNSYNCED = 2;
  constant GENIUS (line 39) | const GENIUS = 3;
  constant CONFIG (line 41) | const CONFIG = {
  constant CACHE (line 155) | let CACHE = {};
  function resolveTranslationSource (line 177) | function resolveTranslationSource(source) {
  class LyricsContainer (line 190) | class LyricsContainer extends react.Component {
    method constructor (line 191) | constructor() {
    method infoFromTrack (line 251) | infoFromTrack(track) {
    method fetchColors (line 266) | async fetchColors(uri) {
    method fetchTempo (line 290) | async fetchTempo(uri) {
    method refreshMusixmatchTranslation (line 311) | async refreshMusixmatchTranslation() {
    method tryServices (line 436) | async tryServices(trackInfo, mode = -1) {
    method fetchLyrics (line 493) | async fetchLyrics(track, mode = -1, refresh = false) {
    method lyricsSource (line 638) | lyricsSource(lyricsState, mode) {
    method provideLanguageCode (line 700) | provideLanguageCode(lyrics) {
    method translateLyrics (line 712) | async translateLyrics(language, lyrics, targetConvert) {
    method resetDelay (line 783) | resetDelay() {
    method onVersionChange (line 787) | async onVersionChange(items, index) {
    method onVersionChange2 (line 803) | async onVersionChange2(items, index) {
    method saveLocalLyrics (line 819) | saveLocalLyrics(uri, lyrics) {
    method deleteLocalLyrics (line 835) | deleteLocalLyrics(uri) {
    method lyricsSaved (line 843) | lyricsSaved(uri) {
    method processLyricsFromFile (line 848) | processLyricsFromFile(event) {
    method initMoustrap (line 890) | initMoustrap() {
    method componentDidMount (line 896) | componentDidMount() {
    method componentWillUnmount (line 987) | componentWillUnmount() {
    method updateVisualOnConfigChange (line 995) | updateVisualOnConfigChange() {
    method render (line 1021) | render() {

FILE: CustomApps/new-releases/Card.js
  function DraggableComponent (line 1) | function DraggableComponent({ uri, title, children }) {
  class Card (line 11) | class Card extends react.Component {
    method constructor (line 12) | constructor(props) {
    method play (line 27) | play(event) {
    method closeButtonClicked (line 32) | closeButtonClicked(event) {
    method render (line 62) | render() {

FILE: CustomApps/new-releases/Icons.js
  class LoadMoreIcon (line 73) | class LoadMoreIcon extends react.Component {
    method render (line 74) | render() {

FILE: CustomApps/new-releases/Settings.js
  function openConfig (line 170) | function openConfig() {

FILE: CustomApps/new-releases/index.js
  function render (line 16) | function render() {
  function getConfig (line 20) | function getConfig(name, defaultVal = true) {
  constant APP_NAME (line 25) | const APP_NAME = "new-releases";
  constant CONFIG (line 27) | const CONFIG = {
  constant DAY_DIVIDER (line 59) | const DAY_DIVIDER = 24 * 3600 * 1000;
  class Grid (line 72) | class Grid extends react.Component {
    method constructor (line 75) | constructor() {
    method updatePostsVisual (line 83) | updatePostsVisual() {
    method removeCards (line 115) | removeCards(id, type) {
    method reload (line 134) | async reload() {
    method componentDidMount (line 208) | async componentDidMount() {
    method componentWillUnmount (line 233) | componentWillUnmount() {
    method render (line 239) | render() {
  function getArtistList (line 279) | async function getArtistList() {
  function getArtistEverything (line 292) | async function getArtistEverything(artist) {
  function getPodcastList (line 335) | async function getPodcastList() {
  function getPodcastRelease (line 340) | async function getPodcastRelease(uri) {
  function metaFromTrack (line 345) | function metaFromTrack(artist, track) {
  function fetchTracks (line 371) | async function fetchTracks() {
  function fetchPodcasts (line 385) | async function fetchPodcasts() {

FILE: CustomApps/reddit/Card.js
  class Card (line 1) | class Card extends react.Component {
    method constructor (line 2) | constructor(props) {
    method play (line 28) | play(event) {
    method getSubtitle (line 33) | getSubtitle() {
    method getFollowers (line 72) | getFollowers() {
    method render (line 85) | render() {

FILE: CustomApps/reddit/Icons.js
  class LoadingIcon (line 1) | class LoadingIcon extends react.Component {
    method render (line 2) | render() {
  class LoadMoreIcon (line 77) | class LoadMoreIcon extends react.Component {
    method render (line 78) | render() {

FILE: CustomApps/reddit/Settings.js
  function openConfig (line 3) | function openConfig() {
  function createSlider (line 118) | function createSlider(name, key) {
  function createServiceOption (line 143) | function createServiceOption(id, posCallback, removeCallback) {

FILE: CustomApps/reddit/SortBox.js
  class SortBox (line 1) | class SortBox extends react.Component {
    method constructor (line 2) | constructor(props) {
    method render (line 21) | render() {

FILE: CustomApps/reddit/TabBar.js
  class TabBarItem (line 1) | class TabBarItem extends react.Component {
    method render (line 2) | render() {

FILE: CustomApps/reddit/index.js
  function render (line 17) | function render() {
  constant CONFIG (line 21) | const CONFIG = {
  class Grid (line 64) | class Grid extends react.Component {
    method constructor (line 67) | constructor(props) {
    method newRequest (line 78) | newRequest(amount) {
    method appendCard (line 85) | appendCard(item) {
    method updateSort (line 91) | updateSort(sortByValue, sortTimeValue) {
    method updateTabs (line 113) | updateTabs() {
    method updatePostsVisual (line 119) | updatePostsVisual() {
    method switchTo (line 126) | switchTo(value) {
    method loadPage (line 141) | async loadPage(queue) {
    method loadAmount (line 175) | async loadAmount(queue, quantity = 50) {
    method loadMore (line 195) | loadMore() {
    method componentDidMount (line 201) | async componentDidMount() {
    method componentWillUnmount (line 223) | componentWillUnmount() {
    method isScrolledBottom (line 231) | isScrolledBottom(event) {
    method render (line 239) | render() {
  function getSubreddit (line 299) | async function getSubreddit(after = "") {
  function fetchPlaylist (line 312) | async function fetchPlaylist(post) {
  function fetchAlbum (line 337) | async function fetchAlbum(post) {
  function fetchTrack (line 362) | async function fetchTrack(post) {
  function postMapper (line 379) | function postMapper(posts) {

FILE: Extensions/bookmark.js
  class BookmarkCollection (line 22) | class BookmarkCollection {
    method constructor (line 23) | constructor() {
    method apply (line 36) | apply() {
    method isTrack (line 61) | isTrack(uri) {
    method getStorage (line 65) | getStorage() {
    method addToStorage (line 78) | addToStorage(data) {
    method removeFromStorage (line 89) | removeFromStorage(id) {
    method changePosition (line 96) | changePosition(x, y) {
    method storeScroll (line 101) | storeScroll() {
    method setScroll (line 105) | setScroll() {
  class CardContainer (line 110) | class CardContainer extends HTMLElement {
    method constructor (line 111) | constructor(info) {
  function createMenuItem (line 201) | function createMenuItem(title, callback) {
  function createSortSelect (line 219) | function createSortSelect(defaultOpt = 0) {
  function storeThisPage (line 236) | async function storeThisPage() {
  function getTrackMeta (line 289) | function getTrackMeta() {
  function storeTrack (line 309) | function storeTrack() {
  function storeTrackWithTime (line 313) | function storeTrackWithTime() {
  function idToProperName (line 321) | function idToProperName(id) {
  function createMenu (line 327) | function createMenu() {
  function onLinkClick (line 450) | async function onLinkClick(info) {
  function onPlayClick (line 459) | function onPlayClick(info) {

FILE: Extensions/fullAppDisplay.js
  class FAD (line 444) | class FAD extends react.Component {
    method constructor (line 445) | constructor(props) {
    method getAlbumDate (line 461) | async getAlbumDate(uri) {
    method fetchInfo (line 474) | async fetchInfo() {
    method animateCanvas (line 547) | animateCanvas(prevImg, nextImg) {
    method componentDidMount (line 594) | componentDidMount() {
    method componentWillUnmount (line 635) | componentWillUnmount() {
    method render (line 641) | render() {
  function toggleFullscreen (line 762) | async function toggleFullscreen() {
  function eventListener (line 772) | function eventListener() {
  function showCursor (line 777) | function showCursor() {
  function hideCursor (line 782) | function hideCursor() {
  function toggleCursor (line 786) | function toggleCursor(show = true) {
  function activate (line 803) | async function activate() {
  function deactivate (line 815) | function deactivate() {
  function toggleFad (line 831) | function toggleFad() {
  function updateStyle (line 839) | function updateStyle() {
  function checkLyricsPlus (line 846) | function checkLyricsPlus() {
  function requestLyricsPlus (line 850) | function requestLyricsPlus() {
  function getConfig (line 860) | function getConfig() {
  function saveConfig (line 873) | function saveConfig() {
  function openConfig (line 908) | function openConfig(event) {

FILE: Extensions/keyboardShortcut.js
  function focusOnApp (line 108) | function focusOnApp() {
  function createScrollCallback (line 114) | function createScrollCallback(step) {
  function scrollToPosition (line 126) | function scrollToPosition(position) {
  function findActiveIndex (line 135) | function findActiveIndex(allItems) {
  function rotateSidebar (line 158) | function rotateSidebar(direction) {
  function VimBind (line 172) | function VimBind() {

FILE: Extensions/loopyLoop.js
  function drawOnBar (line 45) | function drawOnBar() {
  function reset (line 54) | function reset() {
  function createMenuItem (line 80) | function createMenuItem(title, callback) {

FILE: Extensions/popupLyrics.js
  constant CACHE (line 29) | let CACHE = {};
  function PopupLyrics (line 31) | function PopupLyrics() {

FILE: Extensions/shuffle+.js
  function getConfig (line 17) | function getConfig() {
  function saveConfig (line 37) | function saveConfig() {
  function settingsPage (line 41) | function settingsPage() {
  function shouldAddShufflePlus (line 210) | function shouldAddShufflePlus(uri) {
  function shouldAddShufflePlusLiked (line 228) | function shouldAddShufflePlusLiked(uri) {
  function shouldAddShufflePlusLocal (line 239) | function shouldAddShufflePlusLocal(uri) {
  function renderQueuePlaybarButton (line 283) | function renderQueuePlaybarButton() {
  function fetchPlaylistTracks (line 300) | async function fetchPlaylistTracks(uri) {
  function searchFolder (line 307) | function searchFolder(rows, uri) {
  function fetchFolderTracks (line 318) | async function fetchFolderTracks(uri) {
  function fetchAlbumTracks (line 342) | async function fetchAlbumTracks(uri, includeMetadata = false) {
  function scanForTracksFromAlbums (line 360) | async function scanForTracksFromAlbums(res, artistName, type) {
  function fetchArtistTracks (line 384) | async function fetchArtistTracks(uri) {
  function fetchArtistLikedTracks (line 428) | async function fetchArtistLikedTracks(uri) {
  function fetchArtistTopTenTracks (line 438) | async function fetchArtistTopTenTracks(uri) {
  function fetchLikedTracks (line 449) | async function fetchLikedTracks() {
  function fetchLocalTracks (line 455) | async function fetchLocalTracks() {
  function fetchQueue (line 461) | function fetchQueue() {
  function fetchCollection (line 471) | async function fetchCollection(uriObj) {
  function fetchShows (line 495) | async function fetchShows(uri) {
  function shuffle (line 500) | function shuffle(array) {
  function Queue (line 520) | async function Queue(list, context, type) {
  function fetchAndPlay (line 588) | async function fetchAndPlay(rawUri) {

FILE: Extensions/trashbin.js
  function createButton (line 16) | function createButton(text, description, callback) {
  function createSlider (line 29) | function createSlider(name, desc, defaultVal, callback) {
  function settingsContent (line 55) | function settingsContent() {
  function styleSettings (line 88) | function styleSettings() {
  function initValue (line 151) | function initValue(item, defaultValue) {
  function refreshEventListeners (line 227) | function refreshEventListeners(state) {
  function setWidgetState (line 241) | function setWidgetState(state, hidden = false) {
  function watchChange (line 247) | function watchChange() {
  function shouldSkipCurrentTrack (line 284) | function shouldSkipCurrentTrack(uri, type) {
  function toggleThrow (line 311) | function toggleThrow(uris) {
  function shouldAddContextMenu (line 337) | function shouldAddContextMenu(uris) {
  function putDataLocal (line 360) | function putDataLocal() {
  function copyItems (line 365) | function copyItems() {
  function exportItems (line 374) | async function exportItems() {
  function importItems (line 403) | function importItems() {

FILE: Extensions/webnowplaying.js
  class WNPReduxWebSocket (line 19) | class WNPReduxWebSocket {
    method constructor (line 44) | constructor() {
    method updateSpicetifyInfo (line 51) | updateSpicetifyInfo(data) {
    method init (line 77) | init() {
    method close (line 89) | close(cleanupOnly = false) {
    method retry (line 102) | retry() {
    method send (line 115) | send(data) {
    method onOpen (line 120) | onOpen() {
    method onClose (line 129) | onClose() {
    method onError (line 133) | onError() {
    method onMessage (line 137) | onMessage(event) {
    method sendUpdate (line 166) | sendUpdate() {
  function OnMessageLegacy (line 179) | function OnMessageLegacy(self, message) {
  function SendUpdateLegacy (line 236) | function SendUpdateLegacy(self) {
  function OnMessageRev1 (line 269) | function OnMessageRev1(self, message) {
  function SendUpdateRev1 (line 326) | function SendUpdateRev1(self) {
  function pad (line 354) | function pad(num, size) {
  function timeInSecondsToString (line 357) | function timeInSecondsToString(timeInSeconds) {

FILE: globals.d.ts
  type Icon (line 2) | type Icon =
  type Variant (line 80) | type Variant =
  type SemanticColor (line 100) | type SemanticColor =
  type ColorSet (line 129) | type ColorSet =
  type ColorSetBackgroundColors (line 140) | type ColorSetBackgroundColors = {
  type ColorSetNamespaceColors (line 145) | type ColorSetNamespaceColors = {
  type ColorSetBody (line 154) | type ColorSetBody = {
  type Metadata (line 167) | type Metadata = Partial<Record<string, string>>;
  type ContextTrack (line 168) | type ContextTrack = {
  type PlayerState (line 173) | type PlayerState = {
  type PlayerContext (line 195) | type PlayerContext = {
  type PlayerIndex (line 202) | type PlayerIndex = {
  type PlayerTrack (line 207) | type PlayerTrack = {
  type TrackMetadata (line 225) | type TrackMetadata = {
  type Album (line 283) | type Album = {
  type ImagesEntity (line 289) | type ImagesEntity = {
  type ArtistsEntity (line 293) | type ArtistsEntity = {
  type Restrictions (line 298) | type Restrictions = {
  type PlaybackQuality (line 317) | type PlaybackQuality = {
  type Method (line 528) | type Method = "DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "SUB";
  type Error (line 529) | interface Error {
  type Headers (line 536) | type Headers = Record<string, string>;
  type Body (line 537) | type Body = Record<string, any>;
  type Response (line 539) | interface Response {
  type ValidKey (line 595) | type ValidKey =
  type KeysDefine (line 700) | type KeysDefine =
  class Item (line 748) | class Item {
  class SubMenu (line 779) | class SubMenu {
  class URI (line 844) | class URI {
  type OnClickCallback (line 1263) | type OnClickCallback = (uris: string[], uids?: string[], contextUri?: st...
  type ShouldAddCallback (line 1264) | type ShouldAddCallback = (uris: string[], uids?: string[], contextUri?: ...
  class Item (line 1267) | class Item {
  class SubMenu (line 1298) | class SubMenu {
  type Content (line 1323) | interface Content {
  type ContextMenuProps (line 1352) | type ContextMenuProps = {
  type MenuProps (line 1410) | type MenuProps = {
  type MenuItemProps (line 1421) | type MenuItemProps = {
  type TooltipProps (line 1451) | type TooltipProps = {
  type IconComponentProps (line 1499) | type IconComponentProps = {
  type TextComponentProps (line 1540) | type TextComponentProps = {
  type ConfirmDialogProps (line 1567) | type ConfirmDialogProps = {
  type SliderProps (line 1618) | type SliderProps = {
  type ButtonProps (line 1681) | type ButtonProps = {
  class Button (line 1849) | class Button {
  class Button (line 1868) | class Button {
  class Widget (line 1891) | class Widget {
  type Query (line 1976) | type Query =
  type hsl (line 2171) | interface hsl {
  type hsv (line 2176) | interface hsv {
  type rgb (line 2181) | interface rgb {
  type CSSColors (line 2186) | type CSSColors = "HEX" | "HEXA" | "HSL" | "HSLA" | "RGB" | "RGBA";
  class Color (line 2190) | class Color {

FILE: jsHelper/expFeatures.js
  function changeValue (line 180) | function changeValue(name, value) {
  function createSlider (line 188) | function createSlider(name, desc, defaultVal) {
  function createDropdown (line 212) | function createDropdown(name, desc, defaultVal, options) {

FILE: jsHelper/homeConfig.js
  function injectInteraction (line 91) | function injectInteraction() {
  function removeInteraction (line 168) | function removeInteraction() {

FILE: jsHelper/sidebarConfig.js
  function arrangeItems (line 20) | function arrangeItems(storage) {
  function appendItems (line 36) | function appendItems() {
  function writeStorage (line 56) | function writeStorage() {
  function injectInteraction (line 98) | function injectInteraction() {
  function removeInteraction (line 149) | function removeInteraction() {
  function initConfig (line 215) | function initConfig() {
  function InitSidebarXConfig (line 226) | function InitSidebarXConfig() {

FILE: jsHelper/spicetifyWrapper.js
  function checkObject (line 119) | function checkObject(object) {
  function applyScrollingFix (line 350) | function applyScrollingFix() {
  function injectStyles (line 655) | function injectStyles() {
  function useDragToScroll (line 691) | function useDragToScroll({ isDisabled = true } = {}) {
  function useWheelScroll (line 766) | function useWheelScroll(onlyHorizontalWheel) {
  function useScrollState (line 794) | function useScrollState(scrollerRef, contentRef) {
  function ScrollableContainerComponent (line 830) | function ScrollableContainerComponent(props) {
  method origin (line 991) | get origin() {
  method Request (line 997) | get Request() {
  method _relativeTimeFormat (line 1180) | get _relativeTimeFormat() {
  method _dateTimeFormats (line 1183) | get _dateTimeFormats() {
  method _locale (line 1186) | get _locale() {
  method _urlLocale (line 1189) | get _urlLocale() {
  method _dictionary (line 1192) | get _dictionary() {
  method get (line 1232) | get() {
  function wrapProvider (line 1329) | function wrapProvider(component) {
  class Event (line 1425) | class Event {
    method on (line 1427) | on(callback) {
    method fire (line 1431) | fire() {
  function formatKeys (line 1748) | function formatKeys(keys) {
  function parseIcon (line 1965) | function parseIcon(icon, size = 16) {
  function createIconComponent (line 1972) | function createIconComponent(icon, size = 16) {
  function parseProps (line 1988) | function parseProps(props) {
  class Item (line 2004) | class Item {
    method constructor (line 2005) | constructor({ children, disabled = false, leadingIcon, trailingIcon, d...
    method children (line 2053) | set children(children) {
    method children (line 2057) | get children() {
    method disabled (line 2061) | set disabled(bool) {
    method disabled (line 2065) | get disabled() {
    method leadingIcon (line 2069) | set leadingIcon(name) {
    method leadingIcon (line 2073) | get leadingIcon() {
    method trailingIcon (line 2077) | set trailingIcon(name) {
    method trailingIcon (line 2081) | get trailingIcon() {
    method divider (line 2085) | set divider(divider) {
    method divider (line 2089) | get divider() {
    method register (line 2093) | register() {
    method deregister (line 2096) | deregister() {
    method constructor (line 2243) | constructor(children, isEnabled, onClick, leadingIcon) {
    method setState (line 2249) | setState(state) {
    method isEnabled (line 2253) | set isEnabled(bool) {
    method isEnabled (line 2257) | get isEnabled() {
    method constructor (line 2291) | constructor(name, onClick, shouldAdd = () => true, icon = undefined, t...
    method name (line 2308) | set name(name) {
    method name (line 2311) | get name() {
    method icon (line 2315) | set icon(name) {
    method icon (line 2318) | get icon() {
  class ItemSubMenu (line 2101) | class ItemSubMenu {
    method itemsToComponents (line 2102) | static itemsToComponents(items, props, trigger, target, parentDepth = ...
    method constructor (line 2111) | constructor({ text, disabled = false, leadingIcon, divider, items, dep...
    method text (line 2162) | set text(text) {
    method text (line 2166) | get text() {
    method disabled (line 2170) | set disabled(bool) {
    method disabled (line 2174) | get disabled() {
    method leadingIcon (line 2178) | set leadingIcon(name) {
    method leadingIcon (line 2182) | get leadingIcon() {
    method divider (line 2186) | set divider(divider) {
    method divider (line 2190) | get divider() {
    method depth (line 2194) | set depth(value) {
    method depth (line 2198) | get depth() {
    method addItem (line 2202) | addItem(item) {
    method removeItem (line 2206) | removeItem(item) {
    method register (line 2211) | register() {
    method deregister (line 2214) | deregister() {
  function registerItem (line 2219) | function registerItem(item, shouldAdd = () => true) {
  function unregisterItem (line 2223) | function unregisterItem(item) {
  class Item (line 2242) | class Item extends Spicetify.ContextMenuV2.Item {
    method constructor (line 2005) | constructor({ children, disabled = false, leadingIcon, trailingIcon, d...
    method children (line 2053) | set children(children) {
    method children (line 2057) | get children() {
    method disabled (line 2061) | set disabled(bool) {
    method disabled (line 2065) | get disabled() {
    method leadingIcon (line 2069) | set leadingIcon(name) {
    method leadingIcon (line 2073) | get leadingIcon() {
    method trailingIcon (line 2077) | set trailingIcon(name) {
    method trailingIcon (line 2081) | get trailingIcon() {
    method divider (line 2085) | set divider(divider) {
    method divider (line 2089) | get divider() {
    method register (line 2093) | register() {
    method deregister (line 2096) | deregister() {
    method constructor (line 2243) | constructor(children, isEnabled, onClick, leadingIcon) {
    method setState (line 2249) | setState(state) {
    method isEnabled (line 2253) | set isEnabled(bool) {
    method isEnabled (line 2257) | get isEnabled() {
    method constructor (line 2291) | constructor(name, onClick, shouldAdd = () => true, icon = undefined, t...
    method name (line 2308) | set name(name) {
    method name (line 2311) | get name() {
    method icon (line 2315) | set icon(name) {
    method icon (line 2318) | get icon() {
  class SubMenu (line 2262) | class SubMenu extends Spicetify.ContextMenuV2.ItemSubMenu {
    method constructor (line 2263) | constructor(text, items, leadingIcon) {
    method name (line 2267) | set name(text) {
    method name (line 2270) | get name() {
    method icon (line 2274) | set icon(icon) {
    method icon (line 2277) | get icon() {
    method constructor (line 2326) | constructor(name, items, shouldAdd, disabled = false, icon = undefined) {
    method name (line 2339) | set name(name) {
    method name (line 2342) | get name() {
  class Item (line 2288) | class Item extends Spicetify.ContextMenuV2.Item {
    method constructor (line 2005) | constructor({ children, disabled = false, leadingIcon, trailingIcon, d...
    method children (line 2053) | set children(children) {
    method children (line 2057) | get children() {
    method disabled (line 2061) | set disabled(bool) {
    method disabled (line 2065) | get disabled() {
    method leadingIcon (line 2069) | set leadingIcon(name) {
    method leadingIcon (line 2073) | get leadingIcon() {
    method trailingIcon (line 2077) | set trailingIcon(name) {
    method trailingIcon (line 2081) | get trailingIcon() {
    method divider (line 2085) | set divider(divider) {
    method divider (line 2089) | get divider() {
    method register (line 2093) | register() {
    method deregister (line 2096) | deregister() {
    method constructor (line 2243) | constructor(children, isEnabled, onClick, leadingIcon) {
    method setState (line 2249) | setState(state) {
    method isEnabled (line 2253) | set isEnabled(bool) {
    method isEnabled (line 2257) | get isEnabled() {
    method constructor (line 2291) | constructor(name, onClick, shouldAdd = () => true, icon = undefined, t...
    method name (line 2308) | set name(name) {
    method name (line 2311) | get name() {
    method icon (line 2315) | set icon(name) {
    method icon (line 2318) | get icon() {
  class SubMenu (line 2323) | class SubMenu extends Spicetify.ContextMenuV2.ItemSubMenu {
    method constructor (line 2263) | constructor(text, items, leadingIcon) {
    method name (line 2267) | set name(text) {
    method name (line 2270) | get name() {
    method icon (line 2274) | set icon(icon) {
    method icon (line 2277) | get icon() {
    method constructor (line 2326) | constructor(name, items, shouldAdd, disabled = false, icon = undefined) {
    method name (line 2339) | set name(name) {
    method name (line 2342) | get name() {
  class _HTMLGenericModal (line 2494) | class _HTMLGenericModal extends HTMLElement {
    method hide (line 2495) | hide() {
    method display (line 2500) | display({ title, content, isLarge = false }) {
  method render (line 2543) | render(instance) {
  method onShow (line 2562) | onShow(instance) {
  method onMount (line 2565) | onMount(instance) {
  method onHide (line 2571) | onHide(instance) {
  class Button (line 2589) | class Button {
    method constructor (line 2590) | constructor(label, icon, onClick, disabled = false, isRight = false) {
    method label (line 2613) | get label() {
    method label (line 2616) | set label(text) {
    method icon (line 2622) | get icon() {
    method icon (line 2625) | set icon(input) {
    method onClick (line 2633) | get onClick() {
    method onClick (line 2636) | set onClick(func) {
    method disabled (line 2640) | get disabled() {
    method disabled (line 2643) | set disabled(bool) {
    method constructor (line 2701) | constructor(label, icon, onClick = () => {}, disabled = false, active ...
    method label (line 2719) | get label() {
    method label (line 2722) | set label(text) {
    method icon (line 2727) | get icon() {
    method icon (line 2730) | set icon(input) {
    method onClick (line 2738) | get onClick() {
    method onClick (line 2741) | set onClick(func) {
    method disabled (line 2745) | get disabled() {
    method disabled (line 2748) | set disabled(bool) {
    method active (line 2753) | set active(bool) {
    method active (line 2758) | get active() {
    method register (line 2761) | register() {
    method deregister (line 2765) | deregister() {
  function waitForTopbarMounted (line 2650) | function waitForTopbarMounted() {
  class Button (line 2700) | class Button {
    method constructor (line 2590) | constructor(label, icon, onClick, disabled = false, isRight = false) {
    method label (line 2613) | get label() {
    method label (line 2616) | set label(text) {
    method icon (line 2622) | get icon() {
    method icon (line 2625) | set icon(input) {
    method onClick (line 2633) | get onClick() {
    method onClick (line 2636) | set onClick(func) {
    method disabled (line 2640) | get disabled() {
    method disabled (line 2643) | set disabled(bool) {
    method constructor (line 2701) | constructor(label, icon, onClick = () => {}, disabled = false, active ...
    method label (line 2719) | get label() {
    method label (line 2722) | set label(text) {
    method icon (line 2727) | get icon() {
    method icon (line 2730) | set icon(input) {
    method onClick (line 2738) | get onClick() {
    method onClick (line 2741) | set onClick(func) {
    method disabled (line 2745) | get disabled() {
    method disabled (line 2748) | set disabled(bool) {
    method active (line 2753) | set active(bool) {
    method active (line 2758) | get active() {
    method register (line 2761) | register() {
    method deregister (line 2765) | deregister() {
  function addClassname (line 2783) | function addClassname(element) {
  class Widget (line 2797) | class Widget {
    method constructor (line 2798) | constructor(label, icon, onClick = () => {}, disabled = false, active ...
    method label (line 2812) | get label() {
    method label (line 2815) | set label(text) {
    method icon (line 2820) | get icon() {
    method icon (line 2823) | set icon(input) {
    method onClick (line 2831) | get onClick() {
    method onClick (line 2834) | set onClick(func) {
    method disabled (line 2838) | get disabled() {
    method disabled (line 2841) | set disabled(bool) {
    method active (line 2847) | set active(bool) {
    method active (line 2852) | get active() {
    method register (line 2855) | register() {
    method deregister (line 2859) | deregister() {
  function waitForWidgetMounted (line 2865) | function waitForWidgetMounted() {

FILE: spicetify.go
  function init (line 39) | func init() {
  function main (line 144) | func main() {
  function help (line 367) | func help() {
  function helpConfig (line 507) | func helpConfig() {

FILE: src/apply/apply.go
  type Flag (line 14) | type Flag struct
  function AdditionalOptions (line 29) | func AdditionalOptions(appsFolderPath string, flags Flag) {
  function UserCSS (line 103) | func UserCSS(appsFolderPath, themeFolder string, scheme map[string]strin...
  function UserAsset (line 115) | func UserAsset(appsFolderPath, themeFolder string) {
  function htmlMod (line 123) | func htmlMod(htmlPath string, flags Flag) {
  function getUserCSS (line 217) | func getUserCSS(themeFolder string) string {
  function getColorCSS (line 237) | func getColorCSS(scheme map[string]string) string {
  function insertCustomApp (line 261) | func insertCustomApp(jsPath string, flags Flag) {
  function insertNavLink (line 371) | func insertNavLink(str string, appNameArray string) string {
  function insertHomeConfig (line 409) | func insertHomeConfig(jsPath string, flags Flag) {
  function getAssetsPath (line 434) | func getAssetsPath(themeFolder string) string {
  function insertSidebarConfig (line 444) | func insertSidebarConfig(jsPath string, flags Flag) {
  function insertExpFeatures (line 461) | func insertExpFeatures(jsPath string, flags Flag) {
  function insertVersionInfo (line 492) | func insertVersionInfo(jsPath string, flags Flag) {

FILE: src/backup/backup.go
  function Start (line 11) | func Start(appPath, backupPath string) error {
  function Extract (line 16) | func Extract(backupPath, extractPath string) {

FILE: src/cmd/apply.go
  function Apply (line 17) | func Apply(spicetifyVersion string) {
  function RefreshTheme (line 102) | func RefreshTheme() {
  type spicetifyConfigJson (line 116) | type spicetifyConfigJson struct
  function refreshThemeCSS (line 122) | func refreshThemeCSS() {
  function refreshThemeAssets (line 167) | func refreshThemeAssets() {
  function RefreshExtensions (line 174) | func RefreshExtensions(list ...string) {
  function CheckStates (line 190) | func CheckStates() {
  function refreshThemeJS (line 220) | func refreshThemeJS() {
  function pushExtensions (line 232) | func pushExtensions(destExt string, list ...string) {
  function RefreshApps (line 282) | func RefreshApps(list ...string) {
  function nodeModuleSymlink (line 396) | func nodeModuleSymlink() {

FILE: src/cmd/auto.go
  function Auto (line 12) | func Auto(spicetifyVersion string) {

FILE: src/cmd/backup.go
  function Backup (line 17) | func Backup(spicetifyVersion string, silent bool) {
  function Clear (line 104) | func Clear() {
  function clearBackup (line 115) | func clearBackup() {
  function Restore (line 156) | func Restore() {

FILE: src/cmd/block-updates.go
  function BlockSpotifyUpdates (line 15) | func BlockSpotifyUpdates(disabled bool) {

FILE: src/cmd/cmd.go
  function InitConfig (line 42) | func InitConfig(isQuiet bool) {
  function InitPaths (line 56) | func InitPaths() {
  function InitSetting (line 123) | func InitSetting() {
  function GetConfigPath (line 205) | func GetConfigPath() string {
  function GetSpotifyPath (line 210) | func GetSpotifyPath() string {
  function getExtractFolder (line 214) | func getExtractFolder() (string, string) {
  function getThemeFolder (line 226) | func getThemeFolder(themeName string) string {
  function ReadAnswer (line 249) | func ReadAnswer(info string, defaultAnswer bool, quietModeAnswer bool) b...
  function CheckUpdate (line 277) | func CheckUpdate(version string) {

FILE: src/cmd/color.go
  function EditColor (line 12) | func EditColor(args []string) {
  function initCmdColor (line 42) | func initCmdColor() bool {
  function DisplayColors (line 85) | func DisplayColors() {
  function colorChangeSuccess (line 120) | func colorChangeSuccess(field, value string) {
  function colorPreview (line 125) | func colorPreview(color utils.Color) string {

FILE: src/cmd/config-dir.go
  function ShowConfigDirectory (line 8) | func ShowConfigDirectory() {

FILE: src/cmd/config.go
  function EditConfig (line 14) | func EditConfig(args []string) {
  function DisplayAllConfig (line 40) | func DisplayAllConfig() {
  function DisplayConfig (line 81) | func DisplayConfig(field string) {
  function searchField (line 97) | func searchField(field string) *ini.Key {
  function changeSuccess (line 112) | func changeSuccess(key, value string) {
  function unchangeWarning (line 117) | func unchangeWarning(field, reason string) {
  function arrayType (line 121) | func arrayType(section *ini.Section, field, value string) {
  function pluralize (line 186) | func pluralize(count int, singular, plural string) string {
  function stringType (line 193) | func stringType(section *ini.Section, field, value string) {
  function toggleType (line 206) | func toggleType(field, value string) {

FILE: src/cmd/devtools.go
  function EnableDevTools (line 15) | func EnableDevTools() {

FILE: src/cmd/patch.go
  function Patch (line 12) | func Patch() {

FILE: src/cmd/path.go
  function ThemeAssetPath (line 12) | func ThemeAssetPath(kind string) (string, error) {
  function ThemeAllAssetsPath (line 41) | func ThemeAllAssetsPath() (string, error) {
  function ExtensionPath (line 59) | func ExtensionPath(name string) (string, error) {
  function ExtensionAllPath (line 67) | func ExtensionAllPath() (string, error) {
  function AppPath (line 82) | func AppPath(name string) (string, error) {
  function AppAllPath (line 90) | func AppAllPath() (string, error) {
  function AllPaths (line 104) | func AllPaths() (string, error) {

FILE: src/cmd/restart.go
  function SpotifyKill (line 12) | func SpotifyKill() {
  function SpotifyStart (line 35) | func SpotifyStart(flags ...string) {
  function SpotifyRestart (line 67) | func SpotifyRestart(flags ...string) {

FILE: src/cmd/update.go
  function Update (line 15) | func Update(currentVersion string) bool {
  function permissionError (line 109) | func permissionError(err error) {

FILE: src/cmd/watch.go
  function Watch (line 22) | func Watch(liveUpdate bool) {
  function WatchExtensions (line 105) | func WatchExtensions(extName []string, liveUpdate bool) {
  function WatchCustomApp (line 150) | func WatchCustomApp(appName []string, liveUpdate bool) {
  function isValidForWatching (line 224) | func isValidForWatching() bool {
  function startDebugger (line 235) | func startDebugger() {
  function enqueueWatchJob (line 255) | func enqueueWatchJob(job func()) {

FILE: src/preprocess/preprocess.go
  type Flag (line 22) | type Flag struct
  type Patch (line 34) | type Patch struct
  function applyPatches (line 41) | func applyPatches(input string, patches []Patch) string {
  function readRemoteCssMap (line 52) | func readRemoteCssMap(tag string, cssTranslationMap *map[string]string) ...
  function readLocalCssMap (line 67) | func readLocalCssMap(cssTranslationMap *map[string]string) error {
  function Start (line 83) | func Start(version string, spotifyBasePath string, extractedAppsPath str...
  function StartCSS (line 314) | func StartCSS(extractedAppsPath string) {
  function colorVariableReplace (line 331) | func colorVariableReplace(content string) string {
  function colorVariableReplaceForJS (line 492) | func colorVariableReplaceForJS(content string) string {
  function disableSentry (line 527) | func disableSentry(input string) string {
  function disableLogging (line 538) | func disableLogging(input string) string {
  function removeRTL (line 747) | func removeRTL(input string) string {
  function additionalPatches (line 838) | func additionalPatches(input string) string {
  function exposeAPIs_main (line 859) | func exposeAPIs_main(input string) string {
  function exposeAPIs_vendor (line 984) | func exposeAPIs_vendor(input string) string {
  function validateReleaseBuild (line 1058) | func validateReleaseBuild(spotifyBinaryPath string) error {
  function splitVersion (line 1083) | func splitVersion(version string) ([3]int, error) {
  function FetchLatestTagMatchingOrMain (line 1103) | func FetchLatestTagMatchingOrMain(version string) (string, error) {
  function FetchLatestTagMatchingVersion (line 1124) | func FetchLatestTagMatchingVersion(version string) (string, error) {

FILE: src/status/backup/backup.go
  type status (line 11) | type status struct
    method IsBackuped (line 63) | func (s status) IsBackuped() bool {
    method IsEmpty (line 67) | func (s status) IsEmpty() bool {
    method IsOutdated (line 71) | func (s status) IsOutdated() bool {
  type Status (line 16) | type Status interface
  constant EMPTY (line 24) | EMPTY int = iota
  constant BACKUPED (line 26) | BACKUPED
  constant OUTDATED (line 28) | OUTDATED
  function Get (line 32) | func Get(prefsPath, backupPath, backupVersion string) Status {

FILE: src/status/spotify/spotify.go
  type status (line 9) | type status struct
    method IsBackupable (line 64) | func (s status) IsBackupable() bool {
    method IsModdable (line 68) | func (s status) IsModdable() bool {
    method IsStock (line 72) | func (s status) IsStock() bool {
    method IsMixed (line 76) | func (s status) IsMixed() bool {
    method IsApplied (line 80) | func (s status) IsApplied() bool {
    method IsInvalid (line 84) | func (s status) IsInvalid() bool {
  type Status (line 14) | type Status interface
  constant STOCK (line 25) | STOCK int = iota
  constant INVALID (line 27) | INVALID
  constant APPLIED (line 29) | APPLIED
  constant MIXED (line 31) | MIXED
  function Get (line 35) | func Get(appsFolder string) Status {

FILE: src/utils/color.go
  type color (line 62) | type color struct
    method Hex (line 126) | func (c color) Hex() string {
    method RGB (line 130) | func (c color) RGB() string {
    method TerminalRGB (line 134) | func (c color) TerminalRGB() string {
  type Color (line 67) | type Color interface
  function ParseColor (line 76) | func ParseColor(raw string) Color {
  function stringToInt (line 138) | func stringToInt(raw string, base int) int64 {
  function getXRDB (line 155) | func getXRDB() error {
  function fromXResources (line 183) | func fromXResources(input string) string {

FILE: src/utils/config.go
  type config (line 46) | type config struct
    method Write (line 108) | func (c config) Write() error {
    method GetSection (line 112) | func (c config) GetSection(name string) *ini.Section {
    method GetPath (line 125) | func (c config) GetPath() string {
  type Config (line 52) | type Config interface
  function ParseConfig (line 60) | func ParseConfig(configPath string) Config {
  function getDefaultConfig (line 129) | func getDefaultConfig() *ini.File {
  function FindAppPath (line 170) | func FindAppPath() string {
  function FindPrefFilePath (line 192) | func FindPrefFilePath() string {
  function winApp (line 214) | func winApp() string {
  function winPrefs (line 223) | func winPrefs() string {
  function WinXApp (line 232) | func WinXApp() string {
  function WinXPrefs (line 247) | func WinXPrefs() string {
  function linuxApp (line 271) | func linuxApp() string {
  function linuxPrefs (line 327) | func linuxPrefs() string {
  function darwinApp (line 342) | func darwinApp() string {
  function darwinPrefs (line 351) | func darwinPrefs() string {

FILE: src/utils/file-utils.go
  function ReadStringFromUTF16Binary (line 11) | func ReadStringFromUTF16Binary(inputFile string, startMarker []byte, end...
  function encodeUTF16LE (line 63) | func encodeUTF16LE(data []byte) []byte {
  function decodeUTF16LE (line 74) | func decodeUTF16LE(data []byte) ([]byte, error) {

FILE: src/utils/isAdmin/unix.go
  function Check (line 8) | func Check(bypassAdminCheck bool) bool {

FILE: src/utils/isAdmin/windows.go
  function Check (line 10) | func Check(bypassAdminCheck bool) bool {

FILE: src/utils/path-utils.go
  function MigrateConfigFolder (line 11) | func MigrateConfigFolder() {
  function MigrateFolders (line 28) | func MigrateFolders() {
  function ReplaceEnvVarsInString (line 78) | func ReplaceEnvVarsInString(input string) string {
  function GetSpicetifyFolder (line 88) | func GetSpicetifyFolder() string {
  function GetStateFolder (line 119) | func GetStateFolder(name string) string {
  function GetSubFolder (line 152) | func GetSubFolder(folder string, name string) string {
  function GetCustomAppSubfolderPath (line 162) | func GetCustomAppSubfolderPath(folderPath string) string {
  function GetCustomAppPath (line 186) | func GetCustomAppPath(name string) (string, error) {
  function GetExtensionPath (line 210) | func GetExtensionPath(name string) (string, error) {

FILE: src/utils/print.go
  function Bold (line 63) | func Bold(text string) string {
  function Red (line 68) | func Red(text string) string {
  function PrintBold (line 73) | func PrintBold(text string) {
  function PrintNote (line 78) | func PrintNote(text string) {
  function PrintWarning (line 83) | func PrintWarning(text string) {
  function PrintError (line 88) | func PrintError(text string) {
  function PrintSuccess (line 93) | func PrintSuccess(text string) {
  function PrintInfo (line 98) | func PrintInfo(text string) {
  function Fatal (line 103) | func Fatal(err error) {

FILE: src/utils/scanner.go
  function CmdScanner (line 10) | func CmdScanner(cmd *exec.Cmd) {

FILE: src/utils/show-dir.go
  function ShowDirectory (line 9) | func ShowDirectory(dir string) error {

FILE: src/utils/utils.go
  function CheckExistAndCreate (line 22) | func CheckExistAndCreate(dir string) {
  function CheckExistAndDelete (line 31) | func CheckExistAndDelete(dir string) {
  function Unzip (line 39) | func Unzip(src, dest string) error {
  function Copy (line 84) | func Copy(src, dest string, recursive bool, filters []string) error {
  function CopyFile (line 141) | func CopyFile(srcPath, dest string) error {
  function Replace (line 167) | func Replace(str *string, pattern string, repl func(submatches ...string...
  function ReplaceOnce (line 175) | func ReplaceOnce(str *string, pattern string, repl func(submatches ...st...
  function ReplaceOnceWithPriority (line 190) | func ReplaceOnceWithPriority(str *string, patterns []string, repl func(i...
  function FindMatch (line 210) | func FindMatch(input string, regexpTerm string) [][]string {
  function FindFirstMatch (line 216) | func FindFirstMatch(input string, regexpTerm string) []string {
  function FindLastMatch (line 224) | func FindLastMatch(input string, regexpTerm string) []string {
  function ModifyFile (line 234) | func ModifyFile(path string, repl func(string) string) {
  function CreateFile (line 247) | func CreateFile(path string, content string) error {
  function GetSpotifyVersion (line 256) | func GetSpotifyVersion(prefsPath string) string {
  function GetExecutableDir (line 272) | func GetExecutableDir() string {
  function GetJsHelperDir (line 288) | func GetJsHelperDir() string {
  function PrependTime (line 293) | func PrependTime(text string) string {
  function FindSymbol (line 300) | func FindSymbol(debugInfo, content string, clues []string) []string {
  function FindSymbolWithPattern (line 318) | func FindSymbolWithPattern(debugInfo, content string, clues []string) ([...
  function CreateJunction (line 335) | func CreateJunction(location, destination string) error {
  function SeekToCloseParen (line 348) | func SeekToCloseParen(content string, regexpTerm string, leftChar, right...
  type AppManifest (line 374) | type AppManifest struct
  function GetAppManifest (line 380) | func GetAppManifest(app string) (AppManifest, string, error) {

FILE: src/utils/vcs.go
  type GithubRelease (line 10) | type GithubRelease struct
  function FetchLatestTag (line 15) | func FetchLatestTag() (string, error) {

FILE: src/utils/watcher.go
  function Watch (line 22) | func Watch(fileList []string, callbackEach func(fileName string, err err...
  function WatchRecursive (line 50) | func WatchRecursive(root string, callbackEach func(fileName string, err ...
  type debugger (line 84) | type debugger struct
  function GetDebuggerPath (line 96) | func GetDebuggerPath() string {
  function SendReload (line 122) | func SendReload(debuggerURL *string) error {
Condensed preview — 100 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,070K chars).
[
  {
    "path": ".coderabbit.yaml",
    "chars": 52,
    "preview": "issue_enrichment:\n  auto_enrich:\n    enabled: false\n"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 205,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n  -"
  },
  {
    "path": ".github/labeler.yml",
    "chars": 385,
    "preview": "📦 aur:\n  - \"(aur)\"\n📦 snap:\n  - \"(snap)\"\n📦 brew:\n  - \"(brew)\"\n🪟 windows:\n  - \"(windows)\"\n🐧 linux:\n  - \"(linux)\"\n🍎 macos:\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 5298,
    "preview": "name: Build\n\non:\n  pull_request:\n    branches:\n      - \"main\"\n      - \"*/main/*/**\"\n  push:\n    branches:\n      - \"main\""
  },
  {
    "path": ".github/workflows/labeler.yml",
    "chars": 310,
    "preview": "name: Issue Labeler\n\non:\n  issues:\n    types: [opened, edited]\n\njobs:\n  triage:\n    runs-on: ubuntu-latest\n    steps:\n  "
  },
  {
    "path": ".github/workflows/linter.yml",
    "chars": 477,
    "preview": "name: Code quality\n\non:\n  pull_request:\n    branches:\n      - \"main\"\n      - \"*/main/*/**\"\n  push:\n    branches:\n      -"
  },
  {
    "path": ".github/workflows/lintpr.yml",
    "chars": 405,
    "preview": "name: Lint Pull Request\n\non:\n  pull_request_target:\n    types: [opened, edited, synchronize]\n\njobs:\n  lintpr:\n    runs-o"
  },
  {
    "path": ".gitignore",
    "chars": 162,
    "preview": "# Executables\nbin\ncli\nspicetify\nspicetify-cli\n*.exe\n\n# MacOS\n.DS_Store\n\n# Node.js\nnode_modules\npackage-lock.json\npackage"
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 103,
    "preview": "{\n\t\"recommendations\": [\"timonwong.shellcheck\", \"biomejs.biome\", \"golang.go\", \"ms-vscode.powershell\"]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 250,
    "preview": "{\n\t\"editor.formatOnSave\": true,\n\t\"[go]\": {\n\t\t\"editor.defaultFormatter\": \"golang.go\"\n\t},\n\t\"[powershell]\": {\n\t\t\"editor.def"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 7616,
    "preview": "# Contributing to Spicetify-cli\n\n## Table of Contents\n\n- [I Have a Question](#i-have-a-question)\n- [How to Contribute](#"
  },
  {
    "path": "CustomApps/lyrics-plus/OptionsMenu.js",
    "chars": 13089,
    "preview": "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: \""
  },
  {
    "path": "CustomApps/lyrics-plus/Pages.js",
    "chars": 25431,
    "preview": "const CreditFooter = react.memo(({ provider, copyright }) => {\n\tif (provider === \"local\") return null;\n\tconst credit = ["
  },
  {
    "path": "CustomApps/lyrics-plus/PlaybarButton.js",
    "chars": 1758,
    "preview": "(function PlaybarButton() {\n\tif (!Spicetify.Platform.History) {\n\t\tsetTimeout(PlaybarButton, 300);\n\t\treturn;\n\t}\n\n\tconst b"
  },
  {
    "path": "CustomApps/lyrics-plus/ProviderGenius.js",
    "chars": 3111,
    "preview": "const ProviderGenius = (() => {\n\tfunction getChildDeep(parent, isDeep = false) {\n\t\tlet acc = \"\";\n\n\t\tif (!parent.children"
  },
  {
    "path": "CustomApps/lyrics-plus/ProviderLRCLIB.js",
    "chars": 1329,
    "preview": "const ProviderLRCLIB = (() => {\n\tasync function findLyrics(info) {\n\t\tconst baseURL = \"https://lrclib.net/api/get\";\n\t\tcon"
  },
  {
    "path": "CustomApps/lyrics-plus/ProviderMusixmatch.js",
    "chars": 12377,
    "preview": "const ProviderMusixmatch = (() => {\n\tconst headers = {\n\t\tauthority: \"apic-desktop.musixmatch.com\",\n\t\tcookie: \"x-mxm-toke"
  },
  {
    "path": "CustomApps/lyrics-plus/ProviderNetease.js",
    "chars": 5797,
    "preview": "const ProviderNetease = (() => {\n\tconst requestHeader = {\n\t\t\"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:"
  },
  {
    "path": "CustomApps/lyrics-plus/Providers.js",
    "chars": 6502,
    "preview": "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"
  },
  {
    "path": "CustomApps/lyrics-plus/README.md",
    "chars": 2442,
    "preview": "# Spicetify Custom App\n\n### Lyrics Plus\n\nShow current track lyrics. Current lyrics providers:\n\n- Internal Spotify lyrics"
  },
  {
    "path": "CustomApps/lyrics-plus/Settings.js",
    "chars": 16460,
    "preview": "const ButtonSVG = ({ icon, active = true, onClick }) => {\n\treturn react.createElement(\n\t\t\"button\",\n\t\t{\n\t\t\tclassName: `sw"
  },
  {
    "path": "CustomApps/lyrics-plus/TabBar.js",
    "chars": 5106,
    "preview": "class TabBarItem extends react.Component {\n\tonSelect(event) {\n\t\tevent.preventDefault();\n\t\tthis.props.switchTo(this.props"
  },
  {
    "path": "CustomApps/lyrics-plus/Translator.js",
    "chars": 4180,
    "preview": "const kuroshiroPath = \"https://cdn.jsdelivr.net/npm/kuroshiro@1.2.0/dist/kuroshiro.min.js\";\nconst kuromojiPath = \"https:"
  },
  {
    "path": "CustomApps/lyrics-plus/Utils.js",
    "chars": 15495,
    "preview": "const Utils = {\n\taddQueueListener(callback) {\n\t\tSpicetify.Player.origin._events.addListener(\"queue_update\", callback);\n\t"
  },
  {
    "path": "CustomApps/lyrics-plus/index.js",
    "chars": 43344,
    "preview": "// Run \"npm i @types/react\" to have this type package available in workspace\n/// <reference types=\"react\" />\n/// <refere"
  },
  {
    "path": "CustomApps/lyrics-plus/manifest.json",
    "chars": 3555,
    "preview": "{\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\": \"Текст"
  },
  {
    "path": "CustomApps/lyrics-plus/style.css",
    "chars": 17138,
    "preview": "/*!\n * Bootstrap v3.3.7 (http://getbootstrap.com)\n * Copyright 2011-2016 Twitter,\n Inc.\n * Licensed under MIT (https://g"
  },
  {
    "path": "CustomApps/new-releases/Card.js",
    "chars": 6441,
    "preview": "function DraggableComponent({ uri, title, children }) {\n\tconst dragHandler = Spicetify.ReactHook.DragHandler?.([uri], ti"
  },
  {
    "path": "CustomApps/new-releases/Icons.js",
    "chars": 1731,
    "preview": "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\tpre"
  },
  {
    "path": "CustomApps/new-releases/Settings.js",
    "chars": 5818,
    "preview": "const ButtonSVG = ({ icon, active = true, onClick }) => {\n\treturn react.createElement(\n\t\t\"button\",\n\t\t{\n\t\t\tclassName: `sw"
  },
  {
    "path": "CustomApps/new-releases/index.js",
    "chars": 11935,
    "preview": "// Run \"npm i @types/react\" to have this type package available in workspace\n/// <reference types=\"react\" />\n\n/** @type "
  },
  {
    "path": "CustomApps/new-releases/manifest.json",
    "chars": 4338,
    "preview": "{\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 bur"
  },
  {
    "path": "CustomApps/new-releases/style.css",
    "chars": 3977,
    "preview": ".setting-row::after {\n\tcontent: \"\";\n\tdisplay: table;\n\tclear: both;\n}\n.setting-row .col {\n\tdisplay: flex;\n\tpadding: 10px "
  },
  {
    "path": "CustomApps/reddit/Card.js",
    "chars": 5180,
    "preview": "class Card extends react.Component {\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tObject.assign(this, props);\n\t\tconst uriObj "
  },
  {
    "path": "CustomApps/reddit/Icons.js",
    "chars": 1921,
    "preview": "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\","
  },
  {
    "path": "CustomApps/reddit/OptionsMenu.js",
    "chars": 1947,
    "preview": "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: \""
  },
  {
    "path": "CustomApps/reddit/Settings.js",
    "chars": 4886,
    "preview": "let configContainer;\n\nfunction openConfig() {\n\tif (configContainer) {\n\t\tSpicetify.PopupModal.display({\n\t\t\ttitle: \"Reddit"
  },
  {
    "path": "CustomApps/reddit/SortBox.js",
    "chars": 1319,
    "preview": "class SortBox extends react.Component {\n\tconstructor(props) {\n\t\tsuper(props);\n\t\tthis.sortByOptions = [\n\t\t\t{ key: \"hot\", "
  },
  {
    "path": "CustomApps/reddit/TabBar.js",
    "chars": 4285,
    "preview": "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: \"reddi"
  },
  {
    "path": "CustomApps/reddit/index.js",
    "chars": 10022,
    "preview": "// Run \"npm i @types/react-dom @types/react\" to have this type package available in workspace\n/// <reference types=\"reac"
  },
  {
    "path": "CustomApps/reddit/manifest.json",
    "chars": 5391,
    "preview": "{\n\t\"name\": \"Reddit\",\n\t\"icon\": \"<svg viewBox=\\\"0 0 256 256\\\" fill=\\\"currentColor\\\" stroke=\\\"currentColor\\\" stroke-width=\\"
  },
  {
    "path": "CustomApps/reddit/style.css",
    "chars": 4056,
    "preview": ".setting-row::after {\n\tcontent: \"\";\n\tdisplay: table;\n\tclear: both;\n}\n.setting-row .col {\n\tdisplay: flex;\n\tpadding: 10px "
  },
  {
    "path": "Extensions/autoSkipExplicit.js",
    "chars": 912,
    "preview": "// NAME: Christian Spotify\n// AUTHOR: khanhas\n// DESCRIPTION: Auto skip explicit songs. Toggle in Profile menu.\n\n/// <re"
  },
  {
    "path": "Extensions/autoSkipVideo.js",
    "chars": 499,
    "preview": "// NAME: Auto Skip Video\n// AUTHOR: khanhas\n// DESCRIPTION: Auto skip video\n\n/// <reference path=\"../globals.d.ts\" />\n\n("
  },
  {
    "path": "Extensions/bookmark.js",
    "chars": 17847,
    "preview": "// NAME: Bookmark\n// AUTHOR: khanhas\n// VERSION: 2.0\n// DESCRIPTION: Store page, track, track with time to view/listen l"
  },
  {
    "path": "Extensions/fullAppDisplay.js",
    "chars": 25082,
    "preview": "// NAME: Full App Display\n// AUTHOR: khanhas\n// VERSION: 1.0\n// DESCRIPTION: Fancy artwork and track status display.\n\n//"
  },
  {
    "path": "Extensions/keyboardShortcut.js",
    "chars": 10786,
    "preview": "// NAME: Keyboard Shortcut\n// AUTHOR: khanhas, OhItsTom\n// DESCRIPTION: Register a few more keybinds to support keyboard"
  },
  {
    "path": "Extensions/loopyLoop.js",
    "chars": 3684,
    "preview": "// NAME: Loopy loop\n// AUTHOR: khanhas\n// VERSION: 0.1\n// DESCRIPTION: Simple tool to help you practice hitting that not"
  },
  {
    "path": "Extensions/popupLyrics.js",
    "chars": 39312,
    "preview": "// NAME: Popup Lyrics\n// AUTHOR: khanhas\n//         Netease API parser and UI from https://github.com/mantou132/Spotify-"
  },
  {
    "path": "Extensions/shuffle+.js",
    "chars": 17224,
    "preview": "// NAME: Shuffle+\n// AUTHORS: khanhas, Tetrax-10\n// DESCRIPTION: True shuffle with no bias.\n\n/// <reference path=\"../glo"
  },
  {
    "path": "Extensions/trashbin.js",
    "chars": 11828,
    "preview": "// NAME: Trashbin\n// AUTHOR: khanhas and OhItsTom\n// DESCRIPTION: Throw songs to trashbin and never hear it again.\n\n/// "
  },
  {
    "path": "Extensions/webnowplaying.js",
    "chars": 11584,
    "preview": "// NAME: WebNowPlaying\n// AUTHOR: khanhas, keifufu (based on https://github.com/keifufu/WebNowPlaying-Redux)\n// DESCRIPT"
  },
  {
    "path": "LICENSE",
    "chars": 26526,
    "preview": "                  GNU LESSER GENERAL PUBLIC LICENSE\n                       Version 2.1, February 1999\n\n Copyright (C) 19"
  },
  {
    "path": "README.md",
    "chars": 1521,
    "preview": "<h3 align=\"center\"><a href=\"https://spicetify.app/\"><img src=\"https://i.imgur.com/iwcLITQ.png\" width=\"600px\"></a></h3>\n<"
  },
  {
    "path": "Themes/SpicetifyDefault/color.ini",
    "chars": 3922,
    "preview": "; COLORS KEYS DESCRIPTION\n; text               = Main field text; playlist names in main field and sidebar; headings.\n; "
  },
  {
    "path": "Themes/SpicetifyDefault/user.css",
    "chars": 4320,
    "preview": ":root {\n\t--player-bar-height: 105px;\n}\n\n.main-rootlist-rootlistDividerGradient {\n\tbackground: unset;\n}\n\ninput {\n\tbackgro"
  },
  {
    "path": "biome.json",
    "chars": 655,
    "preview": "{\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\"organizeI"
  },
  {
    "path": "css-map.json",
    "chars": 114768,
    "preview": "{\n\t\"n8Bz0c0v17whD3KfMdOk\": \"album-albumPage-sectionWrapper\",\n\t\"HPNSn7d7aZf4nfre61sk\": \"artist-artistAbout-about\",\n\t\"xaeu"
  },
  {
    "path": "globals.d.ts",
    "chars": 61745,
    "preview": "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-do"
  },
  {
    "path": "go.mod",
    "chars": 834,
    "preview": "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"
  },
  {
    "path": "go.sum",
    "chars": 11684,
    "preview": "atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg=\natomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3Q"
  },
  {
    "path": "install.ps1",
    "chars": 7063,
    "preview": "$ErrorActionPreference = 'Stop'\r\n[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\r\n\r\n#reg"
  },
  {
    "path": "install.sh",
    "chars": 4323,
    "preview": "#!/usr/bin/env sh\n# Copyright 2022 khanhas.\n# Copyright 2023-present Spicetify contributors.\n# Edited from project Denol"
  },
  {
    "path": "jsHelper/expFeatures.js",
    "chars": 11341,
    "preview": "(async () => {\n\tlet overrideList;\n\tlet prevSessionOverrideList = [];\n\tconst newFeatures = [];\n\tlet hooksPatched = false;"
  },
  {
    "path": "jsHelper/homeConfig.js",
    "chars": 5692,
    "preview": "SpicetifyHomeConfig = {};\n\n(async () => {\n\t// Status enum\n\tconst NORMAL = 0;\n\tconst STICKY = 1;\n\tconst LOWERED = 2;\n\t// "
  },
  {
    "path": "jsHelper/sidebarConfig.js",
    "chars": 8858,
    "preview": "(function SidebarConfig() {\n\tconst sidebar = document.querySelector(\".Root__nav-bar\");\n\tif (!sidebar) return setTimeout("
  },
  {
    "path": "jsHelper/spicetifyWrapper.js",
    "chars": 123331,
    "preview": "window.Spicetify = {\n\tPlayer: {\n\t\taddEventListener: (type, callback) => {\n\t\t\tif (!(type in Spicetify.Player.eventListene"
  },
  {
    "path": "manifest.json",
    "chars": 2272,
    "preview": "[\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 poli"
  },
  {
    "path": "spicetify.go",
    "chars": 16254,
    "preview": "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"
  },
  {
    "path": "src/apply/apply.go",
    "chars": 15462,
    "preview": "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//"
  },
  {
    "path": "src/backup/backup.go",
    "chars": 816,
    "preview": "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 Ap"
  },
  {
    "path": "src/cmd/apply.go",
    "chars": 12211,
    "preview": "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\t"
  },
  {
    "path": "src/cmd/auto.go",
    "chars": 935,
    "preview": "package cmd\n\nimport (\n\t\"os\"\n\n\tbackupstatus \"github.com/spicetify/cli/src/status/backup\"\n\tspotifystatus \"github.com/spice"
  },
  {
    "path": "src/cmd/backup.go",
    "chars": 5292,
    "preview": "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"
  },
  {
    "path": "src/cmd/block-updates.go",
    "chars": 1883,
    "preview": "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/u"
  },
  {
    "path": "src/cmd/cmd.go",
    "chars": 8087,
    "preview": "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.co"
  },
  {
    "path": "src/cmd/color.go",
    "chars": 2795,
    "preview": "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/sr"
  },
  {
    "path": "src/cmd/config-dir.go",
    "chars": 356,
    "preview": "package cmd\n\nimport (\n\t\"github.com/spicetify/cli/src/utils\"\n)\n\n// ShowConfigDirectory shows config directory in user's d"
  },
  {
    "path": "src/cmd/config.go",
    "chars": 4931,
    "preview": "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"
  },
  {
    "path": "src/cmd/devtools.go",
    "chars": 1876,
    "preview": "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"
  },
  {
    "path": "src/cmd/patch.go",
    "chars": 1834,
    "preview": "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()"
  },
  {
    "path": "src/cmd/path.go",
    "chars": 2857,
    "preview": "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 "
  },
  {
    "path": "src/cmd/restart.go",
    "chars": 1883,
    "preview": "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 r"
  },
  {
    "path": "src/cmd/update.go",
    "chars": 2743,
    "preview": "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/"
  },
  {
    "path": "src/cmd/watch.go",
    "chars": 5598,
    "preview": "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/"
  },
  {
    "path": "src/preprocess/preprocess.go",
    "chars": 35369,
    "preview": "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"
  },
  {
    "path": "src/status/backup/backup.go",
    "chars": 1179,
    "preview": "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\ts"
  },
  {
    "path": "src/status/spotify/spotify.go",
    "chars": 1394,
    "preview": "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 in"
  },
  {
    "path": "src/utils/color.go",
    "chars": 4139,
    "preview": "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[str"
  },
  {
    "path": "src/utils/config.go",
    "chars": 7651,
    "preview": "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 "
  },
  {
    "path": "src/utils/file-utils.go",
    "chars": 2543,
    "preview": "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(inpu"
  },
  {
    "path": "src/utils/isAdmin/unix.go",
    "chars": 180,
    "preview": "//go:build !windows\n// +build !windows\n\npackage isAdmin\n\nimport \"os\"\n\nfunc Check(bypassAdminCheck bool) bool {\n\tif bypas"
  },
  {
    "path": "src/utils/isAdmin/windows.go",
    "chars": 541,
    "preview": "//go:build windows\n// +build windows\n\npackage isAdmin\n\nimport (\n\t\"golang.org/x/sys/windows\"\n)\n\nfunc Check(bypassAdminChe"
  },
  {
    "path": "src/utils/path-utils.go",
    "chars": 5822,
    "preview": "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 runti"
  },
  {
    "path": "src/utils/print.go",
    "chars": 2096,
    "preview": "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: "
  },
  {
    "path": "src/utils/scanner.go",
    "chars": 311,
    "preview": "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\nfu"
  },
  {
    "path": "src/utils/show-dir.go",
    "chars": 531,
    "preview": "package utils\n\nimport (\n\t\"os/exec\"\n\t\"runtime\"\n)\n\n// ShowDirectory shows directory in user's default file manager applica"
  },
  {
    "path": "src/utils/utils.go",
    "chars": 8794,
    "preview": "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"
  },
  {
    "path": "src/utils/vcs.go",
    "chars": 638,
    "preview": "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 `jso"
  },
  {
    "path": "src/utils/watcher.go",
    "chars": 2620,
    "preview": "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."
  }
]

About this extraction

This page contains the full source code of the spicetify/cli GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 100 files (930.5 KB), approximately 310.0k tokens, and a symbol index with 688 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!