[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: hiaaryan\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\npolar: # Replace with a single Polar username\nbuy_me_a_coffee: # Replace with a single Buy Me a Coffee username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/report_issue.yml",
    "content": "name: Bug Report 👾\ndescription: File a bug report.\ntitle: \"[Bug]: \"\nlabels: [\"bug\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report!\n  - type: textarea\n    id: what-happened\n    attributes:\n      label: What Happened?\n      description: Also tell us, what did you expect to happen?\n      placeholder: Ex. I expected the page to load, but instead I got a 404 error.\n    validations:\n      required: true\n  - type: input\n    id: version\n    attributes:\n      label: Wora Version\n      description: Which version of Wora did this bug happen on?\n      placeholder: Ex. 0.3.2\n    validations:\n      required: true\n  - type: input\n    id: os\n    attributes:\n      label: Operating System\n      description: What operating system are you using?\n      placeholder: Ex. Windows 10, macOS 11.2\n    validations:\n      required: true\n  - type: textarea\n    id: steps-to-reproduce\n    attributes:\n      label: Steps to Reproduce\n      description: Please provide step-by-step instructions to reproduce the issue.\n      placeholder: Ex. 1. Go to '...' 2. Click on '...' 3. Scroll down to '...'\n    validations:\n      required: true\n  - type: textarea\n    id: environment-details\n    attributes:\n      label: Environment Details\n      description: Provide any additional details about your environment that might be relevant (e.g., hardware, network conditions).\n      placeholder: Ex. Running on a high-latency network, using an external sound card.\n    validations:\n      required: false\n  - type: dropdown\n    id: severity\n    attributes:\n      label: Severity\n      description: How severe is this issue? (e.g., Minor, Major, Critical)\n      options:\n        - Minor\n        - Major\n        - Critical\n      default: 0\n    validations:\n      required: true\n  - type: textarea\n    id: logs\n    attributes:\n      label: Screenshots/Logs\n      description: Attach any screenshots or logs that might help in diagnosing the problem.\n      placeholder: Ex. Drag and drop your screenshots or logs here.\n    validations:\n      required: false\n  - type: input\n    id: contact\n    attributes:\n      label: Discord Username\n      description: How can we get in touch with you if we need more info?\n      placeholder: Ex. charlie3x, bluespin2e\n    validations:\n      required: false\n  - type: checkboxes\n    id: terms\n    attributes:\n      label: Code of Conduct\n      description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/playwora/wora/blob/main/CODE_OF_CONDUCT.md).\n      options:\n        - label: I agree to follow this project's Code of Conduct\n          required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/request_feature.yml",
    "content": "name: Feature Request 🌟\ndescription: Suggest a new feature or enhancement.\ntitle: \"[Feature]: \"\nlabels: [\"enhancement\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to suggest a feature!\n  - type: textarea\n    id: feature-description\n    attributes:\n      label: Feature Description\n      description: Describe the feature you would like to see.\n      placeholder: Ex. I would like to have a dark mode option in the settings.\n    validations:\n      required: true\n  - type: textarea\n    id: problem-solution\n    attributes:\n      label: Problem and Solution\n      description: Describe the problem this feature will solve and how you envision the solution.\n      placeholder: Ex. The app is too bright at night, a dark mode would make it easier on the eyes.\n    validations:\n      required: true\n  - type: textarea\n    id: additional-context\n    attributes:\n      label: Additional Context\n      description: Provide any other context or screenshots about the feature request.\n      placeholder: Ex. Similar to how dark mode works in other apps.\n    validations:\n      required: false\n  - type: textarea\n    id: potential-issues\n    attributes:\n      label: Potential Issues\n      description: Are there any potential issues or challenges with this feature?\n      placeholder: Ex. It might be challenging to ensure all UI elements are visible in dark mode.\n    validations:\n      required: false\n  - type: input\n    id: version\n    attributes:\n      label: Wora Version\n      description: Which version of Wora are you using?\n      placeholder: Ex. 0.3.2\n    validations:\n      required: true\n  - type: input\n    id: contact\n    attributes:\n      label: Discord Username\n      description: How can we get in touch with you if we need more info?\n      placeholder: Ex. charlie3x, bluespin2e\n    validations:\n      required: false\n  - type: checkboxes\n    id: terms\n    attributes:\n      label: Code of Conduct\n      description: By submitting this request, you agree to follow our [Code of Conduct](https://github.com/playwora/wora/blob/main/CODE_OF_CONDUCT.md).\n      options:\n        - label: I agree to follow this project's Code of Conduct\n          required: true\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release and Build\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  check-version-change:\n    runs-on: ubuntu-latest\n    outputs:\n      version_changed: ${{ steps.check.outputs.version_changed }}\n      new_version: ${{ steps.check.outputs.new_version }}\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 2\n      - name: Check Version Change\n        id: check\n        run: |\n          git diff HEAD^ HEAD --name-only | grep -q '^package.json$' || exit 0\n          old_version=$(git show HEAD^:package.json | jq -r '.version')\n          new_version=$(jq -r '.version' package.json)\n          if [ \"$old_version\" != \"$new_version\" ] && [ $(git diff HEAD^ HEAD --name-only | wc -l) -eq 1 ]; then\n            echo \"version_changed=true\" >> $GITHUB_OUTPUT\n            echo \"new_version=$new_version\" >> $GITHUB_OUTPUT\n          else\n            echo \"version_changed=false\" >> $GITHUB_OUTPUT\n          fi\n\n  build:\n    needs: check-version-change\n    if: needs.check-version-change.outputs.version_changed == 'true'\n    strategy:\n      matrix:\n        os: [macos-latest, ubuntu-latest, windows-latest]\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: \"20\"\n      - uses: oven-sh/setup-bun@v2\n        with:\n          bun-version: latest\n      - name: Install Dependencies\n        run: bun install\n      - name: Build for ${{ matrix.os }}\n        run: |\n          if [ \"${{ matrix.os }}\" == \"macos-latest\" ]; then\n            bun run build:mac\n          elif [ \"${{ matrix.os }}\" == \"ubuntu-latest\" ]; then\n            bun run build:linux\n          elif [ \"${{ matrix.os }}\" == \"windows-latest\" ]; then\n            bun run build:win64\n          fi\n        shell: bash\n      - name: Get Asset Details\n        id: get_asset\n        run: |\n          if [ \"${{ matrix.os }}\" == \"macos-latest\" ]; then\n            echo \"asset_path=./dist/*.dmg\" >> $GITHUB_OUTPUT\n          elif [ \"${{ matrix.os }}\" == \"ubuntu-latest\" ]; then\n            echo \"asset_path=./dist/*.AppImage\" >> $GITHUB_OUTPUT\n          elif [ \"${{ matrix.os }}\" == \"windows-latest\" ]; then\n            echo \"asset_path=./dist/*.exe\" >> $GITHUB_OUTPUT\n          fi\n        shell: bash\n      - name: Release\n        uses: softprops/action-gh-release@v2\n        if: success()\n        with:\n          tag_name: v${{ needs.check-version-change.outputs.new_version }}\n          name: v${{ needs.check-version-change.outputs.new_version }}\n          draft: false\n          prerelease: false\n          files: ${{ steps.get_asset.outputs.asset_path }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n*.log\n.next\napp\ndist\n.DS_Store\n.db\n\n# Environment variables\n.env\n.env.local\n.env.development\n.env.production\n.vercel\n"
  },
  {
    "path": ".prettierignore",
    "content": "build\ncoverage\napp\ndist\n.yarn\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"plugins\": [\"prettier-plugin-tailwindcss\"],\n  \"tailwindConfig\": \"./renderer/tailwind.config.js\"\n}\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "<p align=\"center\">\n  <img src=\"https://github.com/playwora/wora/blob/main/renderer/public/github/Header.png?raw=true\" alt=\"Wora Logo\" />\n</p>\n\n<p align=\"center\">\n  <a href=\"https://github.com/playwora/wora\"><img alt=\"GitHub Actions Workflow Status\" src=\"https://img.shields.io/github/actions/workflow/status/playwora/wora/release.yml\"></a>\n  <a href=\"https://github.com/playwora/wora\"><img src=\"https://img.shields.io/github/last-commit/playwora/wora/main?commit\" alt=\"Last Commit\" /></a>\n  <a href=\"LICENSE\"><img src=\"https://img.shields.io/github/license/playwora/wora?license\" alt=\"License\" /></a>\n  <a href=\"https://discord.gg/CrAbAYMGCe\"><img src=\"https://dcbadge.limes.pink/api/server/https://discord.gg/CrAbAYMGCe?style=flat\" alt=\"Discord\" /></a>\n  <a href=\"https://github.com/playwora/wora/stargazers\"><img src=\"https://img.shields.io/github/stars/playwora/wora?style=flat&stars\" alt=\"GitHub Stars\" /></a>\n  <a href=\"https://github.com/playwora/wora/network\"><img src=\"https://img.shields.io/github/forks/playwora/wora?style=flat&forks\" alt=\"GitHub Forks\" /></a>\n  <a href=\"https://github.com/playwora/wora/watchers\"><img src=\"https://img.shields.io/github/watchers/playwora/wora?style=flat&watchers\" alt=\"GitHub Watchers\" /></a>\n</p>\n\n## 🤝 Contributor Covenant Code of Conduct\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation 🌟\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community 🌈\n\n## 📄 Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n- Demonstrating empathy and kindness toward other people 🤗\n- Being respectful of differing opinions, viewpoints, and experiences 🤝\n- Giving and gracefully accepting constructive feedback 🎯\n- Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience 🙏\n- Focusing on what is best not just for us as individuals, but for the\n  overall community 🌍\n\nExamples of unacceptable behavior include:\n\n- The use of sexualized language or imagery, and sexual attention or\n  advances of any kind 🚫\n- Trolling, insulting or derogatory comments, and personal or political attacks 🗣️\n- Public or private harassment 🔇\n- Publishing others' private information, such as a physical or email\n  address, without their explicit permission 🕵️\n- Other conduct which could reasonably be considered inappropriate in a\n  professional setting ❌\n\n## ⭐️ Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful. ⚖️\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate. ✏️\n\n## 🙌 Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. 🌐\n\n## 👍 Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\n[https://discord.gg/CrAbAYMGCe](https://discord.gg/CrAbAYMGCe). 🔗\nAll complaints will be reviewed and investigated promptly and fairly. ⏱️\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident. 🔒\n\n## 🙏 Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested. 📝\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban. ⚠️\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban. ⛔\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior, harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community. 🚷\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "<p align=\"center\">\n  <img src=\"https://github.com/playwora/wora/blob/main/renderer/public/github/Header.png?raw=true\" alt=\"Wora Logo\" />\n</p>\n\n<p align=\"center\">\n  <a href=\"https://github.com/playwora/wora\"><img alt=\"GitHub Actions Workflow Status\" src=\"https://img.shields.io/github/actions/workflow/status/playwora/wora/release.yml\"></a>\n  <a href=\"https://github.com/playwora/wora\"><img src=\"https://img.shields.io/github/last-commit/playwora/wora/main?commit\" alt=\"Last Commit\" /></a>\n  <a href=\"LICENSE\"><img src=\"https://img.shields.io/github/license/playwora/wora?license\" alt=\"License\" /></a>\n  <a href=\"https://discord.gg/CrAbAYMGCe\"><img src=\"https://dcbadge.limes.pink/api/server/https://discord.gg/CrAbAYMGCe?style=flat\" alt=\"Discord\" /></a>\n  <a href=\"https://github.com/playwora/wora/stargazers\"><img src=\"https://img.shields.io/github/stars/playwora/wora?style=flat&stars\" alt=\"GitHub Stars\" /></a>\n  <a href=\"https://github.com/playwora/wora/network\"><img src=\"https://img.shields.io/github/forks/playwora/wora?style=flat&forks\" alt=\"GitHub Forks\" /></a>\n  <a href=\"https://github.com/playwora/wora/watchers\"><img src=\"https://img.shields.io/github/watchers/playwora/wora?style=flat&watchers\" alt=\"GitHub Watchers\" /></a>\n</p>\n\n## 🤝 Contributing to Wora\n\nThank you for considering contributing to **Wora**! 🎉 We welcome contributions from everyone. We have prepared some guidelines for you to get started ✅\n\n## 🛠️ Project Setup\n\nWora is an Electron app built with Next.js and TailwindCSS, using BetterSQLite3 with Drizzle ORM for database management. Here's an overview of the database schema:\n\n```mermaid\nerDiagram\n    settings {\n        int id\n        string name\n        string profilePicture\n        string musicFolder\n    }\n\n    songs {\n        int id\n        string filePath\n        string name\n        string artist\n        int duration\n        int albumID\n    }\n\n    albums {\n        int id\n        string name\n        string artist\n        int year\n        string coverArt\n    }\n\n    playlists {\n        int id\n        string name\n        string description\n        string coverArt\n    }\n\n    playlistSongs {\n        int playlistId\n        int songId\n    }\n\n    albums ||--|{ songs : \"\"\n    playlists ||--o{ playlistSongs : \"\"\n    songs ||--o{ playlistSongs : \"\"\n```\n\n## 🎯 **How to Contribute**\n\nOnce you get hold of the DB, please check out the file structure in the main branch to get yourself more familiar with the project. If you encounter any issues, support for developers is available through our discord server 🛠️\n\n<a href=\"https://discord.gg/CrAbAYMGCe\"><img src=\"https://dcbadge.limes.pink/api/server/https://discord.gg/CrAbAYMGCe?style=flat\" alt=\"Discord\" /></a>\n\n1. **Fork the Repository**\n\nFork the [repository](https://github.com/playwora/wora) and clone it locally:\n\n```sh\ngit clone https://github.com/your-username/wora.git\ncd wora\n```\n\n2. **Create a New Branch**\n\nCreate a new branch for your feature or bugfix:\n\n```sh\ngit checkout -b feature-branch\n```\n\n3. **Install Dependencies**\n\nInstall the required dependencies:\n\n```sh\nyarn install\n```\n\n4. **Start Development Server**\n\nRun the development server to see your changes:\n\n```sh\nyarn dev\n```\n\n5. **Commit Your Changes**\n\nCommit your changes with a meaningful message:\n\n```sh\ngit commit -am 'Add new feature ✅'\n```\n\n6. **Push to Your Branch**\n\nPush the changes to your branch on GitHub:\n\n```sh\ngit push origin feature-branch\n```\n\n7. **Create a Pull Request**\n\nGo to the original repository on GitHub and create a new pull request. Please also read our [Code of Conduct](CODE_OF_CONDUCT.md) to understand the expectations for behavior within our community 🙏\n\n## 💬 Join the Community\n\nJoin our [Discord server](https://discord.gg/CrAbAYMGCe) to connect with other users and developers 🤝\n\n<a href=\"https://discord.gg/CrAbAYMGCe\"><img src=\"https://dcbadge.limes.pink/api/server/https://discord.gg/CrAbAYMGCe?style=flat\" alt=\"Discord\"></a>\n\n---\n\nMIT License. Made with ❤️ by [hiaaryan](https://github.com/hiaaryan) and contributors.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Wora\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "> [!IMPORTANT]  \n> There is a migrated version which is being built with tauri (rust 🦀). During this time contributions to this repo are severely limited and only critical fixes would be merged. Please join our [Discord](https://discord.gg/CrAbAYMGCe) to follow updates on the new version.\n\n<p align=\"center\">\n  <img src=\"https://github.com/playwora/wora/blob/main/renderer/public/github/Header.png?raw=true\" alt=\"Wora Logo\" />\n</p>\n\n<p align=\"center\">\n  <a href=\"https://github.com/playwora/wora\"><img alt=\"GitHub Actions Workflow Status\" src=\"https://img.shields.io/github/actions/workflow/status/playwora/wora/release.yml\"></a>\n  <a href=\"https://github.com/playwora/wora\"><img src=\"https://img.shields.io/github/last-commit/playwora/wora/main?commit\" alt=\"Last Commit\" /></a>\n  <a href=\"LICENSE\"><img src=\"https://img.shields.io/github/license/playwora/wora?license\" alt=\"License\" /></a>\n  <a href=\"https://discord.gg/CrAbAYMGCe\"><img src=\"https://dcbadge.limes.pink/api/server/https://discord.gg/CrAbAYMGCe?style=flat\" alt=\"Discord\" /></a>\n  <a href=\"https://github.com/playwora/wora/stargazers\"><img src=\"https://img.shields.io/github/stars/playwora/wora?style=flat&stars\" alt=\"GitHub Stars\" /></a>\n  <a href=\"https://github.com/playwora/wora/network\"><img src=\"https://img.shields.io/github/forks/playwora/wora?style=flat&forks\" alt=\"GitHub Forks\" /></a>\n  <a href=\"https://github.com/playwora/wora/releases\"><img alt=\"GitHub Downloads\" src=\"https://img.shields.io/github/downloads/playwora/wora/total?style=flat\"></a>\n</p>\n\n## ⭐️ Description\n\n**Wora** is a beautiful player for audiophiles. An open-source lossless music player app that lets you organize and play your favorite tracks seamlessly. With Wora, you can:\n\n- Create and manage playlists 🎉\n- Stream FLACs, WAVs apart from regular music extensions 🎧\n- Quick play using command menu ⌨️\n- View synced and unsynced lyrics 💬\n- Admire the beautiful UI ✨\n\n<p align=\"center\">\n  <img src=\"https://github.com/playwora/wora/blob/main/renderer/public/github/Home%20Page.png?raw=true\" alt=\"Screenshot 1\" />\n  <img src=\"https://github.com/playwora/wora/blob/main/renderer/public/github/Search%20Console.png?raw=true\" alt=\"Screenshot 2\" />\n  <img src=\"https://github.com/playwora/wora/blob/main/renderer/public/github/Album%20Page.png?raw=true\" alt=\"Screenshot 3\" />\n  <img src=\"https://github.com/playwora/wora/blob/main/renderer/public/github/Synced%20Lyrics.png?raw=true\" alt=\"Screenshot 4\" />\n</p>\n\n## 🚀 Getting Started\n\nA bit simpler process would be to download the latest build through [here](https://github.com/playwora/wora/releases/). But if you want to fiddle around, then please follow the below steps which would help you get started. If you encounter any issues, support is available through our discord server 🛠️\n\n<a href=\"https://discord.gg/CrAbAYMGCe\"><img src=\"https://dcbadge.limes.pink/api/server/https://discord.gg/CrAbAYMGCe?style=flat\" alt=\"Discord\" /></a>\n\n### 〽️ Prerequisites\n\n- [Node.js](https://nodejs.org/) v14 or higher\n- [Git](https://git-scm.com/) for obvious reasons\n- [Bun](https://bun.sh/) for dependencies\n\n### 👾 Installation\n\n1. **Clone the repository:**\n\n   ```sh\n   git clone https://github.com/playwora/wora.git\n   cd wora\n   ```\n\n2. **Install the dependencies:**\n\n   ```sh\n   bun install\n   ```\n\n3. **Start the application:**\n\n   ```sh\n   bun run dev\n   ```\n\n4. **Build the application**\n\n   ```sh\n   bun run build\n   ```\n\n## 🤝 Contributing\n\nContributions are always welcome! Please read the [Contributing Guide](CONTRIBUTING.md) to learn about the process and how to submit your contributions.\n\n1. Fork the repository\n2. Create a new branch (`git checkout -b feature-branch`)\n3. Commit your changes (`git commit -am 'Add new feature'`)\n4. Push to the branch (`git push origin feature-branch`)\n5. Create a new Pull Request\n\n## 💬 Join the Community\n\nJoin our [Discord server](https://discord.gg/CrAbAYMGCe) to connect with other users and developers.\n\n<a href=\"https://discord.gg/CrAbAYMGCe\"><img src=\"https://dcbadge.limes.pink/api/server/https://discord.gg/CrAbAYMGCe?style=flat\" alt=\"Discord\"></a>\n\n---\n\n<br />\n<a href=\"https://vercel.com/oss\">\n  <img alt=\"Vercel OSS Program\" src=\"https://vercel.com/oss/program-badge.svg\" />\n</a>\n<br />\n<br />\n\nMIT License. Made with ❤️ by [hiaaryan](https://github.com/hiaaryan) and contributors.\n"
  },
  {
    "path": "components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.ts\",\n    \"css\": \"app/globals.css\",\n    \"baseColor\": \"neutral\",\n    \"cssVariables\": false,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\"\n  }\n}\n"
  },
  {
    "path": "electron-builder.yml",
    "content": "appId: com.wora.player\nproductName: Wora\ncopyright: Copyright © 2024 Aaryan Kapoor\ndirectories:\n  output: dist\n  buildResources: resources\nfiles:\n  - from: .\n    filter:\n      - package.json\n      - app\npublish: null\nartifactName: Wora [v${version}].${ext}\nlinux:\n  target:\n    - AppImage\n  category: Audio\nmac:\n  target:\n    - dmg\n  category: public.app-category.music\nwin:\n  target:\n    - nsis\nfileAssociations:\n  - ext: mp3\n    name: MP3 Audio File\n  - ext: mpeg\n    name: MPEG Audio File\n  - ext: opus\n    name: Opus Audio File\n  - ext: ogg\n    name: OGG Audio File\n  - ext: oga\n    name: OGA Audio File\n  - ext: wav\n    name: WAV Audio File\n  - ext: aac\n    name: AAC Audio File\n  - ext: caf\n    name: CAF Audio File\n  - ext: m4a\n    name: M4A Audio File\n  - ext: m4b\n    name: M4B Audio File\n  - ext: mp4\n    name: MP4 Audio File\n  - ext: weba\n    name: WEBA Audio File\n  - ext: webm\n    name: WEBM Audio File\n  - ext: flac\n    name: FLAC Audio File\n"
  },
  {
    "path": "main/background.ts",
    "content": "import path from \"path\";\nimport { Menu, Tray, app, dialog, ipcMain, shell } from \"electron\";\nimport serve from \"electron-serve\";\nimport { createWindow } from \"./helpers\";\nimport { protocol } from \"electron\";\nimport {\n  addSongToPlaylist,\n  addToFavourites,\n  createPlaylist,\n  db,\n  getAlbumWithSongs,\n  getAlbums,\n  getAllArtists,\n  getArtistWithAlbums,\n  getLastFmSettings,\n  getLibraryStats,\n  getPlaylistWithSongs,\n  getPlaylists,\n  getRandomLibraryItems,\n  getSettings,\n  initializeData,\n  isSongFavorite,\n  migrateDatabase,\n  removeSongFromPlaylist,\n  searchDB,\n  searchSongs,\n  updateLastFmSettings,\n  deletePlaylist,\n  updatePlaylist,\n  updateSettings,\n  getSongs,\n  getAlbumsWithDuration,\n} from \"./helpers/db/connectDB\";\nimport { initDatabase } from \"./helpers/db/createDB\";\nimport { parseFile } from \"music-metadata\";\nimport fs from \"fs\";\nimport { Client } from \"@xhayper/discord-rpc\";\nimport { eq, sql } from \"drizzle-orm\";\nimport { initializeLastFmHandlers } from \"./helpers/lastfm-service\";\nimport * as electronLog from \"electron-log\";\n\n// Configure application logging for production\nelectronLog.transports.file.level = \"info\";\nconst logger = electronLog.default;\n\n// Log application startup\nlogger.info(`Wora starting up - ${new Date().toISOString()}`);\nlogger.info(`Node environment: ${process.env.NODE_ENV}`);\nlogger.info(`Electron version: ${process.versions.electron}`);\nlogger.info(`Chrome version: ${process.versions.chrome}`);\nlogger.info(`OS: ${process.platform} ${process.arch}`);\n\nconst isProd = process.env.NODE_ENV === \"production\";\n\n// Set the app user model id for Windows\nif (process.platform === \"win32\") {\n  app.setAppUserModelId(\"com.hiaaryan.wora\");\n}\n\nif (isProd) {\n  logger.info(\"Running in production mode\");\n  serve({ directory: \"app\" });\n} else {\n  logger.info(\"Running in development mode\");\n  app.setPath(\"userData\", `${app.getPath(\"userData\")}`);\n}\n\nlet mainWindow: any;\nlet settings: any;\n\n// Global cache for frequently accessed data\nconst dataCache = {\n  libraryStats: null,\n  randomItems: null,\n  lastUpdated: 0,\n};\n\n// @hiaaryan: Initialize Database on Startup with optimized loading\nconst initializeLibrary = async () => {\n  try {\n    // Initialize SQLite database\n    await initDatabase();\n\n    // Run database migrations for schema updates\n    await migrateDatabase();\n\n    // Only load essential data at startup (settings)\n    settings = await getSettings();\n\n    if (settings) {\n      // Start a non-blocking initialization of the music library\n      // This allows the app UI to load while data is being processed\n      setTimeout(() => {\n        initializeData(settings.musicFolder, true)\n          .then(() => {\n            // Pre-cache some common data for faster access\n            Promise.all([getLibraryStats(), getRandomLibraryItems()]).then(\n              ([stats, randomItems]) => {\n                dataCache.libraryStats = stats;\n                dataCache.randomItems = randomItems;\n                dataCache.lastUpdated = Date.now();\n\n                // Notify renderer that library is fully loaded\n                if (mainWindow) {\n                  mainWindow.webContents.send(\"library-initialized\");\n                }\n              },\n            );\n          })\n          .catch((err) => {\n            console.error(\"Error initializing music library:\", err);\n          });\n      }, 1000); // Delay initialization to prioritize app UI loading\n    }\n  } catch (error) {\n    console.error(\"Error initializing library:\", error);\n  }\n};\n\n(async () => {\n  await app.whenReady();\n  await initializeLibrary();\n\n  // Initialize Last.fm IPC handlers\n  initializeLastFmHandlers();\n\n  // @hiaaryan: Using Depreciated API [Seeking Not Supported with Net]\n  protocol.registerFileProtocol(\"wora\", (request, callback) => {\n    callback({ path: decodeURIComponent(request.url.replace(\"wora://\", \"\")) });\n  });\n\n  mainWindow = createWindow(\"main\", {\n    width: 1500,\n    height: 900,\n    titleBarStyle: \"hidden\",\n    trafficLightPosition: { x: 20, y: 20 },\n    transparent: true,\n    frame: false,\n    icon: path.join(__dirname, \"resources/icon.icns\"),\n    webPreferences: {\n      preload: path.join(__dirname, \"preload.js\"),\n      backgroundThrottling: false,\n    },\n  });\n\n  ipcMain.on(\"quitApp\", async () => {\n    return app.quit();\n  });\n\n  ipcMain.on(\"minimizeWindow\", async () => {\n    return mainWindow.minimize();\n  });\n\n  ipcMain.on(\"maximizeWindow\", async (_, isMaximized: boolean) => {\n    if (isMaximized) {\n      return mainWindow.maximize(isMaximized);\n    } else {\n      return mainWindow.unmaximize();\n    }\n  });\n\n  if (settings) {\n    if (isProd) {\n      await mainWindow.loadURL(\"app://./home\");\n    } else {\n      const port = process.argv[2];\n      await mainWindow.loadURL(`http://localhost:${port}/home`);\n    }\n  } else {\n    if (isProd) {\n      await mainWindow.loadURL(\"app://./setup\");\n    } else {\n      const port = process.argv[2];\n      await mainWindow.loadURL(`http://localhost:${port}/setup`);\n    }\n  }\n})();\n\n// @hiaaryan: Initialize Discord RPC\nconst client = new Client({\n  clientId: \"1243707416588320800\",\n});\n\nipcMain.on(\n  \"set-rpc-state\",\n  async (_, { details, state, seek, duration, cover }) => {\n    let startTimestamp, endTimestamp;\n\n    if (duration && seek) {\n      const now = Math.ceil(Date.now());\n      startTimestamp = now - seek * 1000;\n      endTimestamp = now + (duration - seek) * 1000;\n    }\n\n    const setActivity = {\n      details,\n      state,\n      largeImageKey: cover,\n      instance: false,\n      type: 2,\n      startTimestamp: startTimestamp,\n      endTimestamp: endTimestamp,\n      buttons: [\n        { label: \"Support Project\", url: \"https://github.com/playwora/wora\" },\n      ],\n    };\n\n    if (!client.isConnected) {\n      try {\n        await client.login();\n      } catch (error) {\n        console.error(\"Error logging into Discord:\", error);\n      }\n    }\n\n    if (client.isConnected) {\n      client.user.setActivity(setActivity);\n    }\n  },\n);\n\n// @hiaaryan: Called to Rescan Library\nipcMain.handle(\"rescanLibrary\", async () => {\n  await initializeLibrary();\n});\n\n// @hiaaryan: Called to Set Music Folder\nipcMain.handle(\"scanLibrary\", async () => {\n  const diag = await dialog\n    .showOpenDialog({\n      properties: [\"openDirectory\", \"createDirectory\"],\n    })\n    .then(async (result) => {\n      if (result.canceled) {\n        return result;\n      }\n\n      await initializeData(result.filePaths[0]);\n    })\n    .catch((err) => {\n      console.log(err);\n    });\n\n  return diag;\n});\n\n// @hiaaryan: Set Tray for Wora\nlet tray = null;\napp.whenReady().then(() => {\n  const trayIconPath = !isProd\n    ? path.join(__dirname, `../renderer/public/assets/TrayTemplate.png`)\n    : path.join(__dirname, `../app/assets/TrayTemplate.png`);\n  tray = new Tray(trayIconPath);\n  const contextMenu = Menu.buildFromTemplate([\n    { label: \"About\", type: \"normal\", role: \"about\" },\n    { type: \"separator\" },\n    {\n      label: \"GitHub\",\n      type: \"normal\",\n      click: () => {\n        shell.openExternal(\"https://github.com/playwora/wora\");\n      },\n    },\n    {\n      label: \"Discord\",\n      type: \"normal\",\n      click: () => {\n        shell.openExternal(\"https://discord.gg/CrAbAYMGCe\");\n      },\n    },\n    { type: \"separator\" },\n    {\n      label: \"Quit\",\n      type: \"normal\",\n      role: \"quit\",\n      accelerator: \"Cmd+Q\",\n    },\n  ]);\n  tray.setToolTip(\"Wora\");\n  tray.setContextMenu(contextMenu);\n});\n\n// Use cached data when available for frequently accessed endpoints\nipcMain.handle(\"getLibraryStats\", async () => {\n  // Check if we have fresh cached data (less than 5 minutes old)\n  if (dataCache.libraryStats && Date.now() - dataCache.lastUpdated < 300000) {\n    return dataCache.libraryStats;\n  }\n\n  // Otherwise get fresh data and update cache\n  const stats = await getLibraryStats();\n  dataCache.libraryStats = stats;\n  dataCache.lastUpdated = Date.now();\n  return stats;\n});\n\nipcMain.handle(\"getRandomLibraryItems\", async () => {\n  // Check if we have fresh cached data (less than 5 minutes old)\n  if (dataCache.randomItems && Date.now() - dataCache.lastUpdated < 300000) {\n    return dataCache.randomItems;\n  }\n\n  // Otherwise get fresh data and update cache\n  const libraryItems = await getRandomLibraryItems();\n  dataCache.randomItems = libraryItems;\n  dataCache.lastUpdated = Date.now();\n  return libraryItems;\n});\n\n// @hiaaryan: IPC Handlers from Renderer\nipcMain.handle(\"getAlbums\", async (_, page) => {\n  return await getAlbums(page);\n});\n\n// Page state reset handlers\nipcMain.on(\"resetAlbumsPageState\", () => {\n  // Notify renderer to reset albums page state\n  mainWindow.webContents.send(\"resetAlbumsState\");\n});\n\nipcMain.on(\"resetSongsPageState\", () => {\n  // Notify renderer to reset songs page state\n  mainWindow.webContents.send(\"resetSongsState\");\n});\n\nipcMain.on(\"resetPlaylistsPageState\", () => {\n  // Notify renderer to reset playlists page state\n  mainWindow.webContents.send(\"resetPlaylistsState\");\n});\n\nipcMain.on(\"resetHomePageState\", () => {\n  // Notify renderer to reset home page state\n  mainWindow.webContents.send(\"resetHomeState\");\n});\n\nipcMain.handle(\"getAllPlaylists\", async () => {\n  const playlists = await getPlaylists();\n  return playlists;\n});\n\nipcMain.handle(\"getAlbumWithSongs\", async (_, id: number) => {\n  const albumWithSongs = await getAlbumWithSongs(id);\n  return albumWithSongs;\n});\n\nipcMain.handle(\"getPlaylistWithSongs\", async (_, id: number) => {\n  const playlistWithSongs = await getPlaylistWithSongs(id);\n  return playlistWithSongs;\n});\n\nipcMain.handle(\"getSongMetadata\", async (_, file: string) => {\n  const metadata = await parseFile(file, {\n    skipPostHeaders: true,\n    skipCovers: true,\n  });\n\n  const favourite = await isSongFavorite(file);\n\n  return { metadata, favourite };\n});\n\nipcMain.on(\"addToFavourites\", async (_, id: number) => {\n  return addToFavourites(id);\n});\n\nipcMain.handle(\"search\", async (_, query: string) => {\n  const results = await searchDB(query);\n  return results;\n});\n\nipcMain.handle(\"createPlaylist\", async (_, data: any) => {\n  const playlist = await createPlaylist(data);\n  // Invalidate cache when data changes\n  dataCache.lastUpdated = 0;\n  return playlist;\n});\n\nipcMain.handle(\"deletePlaylist\", async (_, data: { id: number; coverPath?: string }) => {\n  return deletePlaylist(data);\n});\n\nipcMain.handle(\"updatePlaylist\", async (_, data: any) => {\n  const playlist = await updatePlaylist(data);\n  // Invalidate cache when data changes\n  dataCache.lastUpdated = 0;\n  return playlist;\n});\n\nipcMain.handle(\"addSongToPlaylist\", async (_, data: any) => {\n  const add = await addSongToPlaylist(data.playlistId, data.songId);\n  // Invalidate cache when data changes\n  dataCache.lastUpdated = 0;\n  return add;\n});\n\nipcMain.handle(\"removeSongFromPlaylist\", async (_, data: any) => {\n  const remove = await removeSongFromPlaylist(data.playlistId, data.songId);\n  // Invalidate cache when data changes\n  dataCache.lastUpdated = 0;\n  return remove;\n});\n\nipcMain.handle(\"getSettings\", async () => {\n  const settings = await getSettings();\n  return settings;\n});\n\nipcMain.handle(\"updateSettings\", async (_, data: any) => {\n  const settings = await updateSettings(data);\n  mainWindow.webContents.send(\"confirmSettingsUpdate\", settings);\n  return settings;\n});\n\nipcMain.handle(\"uploadProfilePicture\", async (_, file) => {\n  const uploadsDir = path.join(\n    app.getPath(\"userData\"),\n    \"utilities/uploads/profile\",\n  );\n  if (!fs.existsSync(uploadsDir)) {\n    fs.mkdirSync(uploadsDir, { recursive: true });\n  }\n\n  const fileName = `profile_${Date.now()}${path.extname(file.name)}`;\n  const filePath = path.join(uploadsDir, fileName);\n\n  fs.writeFileSync(filePath, Buffer.from(file.data));\n\n  return filePath;\n});\n\nipcMain.handle(\"uploadPlaylistCover\", async (_, file) => {\n  const uploadsDir = path.join(\n    app.getPath(\"userData\"),\n    \"utilities/uploads/playlists\",\n  );\n  if (!fs.existsSync(uploadsDir)) {\n    fs.mkdirSync(uploadsDir, { recursive: true });\n  }\n\n  const fileName = `playlists_${Date.now()}${path.extname(file.name)}`;\n  const filePath = path.join(uploadsDir, fileName);\n\n  fs.writeFileSync(filePath, Buffer.from(file.data));\n\n  return filePath;\n});\n\nipcMain.handle(\"getActionsData\", async () => {\n  const isNotMac = process.platform !== \"darwin\";\n  const appVersion = app.getVersion();\n\n  return { isNotMac, appVersion };\n});\n\nipcMain.handle(\"getArtistWithAlbums\", async (_, artist: string) => {\n  const artistData = await getArtistWithAlbums(artist);\n  return artistData;\n});\n\n// Handler to get all artists\nipcMain.handle(\"getAllArtists\", async () => {\n  try {\n    const allArtists = await getAllArtists();\n    return allArtists;\n  } catch (error) {\n    console.error(\"Error getting all artists:\", error);\n    return [];\n  }\n});\n\n// New handler to get all songs for shuffle feature\nipcMain.handle(\"getAllSongs\", async () => {\n  try {\n    console.log(\"Getting all songs for shuffle...\");\n\n    // Get all songs with their album information in a single query for better performance\n    const songsWithAlbums = await db.query.songs.findMany({\n      with: {\n        album: true, // This fetches the full album data for each song\n      },\n      orderBy: sql`RANDOM()`, // Randomize the songs to make shuffling more natural\n    });\n\n    // Transform the data to match the expected format in the frontend\n    const formattedSongs = songsWithAlbums.map((song) => {\n      return {\n        id: song.id,\n        name: song.name || \"Unknown Title\",\n        artist: song.artist || \"Unknown Artist\",\n        duration: song.duration || 0,\n        filePath: song.filePath,\n        album: song.album\n          ? {\n              id: song.album.id,\n              name: song.album.name || \"Unknown Album\",\n              artist: song.album.artist || \"Unknown Artist\",\n              cover: song.album.cover || null,\n              year: song.album.year,\n            }\n          : {\n              id: null,\n              name: \"Unknown Album\",\n              artist: \"Unknown Artist\",\n              cover: null,\n              year: null,\n            },\n      };\n    });\n\n    console.log(\n      `Returning ${formattedSongs.length} songs with complete album data`,\n    );\n    return formattedSongs;\n  } catch (error) {\n    console.error(\"Error in getAllSongs:\", error);\n    return [];\n  }\n});\n\n// New handler to get songs with pagination\nipcMain.handle(\"getSongs\", async (_, page: number = 1) => {\n  try {\n    console.log(`Getting songs for page ${page}...`);\n    const songsWithAlbums = await getSongs(page);\n    return songsWithAlbums;\n  } catch (error) {\n    console.error(\"Error in getSongs:\", error);\n    return [];\n  }\n});\n\n// Handler for searching songs with the new searchSongs function\nipcMain.handle(\"searchSongs\", async (_, query: string) => {\n  try {\n    console.log(`Searching songs with query: \"${query}\"`);\n    const results = await searchSongs(query);\n    console.log(`Found ${results.length} song matches`);\n    return results;\n  } catch (error) {\n    console.error(\"Error in searchSongs:\", error);\n    return [];\n  }\n});\n\n// Handler for getting albums with calculated durations\nipcMain.handle(\"getAlbumsWithDuration\", async (_, page: number = 1) => {\n  try {\n    console.log(`Getting albums with durations for page ${page}...`);\n    const albumsWithDurations = await getAlbumsWithDuration(page);\n    console.log(`Found ${albumsWithDurations.length} albums with durations`);\n    return albumsWithDurations;\n  } catch (error) {\n    console.error(\"Error in getAlbumsWithDuration:\", error);\n    return [];\n  }\n});\n\n// Add LastFM handlers after existing handlers\n\n// Get LastFM settings\nipcMain.handle(\"getLastFmSettings\", async () => {\n  try {\n    const lastFmSettings = await getLastFmSettings();\n    return lastFmSettings;\n  } catch (error) {\n    console.error(\"Error in getLastFmSettings:\", error);\n    return {\n      lastFmUsername: null,\n      lastFmSessionKey: null,\n      enableLastFm: false,\n      scrobbleThreshold: 50,\n    };\n  }\n});\n\n// Update LastFM settings\nipcMain.handle(\"updateLastFmSettings\", async (_, data) => {\n  try {\n    const result = await updateLastFmSettings(data);\n\n    // Notify all renderer processes that Last.fm settings have changed\n    if (mainWindow) {\n      mainWindow.webContents.send(\"lastFmSettingsChanged\", data);\n    }\n\n    return result;\n  } catch (error) {\n    console.error(\"Error in updateLastFmSettings:\", error);\n    return false;\n  }\n});\n\napp.on(\"window-all-closed\", () => {\n  app.quit();\n});\n"
  },
  {
    "path": "main/helpers/create-window.ts",
    "content": "import {\n  screen,\n  BrowserWindow,\n  BrowserWindowConstructorOptions,\n  Rectangle,\n} from \"electron\";\nimport Store from \"electron-store\";\n\nexport const createWindow = (\n  windowName: string,\n  options: BrowserWindowConstructorOptions,\n): BrowserWindow => {\n  const key = \"window-state\";\n  const name = `window-state-${windowName}`;\n  const store = new Store<Rectangle>({ name });\n  const defaultSize = {\n    width: options.width,\n    height: options.height,\n  };\n  let state = {};\n\n  const restore = () => store.get(key, defaultSize);\n\n  const getCurrentPosition = () => {\n    const position = win.getPosition();\n    const size = win.getSize();\n    return {\n      x: position[0],\n      y: position[1],\n      width: size[0],\n      height: size[1],\n    };\n  };\n\n  const windowWithinBounds = (windowState, bounds) => {\n    return (\n      windowState.x >= bounds.x &&\n      windowState.y >= bounds.y &&\n      windowState.x + windowState.width <= bounds.x + bounds.width &&\n      windowState.y + windowState.height <= bounds.y + bounds.height\n    );\n  };\n\n  const resetToDefaults = () => {\n    const bounds = screen.getPrimaryDisplay().bounds;\n    return Object.assign({}, defaultSize, {\n      x: (bounds.width - defaultSize.width) / 2,\n      y: (bounds.height - defaultSize.height) / 2,\n    });\n  };\n\n  const ensureVisibleOnSomeDisplay = (windowState) => {\n    const visible = screen.getAllDisplays().some((display) => {\n      return windowWithinBounds(windowState, display.bounds);\n    });\n    if (!visible) {\n      // Window is partially or fully not visible now.\n      // Reset it to safe defaults.\n      return resetToDefaults();\n    }\n    return windowState;\n  };\n\n  const saveState = () => {\n    if (!win.isMinimized() && !win.isMaximized()) {\n      Object.assign(state, getCurrentPosition());\n    }\n    store.set(key, state);\n  };\n\n  state = ensureVisibleOnSomeDisplay(restore());\n\n  const win = new BrowserWindow({\n    ...state,\n    ...options,\n    webPreferences: {\n      nodeIntegration: false,\n      contextIsolation: true,\n      ...options.webPreferences,\n    },\n  });\n\n  win.on(\"close\", saveState);\n\n  return win;\n};\n"
  },
  {
    "path": "main/helpers/db/connectDB.ts",
    "content": "import { and, eq, like, sql, or, exists, isNotNull } from \"drizzle-orm\";\nimport { albums, songs, settings, playlistSongs, playlists } from \"./schema\";\nimport fs from \"fs\";\nimport { parseFile, selectCover } from \"music-metadata\";\nimport path from \"path\";\nimport { BetterSQLite3Database, drizzle } from \"drizzle-orm/better-sqlite3\";\nimport * as schema from \"./schema\";\nimport { sqlite } from \"./createDB\";\nimport { app } from \"electron\";\n\nexport const db: BetterSQLite3Database<typeof schema> = drizzle(sqlite, {\n  schema,\n});\n\nconst APP_DATA = app.getPath(\"userData\");\nconst ART_DIR = path.join(APP_DATA, \"utilities/uploads/covers\");\n\nconst audioExtensions = [\n  \".mp3\",\n  \".mpeg\",\n  \".opus\",\n  \".ogg\",\n  \".oga\",\n  \".wav\",\n  \".aac\",\n  \".caf\",\n  \".m4a\",\n  \".m4b\",\n  \".mp4\",\n  \".weba\",\n  \".webm\",\n  \".dolby\",\n  \".flac\",\n];\n\nconst imageExtensions = [\".png\", \".jpg\", \".jpeg\", \".gif\", \".bmp\", \".webp\"];\n\nconst processedImages = new Map();\n\nfunction isAudioFile(filePath: string): boolean {\n  return audioExtensions.includes(path.extname(filePath).toLowerCase());\n}\n\nfunction findFirstImageInDirectory(dir: string): string | null {\n  if (processedImages.has(dir)) {\n    return processedImages.get(dir);\n  }\n\n  try {\n    const files = fs.readdirSync(dir);\n    for (const file of files) {\n      const filePath = path.join(dir, file);\n      const stat = fs.statSync(filePath);\n      if (\n        stat.isFile() &&\n        imageExtensions.includes(path.extname(file).toLowerCase())\n      ) {\n        processedImages.set(dir, filePath);\n        return filePath;\n      }\n    }\n  } catch (error) {\n    console.error(`Error reading directory ${dir}:`, error);\n  }\n\n  processedImages.set(dir, null);\n  return null;\n}\n\nfunction readFilesRecursively(dir: string, batch = 100): string[] {\n  let results: string[] = [];\n  let stack = [dir];\n  let count = 0;\n\n  while (stack.length > 0 && count < batch) {\n    const currentDir = stack.pop();\n    try {\n      const items = fs.readdirSync(currentDir);\n\n      for (const item of items) {\n        const itemPath = path.join(currentDir, item);\n        try {\n          const stat = fs.statSync(itemPath);\n\n          if (stat.isDirectory()) {\n            stack.push(itemPath);\n          } else if (isAudioFile(itemPath)) {\n            results.push(itemPath);\n            count++;\n            if (count >= batch) break;\n          }\n        } catch (err) {\n          console.error(`Error accessing ${itemPath}:`, err);\n        }\n      }\n    } catch (err) {\n      console.error(`Error reading directory ${currentDir}:`, err);\n    }\n  }\n\n  return results;\n}\n\nfunction scanEntireLibrary(dir: string): string[] {\n  let results: string[] = [];\n\n  try {\n    const items = fs.readdirSync(dir);\n\n    const chunkSize = 50;\n    for (let i = 0; i < items.length; i += chunkSize) {\n      const chunk = items.slice(i, i + chunkSize);\n\n      for (const item of chunk) {\n        const itemPath = path.join(dir, item);\n        try {\n          const stat = fs.statSync(itemPath);\n          if (stat.isDirectory()) {\n            results.push(...scanEntireLibrary(itemPath));\n          } else if (isAudioFile(itemPath)) {\n            results.push(itemPath);\n          }\n        } catch (err) {\n          console.error(`Error accessing ${itemPath}:`, err);\n        }\n      }\n    }\n  } catch (err) {\n    console.error(`Error reading directory ${dir}:`, err);\n  }\n\n  return results;\n}\n\nexport const getLibraryStats = async () => {\n  const songCount = await db.select({ count: sql`count(*)` }).from(songs);\n  const albumCount = await db.select({ count: sql`count(*)` }).from(albums);\n  const playlistCount = await db\n    .select({ count: sql`count(*)` })\n    .from(playlists);\n\n  return {\n    songs: songCount[0].count,\n    albums: albumCount[0].count,\n    playlists: playlistCount[0].count,\n  };\n};\n\nexport const getSettings = async () => {\n  const settings = await db.select().from(schema.settings).limit(1);\n  return settings[0];\n};\n\nexport const updateSettings = async (data: any) => {\n  const currentSettings = await db.select().from(settings);\n\n  if (currentSettings[0].profilePicture) {\n    try {\n      fs.unlinkSync(currentSettings[0].profilePicture);\n    } catch (error) {\n      console.error(\"Error deleting old profile picture:\", error);\n    }\n  }\n\n  await db.update(settings).set({\n    name: data.name,\n    profilePicture: data.profilePicture,\n  });\n\n  return true;\n};\n\nexport const getSongs = async (page: number = 1, limit: number = 30) => {\n  return await db.query.songs.findMany({\n    with: { album: true },\n    limit: limit,\n    offset: (page - 1) * limit,\n    orderBy: (songs, { asc }) => [asc(songs.name)],\n  });\n};\n\nexport const getAlbums = async (page: number, limit: number = 15) => {\n  // Get albums with pagination\n  const albumsResult = await db\n    .select()\n    .from(albums)\n    .orderBy(albums.name)\n    .limit(limit)\n    .offset((page - 1) * limit);\n\n  // Get durations for these albums\n  const albumsWithDuration = await Promise.all(\n    albumsResult.map(async (album) => {\n      // Get total duration from songs in this album\n      const durationResult = await db\n        .select({ totalDuration: sql`SUM(${songs.duration})` })\n        .from(songs)\n        .where(eq(songs.albumId, album.id));\n\n      return {\n        ...album,\n        duration: durationResult[0]?.totalDuration || 0,\n      };\n    }),\n  );\n\n  return albumsWithDuration;\n};\n\nexport const getPlaylists = async () => {\n  return await db.select().from(playlists);\n};\n\nexport const createPlaylist = async (data: any) => {\n  let description: string;\n  let cover: string;\n\n  if (data.description) {\n    description = data.description;\n  } else {\n    description = \"An epic playlist created by you.\";\n  }\n\n  if (data.cover) {\n    cover = data.cover;\n  } else {\n    cover = null;\n  }\n\n  const playlist = await db.insert(playlists).values({\n    name: data.name,\n    description: description,\n    cover: cover,\n  });\n\n  return playlist;\n};\n\nexport const deletePlaylist = async (data: { id: number }) => {\n  await db.transaction(async (tx) => {\n    // Remove all links in playlistSongs\n    await tx.delete(playlistSongs).where(eq(playlistSongs.playlistId, data.id));\n\n    // Now delete the playlist\n    const result = await tx.delete(playlists).where(eq(playlists.id, data.id));\n    if (\"changes\" in result && result.changes === 0) {\n      throw new Error(`Playlist ${data.id} not found`);\n    }\n  });\n\n  return { message: `Playlist ${data.id} deleted successfully` };\n};\n\nexport const updatePlaylist = async (data: any) => {\n  let description: string;\n  let cover: string;\n\n  if (data.data.description) {\n    description = data.data.description;\n  } else {\n    description = \"An epic playlist created by you.\";\n  }\n\n  if (data.cover) {\n    cover = data.data.cover;\n  }\n\n  const playlist = await db\n    .update(playlists)\n    .set({\n      name: data.data.name,\n      description: description,\n      cover: cover,\n    })\n    .where(eq(playlists.id, data.id));\n\n  return playlist;\n};\n\nexport const getAlbumWithSongs = async (id: number) => {\n  const albumWithSongs = await db.query.albums.findFirst({\n    where: eq(albums.id, id),\n    with: {\n      songs: {\n        with: { album: true },\n      },\n    },\n  });\n\n  if (albumWithSongs) {\n    // Calculate total duration from all songs in this album\n    const totalDuration = albumWithSongs.songs.reduce(\n      (total, song) => total + (song.duration || 0),\n      0,\n    );\n\n    return {\n      ...albumWithSongs,\n      duration: totalDuration,\n    };\n  }\n\n  return albumWithSongs;\n};\n\nexport const getPlaylistWithSongs = async (id: number) => {\n  const playlistWithSongs = await db.query.playlists.findFirst({\n    where: eq(playlists.id, id),\n    with: {\n      songs: {\n        with: {\n          song: {\n            with: { album: true },\n          },\n        },\n      },\n    },\n  });\n\n  return {\n    ...playlistWithSongs,\n    songs: playlistWithSongs.songs.map((playlistSong) => ({\n      ...playlistSong.song,\n      album: playlistSong.song.album,\n    })),\n  };\n};\n\nexport const isSongFavorite = async (file: string) => {\n  const song = await db.query.songs.findFirst({\n    where: eq(songs.filePath, file),\n  });\n\n  if (!song) return false;\n\n  const isFavourite = await db.query.playlistSongs.findFirst({\n    where: and(\n      eq(playlistSongs.playlistId, 1),\n      eq(playlistSongs.songId, song.id),\n    ),\n  });\n\n  return !!isFavourite;\n};\n\nexport const addToFavourites = async (songId: number) => {\n  const existingEntry = await db\n    .select()\n    .from(playlistSongs)\n    .where(\n      and(eq(playlistSongs.playlistId, 1), eq(playlistSongs.songId, songId)),\n    );\n\n  if (!existingEntry[0]) {\n    await db.insert(playlistSongs).values({\n      playlistId: 1,\n      songId,\n    });\n  } else {\n    await db\n      .delete(playlistSongs)\n      .where(\n        and(eq(playlistSongs.playlistId, 1), eq(playlistSongs.songId, songId)),\n      );\n  }\n};\n\nexport const searchDB = async (query: string) => {\n  const lowerSearch = query.toLowerCase();\n\n  const searchAlbums = await db.query.albums.findMany({\n    where: like(albums.name, `%${lowerSearch}%`),\n    limit: 5,\n  });\n\n  const searchPlaylists = await db.query.playlists.findMany({\n    where: like(playlists.name, `%${lowerSearch}%`),\n    limit: 5,\n  });\n\n  const searchSongs = await db.query.songs.findMany({\n    where: like(songs.name, `%${lowerSearch}%`),\n    with: {\n      album: {\n        columns: {\n          id: true,\n          cover: true,\n        },\n      },\n    },\n    limit: 5,\n  });\n\n  // Search for artists by querying unique artist names from the albums table\n  const searchArtists = await db.query.albums.findMany({\n    where: like(albums.artist, `%${lowerSearch}%`),\n    columns: {\n      artist: true,\n    },\n    limit: 5,\n  });\n\n  // Remove duplicate artists by name\n  const uniqueArtists = Array.from(\n    new Set(searchArtists.map((a) => a.artist)),\n  ).map((name) => ({\n    name,\n  }));\n\n  return {\n    searchAlbums,\n    searchPlaylists,\n    searchSongs,\n    searchArtists: uniqueArtists,\n  };\n};\n\nexport const addSongToPlaylist = async (playlistId: number, songId: number) => {\n  const checkIfExists = await db.query.playlistSongs.findFirst({\n    where: and(\n      eq(playlistSongs.playlistId, playlistId),\n      eq(playlistSongs.songId, songId),\n    ),\n  });\n\n  if (checkIfExists) return false;\n\n  await db.insert(playlistSongs).values({\n    playlistId,\n    songId,\n  });\n\n  return true;\n};\n\nexport const removeSongFromPlaylist = async (\n  playlistId: number,\n  songId: number,\n) => {\n  await db\n    .delete(playlistSongs)\n    .where(\n      and(\n        eq(playlistSongs.playlistId, playlistId),\n        eq(playlistSongs.songId, songId),\n      ),\n    );\n\n  return true;\n};\n\nexport const getRandomLibraryItems = async () => {\n  const randomAlbums = await db\n    .select()\n    .from(albums)\n    .orderBy(sql`RANDOM()`)\n    .limit(10);\n\n  // Add duration calculation for albums\n  const albumsWithDuration = await Promise.all(\n    randomAlbums.map(async (album) => {\n      // Get total duration from songs in this album\n      const durationResult = await db\n        .select({ totalDuration: sql`SUM(${songs.duration})` })\n        .from(songs)\n        .where(eq(songs.albumId, album.id));\n\n      return {\n        ...album,\n        duration: durationResult[0]?.totalDuration || 0,\n      };\n    }),\n  );\n\n  const randomSongs = await db.query.songs.findMany({\n    with: { album: true },\n    limit: 10,\n    orderBy: sql`RANDOM()`,\n  });\n\n  return {\n    albums: albumsWithDuration,\n    songs: randomSongs,\n  };\n};\n\n// Added incremental loading support\nexport const initializeData = async (\n  musicFolder: string,\n  incremental = false,\n) => {\n  if (!fs.existsSync(musicFolder)) {\n    console.error(\"Music folder does not exist:\", musicFolder);\n    return false;\n  }\n\n  try {\n    // Add default playlist if it doesn't exist\n    const defaultPlaylist = await db\n      .select()\n      .from(playlists)\n      .where(eq(playlists.id, 1));\n\n    if (!defaultPlaylist[0]) {\n      await db.insert(playlists).values({\n        name: \"Favourites\",\n        cover: null,\n        description: \"Songs liked by you.\",\n      });\n    }\n\n    // Update settings\n    const existingSettings = await db\n      .select()\n      .from(settings)\n      .where(eq(settings.id, 1));\n\n    if (existingSettings[0]) {\n      await db.update(settings).set({ musicFolder }).where(eq(settings.id, 1));\n    } else {\n      await db.insert(settings).values({ musicFolder });\n    }\n\n    // Create art directory if it doesn't exist\n    if (!fs.existsSync(ART_DIR)) {\n      await fs.promises.mkdir(ART_DIR, { recursive: true });\n    }\n\n    // First pass: Just load metadata or do a full scan based on incremental flag\n    await processLibrary(musicFolder, incremental);\n\n    return true;\n  } catch (error) {\n    console.error(\"Error initializing data:\", error);\n    return false;\n  }\n};\n\n// Batch process files to reduce memory usage and improve UI responsiveness\nasync function processLibrary(musicFolder: string, incremental = false) {\n  const startTime = Date.now();\n  const dbFilePaths = await getAllFilePathsFromDb();\n\n  if (incremental) {\n    console.log(\"Starting incremental library scan...\");\n\n    // Scan only the immediate music folder first to reduce initial delay\n    const initialBatch = scanImmediateDirectory(musicFolder);\n    const batchSize = 100; // Increased from 50 for better throughput\n\n    // Process the initial batch right away for quick UI updates\n    await processBatch(initialBatch, dbFilePaths);\n\n    // Process the rest of the library in the background\n    setTimeout(async () => {\n      // Use a more efficient scanning algorithm for the full scan\n      const allFiles = scanEntireLibrary(musicFolder);\n      console.log(`Found ${allFiles.length} files in music library`);\n\n      // Skip files we've already processed in the initial batch\n      for (let i = initialBatch.length; i < allFiles.length; i += batchSize) {\n        const batch = allFiles.slice(i, i + batchSize);\n        await processBatch(batch, dbFilePaths);\n\n        // Yield to UI thread periodically but not too often (increased from 10ms)\n        if (i % (batchSize * 5) === 0) {\n          await new Promise((resolve) => setTimeout(resolve, 30));\n        }\n      }\n\n      // Final cleanup - remove orphaned records\n      await cleanupOrphanedRecords(allFiles);\n\n      console.log(\n        `Library processing completed in ${(Date.now() - startTime) / 1000} seconds`,\n      );\n    }, 1000); // Reduced from 2000ms for faster startup\n  } else {\n    // Do full scan immediately if not incremental\n    const allFiles = scanEntireLibrary(musicFolder);\n    console.log(`Found ${allFiles.length} files in music library`);\n\n    // Process in larger batches since we're not concerned about UI responsiveness\n    const batchSize = 300; // Increased from 200 for better throughput\n\n    for (let i = 0; i < allFiles.length; i += batchSize) {\n      const batch = allFiles.slice(i, i + batchSize);\n      await processBatch(batch, dbFilePaths);\n\n      // Still yield occasionally to prevent potential lockups\n      if (i % (batchSize * 3) === 0) {\n        await new Promise((resolve) => setTimeout(resolve, 20));\n      }\n    }\n\n    await cleanupOrphanedRecords(allFiles);\n    console.log(\n      `Library processing completed in ${(Date.now() - startTime) / 1000} seconds`,\n    );\n  }\n}\n\n// Helper function to get all file paths from database\nasync function getAllFilePathsFromDb(): Promise<Set<string>> {\n  const dbFiles = await db.select().from(songs);\n  return new Set(dbFiles.map((file) => file.filePath));\n}\n\n// Scan only the immediate directory for quick initial loading\nfunction scanImmediateDirectory(dir: string): string[] {\n  let results: string[] = [];\n\n  try {\n    const items = fs.readdirSync(dir);\n\n    // First collect all audio files in the current directory\n    for (const item of items) {\n      const itemPath = path.join(dir, item);\n      try {\n        const stat = fs.statSync(itemPath);\n        if (!stat.isDirectory() && isAudioFile(itemPath)) {\n          results.push(itemPath);\n        }\n      } catch (err) {\n        console.error(`Error accessing ${itemPath}:`, err);\n      }\n    }\n\n    // Then check immediate subdirectories (but not recursively)\n    for (const item of items) {\n      const itemPath = path.join(dir, item);\n      try {\n        const stat = fs.statSync(itemPath);\n        if (stat.isDirectory()) {\n          const subItems = fs.readdirSync(itemPath);\n          for (const subItem of subItems) {\n            const subItemPath = path.join(itemPath, subItem);\n            try {\n              const subStat = fs.statSync(subItemPath);\n              if (!subStat.isDirectory() && isAudioFile(subItemPath)) {\n                results.push(subItemPath);\n              }\n            } catch (err) {\n              console.error(`Error accessing ${subItemPath}:`, err);\n            }\n          }\n        }\n      } catch (err) {\n        console.error(`Error accessing ${itemPath}:`, err);\n      }\n    }\n  } catch (err) {\n    console.error(`Error reading directory ${dir}:`, err);\n  }\n\n  return results;\n}\n\nasync function processBatch(files: string[], dbFilePaths: Set<string>) {\n  const albumCache = new Map();\n\n  for (const file of files) {\n    try {\n      if (!dbFilePaths.has(file)) {\n        // New file - add to database\n        await processAudioFile(file, albumCache);\n      }\n    } catch (error) {\n      console.error(`Error processing file ${file}:`, error);\n    }\n  }\n}\n\nasync function processAudioFile(file: string, albumCache: Map<string, any>) {\n  try {\n    // Use more efficient metadata parsing with stripped options\n    const metadata = await parseFile(file, {\n      skipPostHeaders: true,\n      skipCovers: false, // Still need covers\n      duration: true,\n      includeChapters: false,\n    });\n\n    // Skip files with insufficient metadata\n    if (!metadata.common.title) {\n      return;\n    }\n\n    const albumFolder = path.dirname(file);\n    let artPath = null;\n\n    // Try to find album art in efficient order: embedded first, then folder\n    if (metadata.common.picture && metadata.common.picture.length > 0) {\n      const cover = selectCover(metadata.common.picture);\n      if (cover) {\n        artPath = await processEmbeddedArt(cover);\n      }\n    } else {\n      // Fall back to external images if no embedded art is found\n      const albumImage = findFirstImageInDirectory(albumFolder);\n      if (albumImage) {\n        artPath = await processAlbumArt(albumImage);\n      }\n    }\n\n    // Get or create album with better caching\n    let album;\n    const albumKey = `${metadata.common.album || \"Unknown Album\"}-${metadata.common.artist || \"Unknown Artist\"}`;\n\n    if (albumCache.has(albumKey)) {\n      album = albumCache.get(albumKey);\n    } else {\n      // Optimize the database lookup for album\n      const albumsFound = await db\n        .select()\n        .from(albums)\n        .where(eq(albums.name, metadata.common.album || \"Unknown Album\"));\n\n      if (albumsFound.length > 0) {\n        album = albumsFound[0];\n\n        // Update album if needed (only when data differs)\n        const albumArtist =\n          metadata.common.albumartist ||\n          metadata.common.artist ||\n          \"Various Artists\";\n        if (\n          album.artist !== albumArtist ||\n          album.year !== metadata.common.year ||\n          (artPath && album.cover !== artPath)\n        ) {\n          await db\n            .update(albums)\n            .set({\n              artist: albumArtist,\n              year: metadata.common.year,\n              cover: artPath || album.cover,\n            })\n            .where(eq(albums.id, album.id));\n\n          // Update cached version\n          album.artist = albumArtist;\n          album.year = metadata.common.year;\n          album.cover = artPath || album.cover;\n        }\n      } else {\n        // Create new album with a single transaction\n        const [newAlbum] = await db\n          .insert(albums)\n          .values({\n            name: metadata.common.album || \"Unknown Album\",\n            artist:\n              metadata.common.albumartist ||\n              metadata.common.artist ||\n              \"Various Artists\",\n            year: metadata.common.year,\n            cover: artPath,\n          })\n          .returning();\n\n        album = newAlbum;\n      }\n\n      albumCache.set(albumKey, album);\n    }\n\n    // Add the song using pre-calculated values to avoid repeated operations\n    await db.insert(songs).values({\n      filePath: file,\n      name: metadata.common.title,\n      artist: metadata.common.artist || \"Unknown Artist\",\n      duration: Math.round(metadata.format.duration || 0),\n      albumId: album.id,\n    });\n  } catch (error) {\n    console.error(`Error processing audio file ${file}:`, error);\n  }\n}\n\nasync function processAlbumArt(imagePath: string): Promise<string> {\n  try {\n    // Use a shorter hash method for faster processing\n    const crypto = require(\"crypto\");\n    const imageExt = path.extname(imagePath).slice(1);\n\n    // Generate hash from filename and modified time instead of reading the whole file\n    // This is much faster for large image files\n    const stats = fs.statSync(imagePath);\n    const hashInput = `${imagePath}-${stats.size}-${stats.mtimeMs}`;\n    const hash = crypto.createHash(\"md5\").update(hashInput).digest(\"hex\");\n\n    const artPath = path.join(ART_DIR, `${hash}.${imageExt}`);\n\n    // If the processed file already exists, return its path immediately\n    if (fs.existsSync(artPath)) {\n      return artPath;\n    }\n\n    // Only read the file if we need to process it\n    const imageData = fs.readFileSync(imagePath);\n\n    // For common image formats that don't need processing, just copy the file\n    if (imageExt.match(/^(jpe?g|png|webp)$/i)) {\n      await fs.promises.writeFile(artPath, imageData);\n      return artPath;\n    }\n\n    // For other formats, we might want to convert them (implementation depends on available modules)\n    // For now, just save as is\n    await fs.promises.writeFile(artPath, imageData);\n    return artPath;\n  } catch (error) {\n    console.error(\"Error processing album art:\", error);\n    return null;\n  }\n}\n\nasync function processEmbeddedArt(cover: any): Promise<string> {\n  try {\n    // If we don't have cover data, return early\n    if (!cover || !cover.data) {\n      return null;\n    }\n\n    // Generate a hash based on a small sample of the image data\n    // Using the full data can be slow for large embedded images\n    const sampleSize = Math.min(cover.data.length, 4096); // Sample first 4KB\n    const sampleBuffer = cover.data.slice(0, sampleSize);\n\n    const crypto = require(\"crypto\");\n    const hash = crypto.createHash(\"md5\").update(sampleBuffer).digest(\"hex\");\n\n    const format = cover.format ? cover.format.split(\"/\")[1] || \"jpg\" : \"jpg\";\n\n    const artPath = path.join(ART_DIR, `${hash}.${format}`);\n\n    // Skip writing if it already exists\n    if (fs.existsSync(artPath)) {\n      return artPath;\n    }\n\n    // Write the full image data\n    await fs.promises.writeFile(artPath, cover.data);\n    return artPath;\n  } catch (error) {\n    console.error(\"Error processing embedded art:\", error);\n    return null;\n  }\n}\n\nasync function cleanupOrphanedRecords(currentFiles: string[]) {\n  // Create a set of current file paths for faster lookups\n  const currentFilesSet = new Set(currentFiles);\n\n  // Get all songs from the database\n  const dbFiles = await db.select().from(songs);\n\n  // Find songs that no longer exist\n  const deletedFiles = dbFiles.filter(\n    (dbFile) => !currentFilesSet.has(dbFile.filePath),\n  );\n\n  if (deletedFiles.length > 0) {\n    console.log(`Removing ${deletedFiles.length} orphaned song records`);\n\n    // Delete in batches to avoid locking the database for too long\n    const batchSize = 50;\n    for (let i = 0; i < deletedFiles.length; i += batchSize) {\n      const batch = deletedFiles.slice(i, i + batchSize);\n\n      await db.transaction(async (tx) => {\n        for (const file of batch) {\n          await tx\n            .delete(playlistSongs)\n            .where(eq(playlistSongs.songId, file.id));\n          await tx.delete(songs).where(eq(songs.id, file.id));\n        }\n      });\n    }\n  }\n\n  // Clean up empty albums\n  const allAlbums = await db.select().from(albums);\n\n  for (const album of allAlbums) {\n    const songsInAlbum = await db\n      .select()\n      .from(songs)\n      .where(eq(songs.albumId, album.id));\n\n    if (songsInAlbum.length === 0) {\n      await db.delete(albums).where(eq(albums.id, album.id));\n    }\n  }\n}\n\n// Migrate database to add columns that might be missing\nexport const migrateDatabase = async () => {\n  try {\n    console.log(\"Checking database schema for migrations...\");\n\n    // Check if LastFM columns exist in settings table\n    const tableInfo = sqlite\n      .prepare(\"PRAGMA table_info(settings)\")\n      .all() as Array<{ name: string }>;\n    const columnNames = tableInfo.map((col) => col.name);\n\n    const missingColumns = [];\n\n    // Check for lastFmUsername column\n    if (!columnNames.includes(\"lastFmUsername\")) {\n      missingColumns.push(\"lastFmUsername TEXT\");\n    }\n\n    // Check for lastFmSessionKey column\n    if (!columnNames.includes(\"lastFmSessionKey\")) {\n      missingColumns.push(\"lastFmSessionKey TEXT\");\n    }\n\n    // Check for enableLastFm column\n    if (!columnNames.includes(\"enableLastFm\")) {\n      missingColumns.push(\"enableLastFm INTEGER DEFAULT 0\");\n    }\n\n    // Check for scrobbleThreshold column\n    if (!columnNames.includes(\"scrobbleThreshold\")) {\n      missingColumns.push(\"scrobbleThreshold INTEGER DEFAULT 50\");\n    }\n\n    // Add missing columns if any\n    if (missingColumns.length > 0) {\n      console.log(\n        `Adding ${missingColumns.length} missing columns to settings table...`,\n      );\n\n      for (const columnDef of missingColumns) {\n        const alterSql = `ALTER TABLE settings ADD COLUMN ${columnDef}`;\n        sqlite.exec(alterSql);\n        console.log(`Added column: ${columnDef}`);\n      }\n\n      console.log(\"Database migration completed successfully.\");\n    } else {\n      console.log(\"Database schema is up to date, no migration needed.\");\n    }\n\n    return true;\n  } catch (error) {\n    console.error(\"Error during database migration:\", error);\n    return false;\n  }\n};\n\n// Helper function to send messages to the renderer process\nfunction sendToRenderer(channel: string, data: any) {\n  try {\n    // Check if we have access to the webContents\n    const { BrowserWindow } = require(\"electron\");\n    const win = BrowserWindow.getAllWindows()[0];\n    if (win && win.webContents) {\n      win.webContents.send(channel, data);\n    }\n  } catch (error) {\n    console.error(`Failed to send message to renderer: ${error}`);\n  }\n}\n\nexport const getArtistWithAlbums = async (artist: string) => {\n  try {\n    if (!artist) {\n      console.log(\"Missing artist name in getArtistWithAlbums\");\n      return {\n        name: \"Unknown Artist\",\n        albums: [],\n        albumsWithSongs: [],\n        songs: [],\n        stats: null,\n      };\n    }\n\n    // Get all albums by this artist\n    const artistAlbums = await db\n      .select()\n      .from(albums)\n      .where(eq(albums.artist, artist))\n      .orderBy(albums.year);\n\n    // Get all songs by this artist (across all albums)\n    const artistSongs = await db.query.songs.findMany({\n      where: eq(songs.artist, artist),\n      with: {\n        album: true,\n      },\n      orderBy: (songs, { asc }) => [asc(songs.name)],\n    });\n\n    // Group songs by albums for better organization\n    const albumsWithSongs = await Promise.all(\n      artistAlbums.map(async (album) => {\n        const albumSongs = await db.query.songs.findMany({\n          where: eq(songs.albumId, album.id),\n          with: {\n            album: true,\n          },\n          orderBy: (songs, { asc }) => [asc(songs.name)],\n        });\n\n        return {\n          ...album,\n          songs: albumSongs,\n        };\n      }),\n    );\n\n    // Calculate statistics\n    const totalDuration = artistSongs.reduce(\n      (sum, song) => sum + (song.duration || 0),\n      0,\n    );\n    const genres = new Set<string>();\n    const formats = new Set<string>();\n\n    // Extract genres and formats from songs\n    artistSongs.forEach((song) => {\n      if (song.filePath) {\n        const ext = song.filePath.split(\".\").pop()?.toUpperCase();\n        if (ext) formats.add(ext);\n      }\n    });\n\n    // Get year range\n    const years = artistAlbums.filter((a) => a.year).map((a) => a.year);\n    const yearRange =\n      years.length > 0\n        ? { start: Math.min(...years), end: Math.max(...years) }\n        : null;\n\n    // Get most played song (would need play count tracking, using random for now)\n    const topSongs = artistSongs.slice(0, 5).map((song) => ({\n      id: song.id,\n      name: song.name,\n      duration: song.duration,\n      album: song.album?.name || \"Unknown Album\",\n    }));\n\n    const stats = {\n      totalSongs: artistSongs.length,\n      totalAlbums: artistAlbums.length,\n      totalDuration,\n      genres: Array.from(genres),\n      formats: Array.from(formats),\n      yearRange,\n      topSongs,\n    };\n\n    return {\n      name: artist,\n      albums: artistAlbums,\n      albumsWithSongs: albumsWithSongs,\n      songs: artistSongs,\n      stats,\n    };\n  } catch (error) {\n    console.error(`Error in getArtistWithAlbums for \"${artist}\":`, error);\n    return {\n      name: artist || \"Unknown Artist\",\n      albums: [],\n      albumsWithSongs: [],\n      songs: [],\n      stats: null,\n    };\n  }\n};\n\nexport const getAllArtists = async () => {\n  try {\n    const albumArtists = await db\n      .selectDistinct({ artist: albums.artist })\n      .from(albums)\n      .where(isNotNull(albums.artist));\n\n    const songArtists = await db\n      .selectDistinct({ artist: songs.artist })\n      .from(songs)\n      .where(isNotNull(songs.artist));\n\n    const artistNames = new Set<string>();\n    albumArtists.forEach((a) => {\n      if (a.artist) artistNames.add(a.artist);\n    });\n    songArtists.forEach((s) => {\n      if (s.artist) artistNames.add(s.artist);\n    });\n\n    const artistStats = await db\n      .select({\n        artist: albums.artist,\n        albumCount: sql<number>`COUNT(DISTINCT ${albums.id})`,\n        cover: sql<string>`MAX(${albums.cover})`,\n      })\n      .from(albums)\n      .where(isNotNull(albums.artist))\n      .groupBy(albums.artist);\n\n    const songStats = await db\n      .select({\n        artist: songs.artist,\n        songCount: sql<number>`COUNT(*)`,\n      })\n      .from(songs)\n      .where(isNotNull(songs.artist))\n      .groupBy(songs.artist);\n\n    const songCountMap = new Map<string, number>();\n    songStats.forEach((s) => {\n      if (s.artist) {\n        songCountMap.set(s.artist, Number(s.songCount));\n      }\n    });\n\n    // Combine the data\n    const artistsWithDetails = Array.from(artistNames).map((artistName) => {\n      const albumData = artistStats.find((a) => a.artist === artistName);\n      return {\n        name: artistName,\n        albumCount: albumData ? Number(albumData.albumCount) : 0,\n        songCount: songCountMap.get(artistName) || 0,\n        cover: albumData?.cover || null,\n      };\n    });\n\n    // Sort by artist name\n    return artistsWithDetails.sort((a, b) =>\n      a.name.localeCompare(b.name, undefined, { sensitivity: \"base\" }),\n    );\n  } catch (error) {\n    console.error(\"Error getting all artists:\", error);\n    return [];\n  }\n};\n\nexport const searchSongs = async (query: string) => {\n  if (!query || query.trim() === \"\") {\n    return [];\n  }\n\n  // Normalize the search query\n  const searchTerm = `%${query.toLowerCase().trim()}%`;\n\n  // Efficiently search for songs matching the query across name, artist and album name\n  const searchResults = await db.query.songs.findMany({\n    where: or(\n      like(songs.name, searchTerm),\n      like(songs.artist, searchTerm),\n      // Join with albums to search by album name\n      exists(\n        db\n          .select()\n          .from(albums)\n          .where(\n            and(eq(albums.id, songs.albumId), like(albums.name, searchTerm)),\n          ),\n      ),\n    ),\n    with: {\n      album: true,\n    },\n    // Limit to a reasonable number to avoid performance issues\n    limit: 100,\n    orderBy: (songs, { asc }) => [asc(songs.name)],\n  });\n\n  return searchResults;\n};\n\nexport const getAlbumsWithDuration = async (\n  page: number = 1,\n  limit: number = 15,\n) => {\n  // Get albums with pagination, including a more efficient duration calculation\n  const albumsResult = await db\n    .select()\n    .from(albums)\n    .orderBy(albums.name)\n    .limit(limit)\n    .offset((page - 1) * limit);\n\n  // Get durations for these albums in a single batch query for better performance\n  const albumIds = albumsResult.map((album) => album.id);\n\n  // If no albums were found, return empty array\n  if (albumIds.length === 0) {\n    return [];\n  }\n\n  // Query total durations for all albums in a single database call\n  const durationResults = await db\n    .select({\n      albumId: songs.albumId,\n      totalDuration: sql`SUM(${songs.duration})`,\n    })\n    .from(songs)\n    .where(sql`${songs.albumId} IN (${albumIds.join(\",\")})`)\n    .groupBy(songs.albumId);\n\n  // Create a duration lookup map for efficient access\n  const durationMap = new Map();\n  durationResults.forEach((result) => {\n    durationMap.set(result.albumId, result.totalDuration || 0);\n  });\n\n  // Map the albums with their durations\n  const albumsWithDurations = albumsResult.map((album) => {\n    return {\n      ...album,\n      duration: durationMap.get(album.id) || 0,\n    };\n  });\n\n  return albumsWithDurations;\n};\n\n// Add these functions at the end of the file\n\n// LastFM related functions\nexport const updateLastFmSettings = async (data: {\n  lastFmUsername: string;\n  lastFmSessionKey: string;\n  enableLastFm: boolean;\n  scrobbleThreshold: number;\n}) => {\n  try {\n    const currentSettings = await db.select().from(settings);\n\n    if (currentSettings.length === 0) {\n      // Create new settings if none exist\n      await db.insert(settings).values({\n        lastFmUsername: data.lastFmUsername,\n        lastFmSessionKey: data.lastFmSessionKey,\n        enableLastFm: data.enableLastFm,\n        scrobbleThreshold: data.scrobbleThreshold || 50,\n      });\n    } else {\n      // Update existing settings\n      await db\n        .update(settings)\n        .set({\n          lastFmUsername: data.lastFmUsername,\n          lastFmSessionKey: data.lastFmSessionKey,\n          enableLastFm: data.enableLastFm,\n          scrobbleThreshold: data.scrobbleThreshold || 50,\n        })\n        .where(eq(settings.id, currentSettings[0].id));\n    }\n\n    return true;\n  } catch (error) {\n    console.error(\"Error updating LastFM settings:\", error);\n    return false;\n  }\n};\n\nexport const getLastFmSettings = async () => {\n  try {\n    const settingsRow = await db\n      .select({\n        lastFmUsername: settings.lastFmUsername,\n        lastFmSessionKey: settings.lastFmSessionKey,\n        enableLastFm: settings.enableLastFm,\n        scrobbleThreshold: settings.scrobbleThreshold,\n      })\n      .from(settings)\n      .limit(1);\n\n    if (settingsRow.length === 0) {\n      return {\n        lastFmUsername: null,\n        lastFmSessionKey: null,\n        enableLastFm: false,\n        scrobbleThreshold: 50,\n      };\n    }\n\n    return settingsRow[0];\n  } catch (error) {\n    console.error(\"Error getting LastFM settings:\", error);\n    return {\n      lastFmUsername: null,\n      lastFmSessionKey: null,\n      enableLastFm: false,\n      scrobbleThreshold: 50,\n    };\n  }\n};"
  },
  {
    "path": "main/helpers/db/createDB.ts",
    "content": "import Database from \"better-sqlite3\";\nimport { app } from \"electron\";\nimport path from \"path\";\n\nexport const sqlite = new Database(\n  path.join(app.getPath(\"userData\"), \"wora.db\"),\n);\n\nexport const initDatabase = async () => {\n  sqlite.exec(`\n      CREATE TABLE IF NOT EXISTS settings (\n        id INTEGER PRIMARY KEY,\n        name TEXT,\n        profilePicture TEXT,\n        musicFolder TEXT\n      );\n      CREATE TABLE IF NOT EXISTS albums (\n        id INTEGER PRIMARY KEY,\n        name TEXT,\n        artist TEXT,\n        year INTEGER,\n        cover TEXT\n      );\n      CREATE TABLE IF NOT EXISTS songs (\n        id INTEGER PRIMARY KEY,\n        filePath TEXT,\n        name TEXT,\n        artist TEXT,\n        duration INTEGER,\n        albumId INTEGER,\n        FOREIGN KEY (albumId) REFERENCES albums(id)\n      );\n      CREATE TABLE IF NOT EXISTS playlists (\n        id INTEGER PRIMARY KEY,\n        name TEXT,\n        description TEXT,\n        cover TEXT\n      );\n      CREATE TABLE IF NOT EXISTS playlistSongs (\n        playlistId INTEGER,\n        songId INTEGER,\n        FOREIGN KEY (playlistId) REFERENCES playlists(id),\n        Foreign KEY (songId) REFERENCES songs(id)\n      );\n  `);\n};\n"
  },
  {
    "path": "main/helpers/db/schema.ts",
    "content": "import { integer, sqliteTable, text, blob } from \"drizzle-orm/sqlite-core\";\nimport { relations } from \"drizzle-orm\";\n\nexport const settings = sqliteTable(\"settings\", {\n  id: integer(\"id\", { mode: \"number\" }).primaryKey({ autoIncrement: true }),\n  name: text(\"name\"),\n  profilePicture: text(\"profilePicture\"),\n  musicFolder: text(\"musicFolder\"),\n  lastFmUsername: text(\"lastFmUsername\"),\n  lastFmSessionKey: text(\"lastFmSessionKey\"),\n  enableLastFm: integer(\"enableLastFm\", { mode: \"boolean\" }).default(false),\n  scrobbleThreshold: integer(\"scrobbleThreshold\").default(50),\n});\n\nexport const albums = sqliteTable(\"albums\", {\n  id: integer(\"id\", { mode: \"number\" }).primaryKey({ autoIncrement: true }),\n  name: text(\"name\"),\n  artist: text(\"artist\"),\n  year: integer(\"year\"),\n  cover: text(\"cover\"),\n});\n\nexport const songs = sqliteTable(\"songs\", {\n  id: integer(\"id\", { mode: \"number\" }).primaryKey({ autoIncrement: true }),\n  filePath: text(\"filePath\"),\n  name: text(\"name\"),\n  artist: text(\"artist\"),\n  duration: integer(\"duration\"),\n  albumId: integer(\"albumId\").references(() => albums.id),\n});\n\nexport const albumsRelations = relations(albums, ({ many }) => ({\n  songs: many(songs),\n}));\n\nexport const songsRelations = relations(songs, ({ one }) => ({\n  album: one(albums, {\n    fields: [songs.albumId],\n    references: [albums.id],\n  }),\n}));\n\nexport const playlists = sqliteTable(\"playlists\", {\n  id: integer(\"id\", { mode: \"number\" }).primaryKey({ autoIncrement: true }),\n  name: text(\"name\").notNull().unique(),\n  description: text(\"description\").notNull(),\n  cover: text(\"cover\").notNull(),\n});\n\nexport const playlistSongs = sqliteTable(\"playlistSongs\", {\n  playlistId: integer(\"playlistId\").references(() => playlists.id, {\n    onDelete: \"cascade\",\n  }),\n  songId: integer(\"songId\").references(() => songs.id, {\n    onDelete: \"cascade\",\n  }),\n});\n\nexport const playlistRelations = relations(playlists, ({ many }) => ({\n  songs: many(playlistSongs),\n}));\n\nexport const playlistSongRelations = relations(playlistSongs, ({ one }) => ({\n  playlist: one(playlists, {\n    fields: [playlistSongs.playlistId],\n    references: [playlists.id],\n  }),\n  song: one(songs, { fields: [playlistSongs.songId], references: [songs.id] }),\n}));\n"
  },
  {
    "path": "main/helpers/index.ts",
    "content": "export * from \"./create-window\";\n"
  },
  {
    "path": "main/helpers/lastfm-service.ts",
    "content": "import { ipcMain } from \"electron\";\nimport fetch from \"node-fetch\";\nimport * as crypto from \"crypto\";\nimport * as path from \"path\";\nimport * as fs from \"fs\";\nimport * as electronLog from \"electron-log\";\n\nconst lastFmLogger = electronLog.create({ logId: \"lastfm\" });\nlastFmLogger.transports.file.fileName = \"lastfm.log\";\nlastFmLogger.transports.file.level = \"info\";\n\nconst logLastFm = (\n  message: string,\n  data?: any,\n  level: \"info\" | \"error\" | \"warn\" = \"info\",\n) => {\n  const shouldLogToConsole = process.env.NODE_ENV !== \"production\";\n  switch (level) {\n    case \"error\":\n      lastFmLogger.error(message, data);\n      if (shouldLogToConsole) console.error(`[LastFm] ${message}`, data || \"\");\n      break;\n    case \"warn\":\n      lastFmLogger.warn(message, data);\n      if (shouldLogToConsole) console.warn(`[LastFm] ${message}`, data || \"\");\n      break;\n    default:\n      lastFmLogger.info(message, data);\n      if (shouldLogToConsole) console.log(`[LastFm] ${message}`, data || \"\");\n  }\n};\n\nconst API_URL = \"https://ws.audioscrobbler.com/2.0/\";\n\nconst apiCache = new Map<string, { data: any; timestamp: number }>();\nconst CACHE_TTL = 15 * 60 * 1000;\n\nconst loadEnvVariables = () => {\n  try {\n    const envPath = path.join(process.cwd(), \".env.local\");\n    if (!fs.existsSync(envPath)) return false;\n\n    logLastFm(`Loading environment variables from ${envPath}`);\n    const envContent = fs.readFileSync(envPath, \"utf-8\");\n\n    envContent.split(\"\\n\").forEach((line) => {\n      const match = line.match(/^\\s*([\\w.-]+)\\s*=\\s*(.*)?\\s*$/);\n      if (match) {\n        const key = match[1];\n        let value = match[2] || \"\";\n        if (\n          value.length > 0 &&\n          value.charAt(0) === '\"' &&\n          value.charAt(value.length - 1) === '\"'\n        ) {\n          value = value.replace(/^\"|\"$/g, \"\");\n        }\n        process.env[key] = value;\n      }\n    });\n    return true;\n  } catch (error) {\n    logLastFm(\"Error loading environment variables\", error, \"error\");\n    return false;\n  }\n};\n\nif (process.env.NODE_ENV !== \"production\") {\n  loadEnvVariables();\n}\n\nconst DEV_API_KEY = process.env.LASTFM_API_KEY || \"\";\nconst DEV_API_SECRET = process.env.LASTFM_API_SECRET || \"\";\n\nif (process.env.NODE_ENV !== \"production\") {\n  if (!DEV_API_KEY || !DEV_API_SECRET) {\n    logLastFm(\n      \"WARNING: Last.fm API credentials not found in environment variables\",\n      null,\n      \"warn\",\n    );\n  }\n}\n\nconst useBackend = process.env.NODE_ENV === \"production\" || \n                   process.env.USE_BACKEND === \"true\" ||\n                   (!DEV_API_KEY || !DEV_API_SECRET);\n\n// Get the backend URL based on environment\nconst getBackendUrl = (): string => {\n  return process.env.NODE_ENV === \"production\"\n    ? \"https://wora-ten.vercel.app\"\n    : \"http://localhost:3000\";\n};\n\n/**\n * Forward Last.fm requests to the Vercel backend\n */\nconst forwardToBackend = async (\n  endpoint: string,\n  method: string = \"GET\",\n  body?: any,\n) => {\n  try {\n    const baseUrl = getBackendUrl();\n    const url = `${baseUrl}/api/lastfm/${endpoint}`;\n\n    const options: any = {\n      method,\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n    };\n\n    if (body && method === \"POST\") {\n      options.body = JSON.stringify(body);\n    }\n\n    const response = await fetch(url, options);\n    return await response.json();\n  } catch (error) {\n    logLastFm(\"Error forwarding request to backend\", error, \"error\");\n    return {\n      success: false,\n      error: \"Failed to communicate with the backend API\",\n    };\n  }\n};\n\n/**\n * Generate a signature for Last.fm API\n */\nconst generateSignature = (params: Record<string, string>): string => {\n  // Remove format and callback parameters\n  const filteredParams = { ...params };\n  delete filteredParams.format;\n  delete filteredParams.callback;\n\n  // Sort parameters alphabetically by name\n  const sortedKeys = Object.keys(filteredParams).sort();\n\n  // Concatenate parameters\n  let signatureStr = \"\";\n  for (const key of sortedKeys) {\n    signatureStr += key + filteredParams[key];\n  }\n\n  // Append secret\n  signatureStr += DEV_API_SECRET;\n\n  // Create MD5 hash\n  return crypto.createHash(\"md5\").update(signatureStr).digest(\"hex\");\n};\n\n/**\n * Make a direct request to Last.fm API\n */\nconst makeLastFmRequest = async (\n  params: Record<string, string>,\n  isAuthRequest: boolean = false,\n): Promise<any> => {\n  try {\n    // Always add these parameters\n    const requestParams: Record<string, string> = {\n      ...params,\n      api_key: DEV_API_KEY,\n      format: \"json\",\n    };\n\n    // If this is an authenticated request, add signature\n    if (isAuthRequest) {\n      requestParams.api_sig = generateSignature(requestParams);\n    }\n\n    // Build query string\n    const queryString = Object.entries(requestParams)\n      .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)\n      .join(\"&\");\n\n    // Make the request\n    const url = `${API_URL}?${queryString}`;\n\n    const response = await fetch(url, {\n      method: \"POST\",\n    });\n\n    const data = await response.json();\n\n    // Check for errors\n    if (data.error) {\n      logLastFm(`API Error ${data.error}: ${data.message}`, null, \"error\");\n      return {\n        success: false,\n        error: data.message,\n        code: data.error,\n      };\n    }\n\n    return data;\n  } catch (error) {\n    logLastFm(\"Error making Last.fm API request\", error, \"error\");\n    return {\n      success: false,\n      error: \"Error making Last.fm API request\",\n    };\n  }\n};\n\n/**\n * Generate MD5 hash for password authentication\n */\nconst getMD5Auth = (username: string, password: string): string => {\n  const authString = username.toLowerCase() + password;\n  return crypto.createHash(\"md5\").update(authString).digest(\"hex\");\n};\n\n/**\n * Initialize Last.fm IPC handlers\n */\nexport const initializeLastFmHandlers = () => {\n  // Handler for log messages from renderer process - simplified to avoid duplicate logging\n  ipcMain.on(\"lastfm:log\", (_, data) => {\n    const { level, message } = data;\n    if (!level || !message) return;\n\n    // Just pass to the right logger method\n    switch (level) {\n      case \"error\":\n        lastFmLogger.error(message);\n        break;\n      case \"warn\":\n        lastFmLogger.warn(message);\n        break;\n      default:\n        lastFmLogger.info(message);\n    }\n  });\n\n  // Handle authentication requests\n  ipcMain.handle(\n    \"lastfm:authenticate\",\n    async (_, username: string, password: string) => {\n      try {\n        // In production, use the backend API\n        if (useBackend) {\n          const response = await forwardToBackend(\"auth\", \"POST\", {\n            username,\n            password,\n          });\n\n          if (!response.success) {\n            logLastFm(\"Authentication error\", response.error, \"error\");\n          }\n          return response;\n        }\n        // In development, call Last.fm API directly\n        else {\n          // Use the mobile session API for desktop auth\n          const params = {\n            method: \"auth.getMobileSession\",\n            username: username,\n            password: password,\n          };\n\n          const response = await makeLastFmRequest(params, true);\n\n          if (response.error) {\n            return {\n              success: false,\n              error: response.error,\n            };\n          }\n\n          // Return success with session\n          return {\n            success: true,\n            session: response.session,\n          };\n        }\n      } catch (error) {\n        logLastFm(\"Error in authentication\", error, \"error\");\n        return {\n          success: false,\n          error: \"Internal error during authentication\",\n        };\n      }\n    },\n  );\n\n  // Handle \"now playing\" updates\n  ipcMain.handle(\"lastfm:updateNowPlaying\", async (_, data) => {\n    try {\n      const { sessionKey, artist, track, album, duration } = data;\n\n      if (!sessionKey || !artist || !track) {\n        return { success: false, error: \"Missing required parameters\" };\n      }\n\n      // Use backend or direct API based on environment\n      if (useBackend) {\n        const response = await forwardToBackend(\"now-playing\", \"POST\", data);\n        if (!response.success) {\n          logLastFm(\"Error updating now playing\", response.error, \"error\");\n        }\n        return response;\n      } else {\n        // Call Last.fm API directly\n        const params: Record<string, string> = {\n          method: \"track.updateNowPlaying\",\n          artist,\n          track,\n          sk: sessionKey,\n        };\n\n        // Add optional parameters if available\n        if (album) params.album = album;\n        if (duration) params.duration = duration;\n\n        const response = await makeLastFmRequest(params, true);\n\n        if (response.error) {\n          return {\n            success: false,\n            error: response.message || \"Failed to update now playing\",\n          };\n        }\n\n        return {\n          success: true,\n        };\n      }\n    } catch (error) {\n      logLastFm(\"Error in updateNowPlaying\", error, \"error\");\n      return {\n        success: false,\n        error: \"Internal error updating now playing status\",\n      };\n    }\n  });\n\n  // Handle track scrobbling - simplified error handling\n  ipcMain.handle(\"lastfm:scrobbleTrack\", async (_, data) => {\n    try {\n      const { sessionKey, artist, track, album, timestamp, duration } = data;\n\n      if (!sessionKey || !artist || !track) {\n        return { success: false, error: \"Missing required parameters\" };\n      }\n\n      // Use backend or direct API based on environment\n      if (useBackend) {\n        const response = await forwardToBackend(\"scrobble\", \"POST\", data);\n        if (!response.success) {\n          logLastFm(\"Error scrobbling track\", response.error, \"error\");\n        }\n        return response;\n      } else {\n        // Call Last.fm API directly\n        const params: Record<string, string> = {\n          method: \"track.scrobble\",\n          artist,\n          track,\n          timestamp: timestamp || Math.floor(Date.now() / 1000).toString(),\n          sk: sessionKey,\n        };\n\n        // Add optional parameters if available\n        if (album) params.album = album;\n        if (duration) params.duration = duration;\n\n        const response = await makeLastFmRequest(params, true);\n\n        if (response.error) {\n          return {\n            success: false,\n            error: response.message || \"Failed to scrobble track\",\n          };\n        }\n\n        return {\n          success: true,\n        };\n      }\n    } catch (error) {\n      logLastFm(\"Error in scrobbleTrack\", error, \"error\");\n      return { success: false, error: \"Internal error scrobbling track\" };\n    }\n  });\n\n  // Handle get user info - simplified\n  ipcMain.handle(\"lastfm:getUserInfo\", async (_, username, sessionKey) => {\n    try {\n      if (!username) {\n        return { success: false, error: \"Username is required\" };\n      }\n\n      // Use backend or direct API based on environment\n      if (useBackend) {\n        const response = await forwardToBackend(\n          `user-info?username=${encodeURIComponent(username)}&sessionKey=${encodeURIComponent(sessionKey || \"\")}`,\n        );\n\n        if (!response.success) {\n          logLastFm(\"Error getting user info\", response.error, \"error\");\n        }\n        return response;\n      } else {\n        // Call Last.fm API directly\n        const params: Record<string, string> = {\n          method: \"user.getInfo\",\n          user: username,\n        };\n\n        // Add session key if available for private data\n        if (sessionKey) params.sk = sessionKey;\n\n        const response = await makeLastFmRequest(params, !!sessionKey);\n\n        if (response.error) {\n          return {\n            success: false,\n            error: response.message || \"Failed to get user info\",\n          };\n        }\n\n        return {\n          success: true,\n          user: response.user,\n        };\n      }\n    } catch (error) {\n      logLastFm(\"Error in getUserInfo\", error, \"error\");\n      return { success: false, error: \"Internal error getting user info\" };\n    }\n  });\n\n  // Handle get track info - simplified\n  ipcMain.handle(\"lastfm:getTrackInfo\", async (_, artist, track, username) => {\n    try {\n      if (!artist || !track) {\n        return { success: false, error: \"Artist and track are required\" };\n      }\n\n      // Use backend or direct API based on environment\n      if (useBackend) {\n        // Create query string\n        let query = `artist=${encodeURIComponent(artist)}&track=${encodeURIComponent(track)}`;\n        if (username) {\n          query += `&username=${encodeURIComponent(username)}`;\n        }\n\n        // Forward track info request to backend\n        const response = await forwardToBackend(`track-info?${query}`);\n        if (!response.success) {\n          logLastFm(\"Error getting track info\", response.error, \"error\");\n        }\n        return response;\n      } else {\n        // Call Last.fm API directly\n        const params: Record<string, string> = {\n          method: \"track.getInfo\",\n          artist,\n          track,\n        };\n\n        // Add username if available for loved status\n        if (username) params.username = username;\n\n        const response = await makeLastFmRequest(params, false);\n\n        if (response.error) {\n          return {\n            success: false,\n            error: response.message || \"Failed to get track info\",\n          };\n        }\n\n        return {\n          success: true,\n          track: response.track,\n        };\n      }\n    } catch (error) {\n      logLastFm(\"Error in getTrackInfo\", error, \"error\");\n      return { success: false, error: \"Internal error getting track info\" };\n    }\n  });\n\n  // Handle get artist info\n  ipcMain.handle(\"lastfm:getArtistInfo\", async (_, artist) => {\n    try {\n      if (!artist) {\n        return { success: false, error: \"Artist name is required\" };\n      }\n\n      // Use backend or direct API based on environment\n      if (useBackend) {\n        const query = `artist=${encodeURIComponent(artist)}`;\n        const response = await forwardToBackend(`artist-info?${query}`);\n        if (!response.success) {\n          logLastFm(\"Error getting artist info\", response.error, \"error\");\n        }\n        return response;\n      } else {\n        const params: Record<string, string> = {\n          method: \"artist.getInfo\",\n          artist,\n          autocorrect: \"1\",\n        };\n\n        const response = await makeLastFmRequest(params, false);\n\n        if (response.error) {\n          return {\n            success: false,\n            error: response.message || \"Failed to get artist info\",\n          };\n        }\n\n        return {\n          success: true,\n          artist: response.artist,\n        };\n      }\n    } catch (error) {\n      logLastFm(\"Error in getArtistInfo\", error, \"error\");\n      return { success: false, error: \"Internal error getting artist info\" };\n    }\n  });\n\n  // Handle get artist top tracks\n  ipcMain.handle(\"lastfm:getArtistTopTracks\", async (_, artist) => {\n    try {\n      if (!artist) {\n        return { success: false, error: \"Artist name is required\" };\n      }\n\n      // Use backend or direct API based on environment\n      if (useBackend) {\n        const query = `artist=${encodeURIComponent(artist)}`;\n        const response = await forwardToBackend(`artist-top-tracks?${query}`);\n        if (!response.success) {\n          logLastFm(\"Error getting artist top tracks\", response.error, \"error\");\n        }\n        return response;\n      } else {\n        const params: Record<string, string> = {\n          method: \"artist.getTopTracks\",\n          artist,\n          limit: \"10\",\n          autocorrect: \"1\",\n        };\n\n        const response = await makeLastFmRequest(params, false);\n\n        if (response.error) {\n          return {\n            success: false,\n            error: response.message || \"Failed to get top tracks\",\n          };\n        }\n\n        return {\n          success: true,\n          toptracks: response.toptracks,\n        };\n      }\n    } catch (error) {\n      logLastFm(\"Error in getArtistTopTracks\", error, \"error\");\n      return { success: false, error: \"Internal error getting top tracks\" };\n    }\n  });\n\n  // Handle get similar artists\n  ipcMain.handle(\"lastfm:getSimilarArtists\", async (_, artist) => {\n    try {\n      if (!artist) {\n        return { success: false, error: \"Artist name is required\" };\n      }\n\n      // Use backend or direct API based on environment\n      if (useBackend) {\n        const query = `artist=${encodeURIComponent(artist)}`;\n        const response = await forwardToBackend(`similar-artists?${query}`);\n        if (!response.success) {\n          logLastFm(\"Error getting similar artists\", response.error, \"error\");\n        }\n        return response;\n      } else {\n        const params: Record<string, string> = {\n          method: \"artist.getSimilar\",\n          artist,\n          limit: \"6\",\n          autocorrect: \"1\",\n        };\n\n        const response = await makeLastFmRequest(params, false);\n\n        if (response.error) {\n          return {\n            success: false,\n            error: response.message || \"Failed to get similar artists\",\n          };\n        }\n\n        return {\n          success: true,\n          similarartists: response.similarartists,\n        };\n      }\n    } catch (error) {\n      logLastFm(\"Error in getSimilarArtists\", error, \"error\");\n      return { success: false, error: \"Internal error getting similar artists\" };\n    }\n  });\n\n  // Handle love/unlove track - simplified\n  ipcMain.handle(\"lastfm:loveTrack\", async (_, data) => {\n    try {\n      const { sessionKey, artist, track, love } = data;\n\n      if (!sessionKey || !artist || !track || love === undefined) {\n        return { success: false, error: \"Missing required parameters\" };\n      }\n\n      const action = love ? \"love\" : \"unlove\";\n\n      // Use backend or direct API based on environment\n      if (useBackend) {\n        const response = await forwardToBackend(\"track-action\", \"POST\", {\n          sessionKey,\n          artist,\n          track,\n          action,\n        });\n\n        if (!response.success) {\n          logLastFm(`Error ${action} track`, response.error, \"error\");\n        }\n        return response;\n      } else {\n        // Call Last.fm API directly\n        const params: Record<string, string> = {\n          method: `track.${action}`,\n          artist,\n          track,\n          sk: sessionKey,\n        };\n\n        const response = await makeLastFmRequest(params, true);\n\n        if (response.error) {\n          return {\n            success: false,\n            error: response.message || `Failed to ${action} track`,\n          };\n        }\n\n        return {\n          success: true,\n        };\n      }\n    } catch (error) {\n      logLastFm(\"Error in loveTrack\", error, \"error\");\n      return {\n        success: false,\n        error: `Internal error processing track love/unlove`,\n      };\n    }\n  });\n\n  // Log initialization only in development\n  if (process.env.NODE_ENV !== \"production\") {\n    logLastFm(\n      `Using ${useBackend ? \"backend API\" : \"direct API calls\"} for Last.fm`,\n    );\n  }\n};\n"
  },
  {
    "path": "main/preload.ts",
    "content": "import { contextBridge, ipcRenderer, IpcRendererEvent } from \"electron\";\n\nconst handler = {\n  send(channel: string, value: unknown) {\n    ipcRenderer.send(channel, value);\n  },\n  on(channel: string, callback: (...args: unknown[]) => void) {\n    const subscription = (_event: IpcRendererEvent, ...args: unknown[]) =>\n      callback(...args);\n    ipcRenderer.on(channel, subscription);\n\n    return () => {\n      ipcRenderer.removeListener(channel, subscription);\n    };\n  },\n  async invoke(channel: string, ...args: unknown[]) {\n    try {\n      const result = await ipcRenderer.invoke(channel, ...args);\n      return result;\n    } catch (error) {\n      console.error(`Error invoking channel ${channel}:`, error);\n      throw error;\n    }\n  },\n};\n\ncontextBridge.exposeInMainWorld(\"ipc\", handler);\n\nexport type IpcHandler = typeof handler;\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"private\": true,\n  \"name\": \"wora\",\n  \"description\": \"🎧 A beautiful player for audiophiles.\",\n  \"version\": \"0.4.0-beta2\",\n  \"author\": {\n    \"name\": \"Aaryan Kapoor\",\n    \"email\": \"hi.aaryankapoor@gmail.com\"\n  },\n  \"main\": \"app/background.js\",\n  \"scripts\": {\n    \"dev\": \"nextron\",\n    \"build\": \"nextron build\",\n    \"postinstall\": \"electron-builder install-app-deps\",\n    \"build:mac\": \"nextron build --mac --universal\",\n    \"build:linux\": \"nextron build --linux\",\n    \"build:win64\": \"nextron build --win --x64\"\n  },\n  \"dependencies\": {\n    \"@hookform/resolvers\": \"^5.1.1\",\n    \"@radix-ui/react-avatar\": \"^1.1.10\",\n    \"@radix-ui/react-context-menu\": \"^2.2.15\",\n    \"@radix-ui/react-dialog\": \"^1.1.14\",\n    \"@radix-ui/react-label\": \"^2.1.7\",\n    \"@radix-ui/react-progress\": \"^1.1.7\",\n    \"@radix-ui/react-scroll-area\": \"^1.2.9\",\n    \"@radix-ui/react-select\": \"^2.2.5\",\n    \"@radix-ui/react-slider\": \"^1.3.5\",\n    \"@radix-ui/react-slot\": \"^1.2.3\",\n    \"@radix-ui/react-switch\": \"^1.2.5\",\n    \"@radix-ui/react-tabs\": \"^1.1.12\",\n    \"@radix-ui/react-tooltip\": \"^1.2.7\",\n    \"@tabler/icons-react\": \"^3.34.0\",\n    \"@tailwindcss/postcss\": \"^4.1.10\",\n    \"@types/better-sqlite3\": \"^7.6.13\",\n    \"@types/crypto-js\": \"^4.2.2\",\n    \"@xhayper/discord-rpc\": \"^1.2.2\",\n    \"axios\": \"^1.10.0\",\n    \"better-sqlite3\": \"^12.0.0\",\n    \"class-variance-authority\": \"^0.7.1\",\n    \"clsx\": \"^2.1.1\",\n    \"cmdk\": \"^1.1.1\",\n    \"drizzle-orm\": \"^0.44.2\",\n    \"electron-log\": \"^5.4.1\",\n    \"electron-serve\": \"^1.3.0\",\n    \"electron-store\": \"^8.2.0\",\n    \"embla-carousel-react\": \"^8.6.0\",\n    \"eslint-config-next\": \"^15.3.4\",\n    \"framer-motion\": \"^12.23.12\",\n    \"howler\": \"^2.2.4\",\n    \"last-fm\": \"^5.3.0\",\n    \"music-metadata\": \"^7.14.0\",\n    \"next-themes\": \"^0.4.6\",\n    \"node-fetch\": \"2\",\n    \"react-hook-form\": \"^7.58.1\",\n    \"react-virtualized-auto-sizer\": \"^1.0.26\",\n    \"react-window\": \"^1.8.11\",\n    \"seamless-scroll-polyfill\": \"^2.3.4\",\n    \"sonner\": \"^2.0.5\",\n    \"tailwind-merge\": \"^3.3.1\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"zod\": \"^3.25.67\"\n  },\n  \"devDependencies\": {\n    \"@electron/rebuild\": \"^4.0.1\",\n    \"@types/howler\": \"^2.2.12\",\n    \"@types/node\": \"^24.0.3\",\n    \"@types/react\": \"^19.1.8\",\n    \"autoprefixer\": \"^10.4.21\",\n    \"drizzle-kit\": \"^0.31.2\",\n    \"electron\": \"^32.2.7\",\n    \"electron-builder\": \"^24.13.3\",\n    \"next\": \"^15.3.4\",\n    \"nextron\": \"^9.5.0\",\n    \"postcss\": \"^8.4.49\",\n    \"prettier\": \"3.6.0\",\n    \"prettier-plugin-tailwindcss\": \"^0.6.13\",\n    \"react\": \"^19.1.0\",\n    \"react-dom\": \"^19.1.0\",\n    \"rebuild\": \"^0.1.2\",\n    \"tailwindcss\": \"^4.1.10\",\n    \"typescript\": \"^5.8.3\"\n  },\n  \"packageManager\": \"yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e\"\n}\n"
  },
  {
    "path": "renderer/components/ErrorBoundary.tsx",
    "content": "import React, { Component, ErrorInfo, ReactNode } from 'react';\nimport { Button } from '@/components/ui/button';\nimport { IconAlertTriangle, IconRefresh } from '@tabler/icons-react';\n\ninterface Props {\n  children: ReactNode;\n  fallback?: ReactNode;\n}\n\ninterface State {\n  hasError: boolean;\n  error: Error | null;\n}\n\nexport default class ErrorBoundary extends Component<Props, State> {\n  public state: State = {\n    hasError: false,\n    error: null,\n  };\n\n  public static getDerivedStateFromError(error: Error): State {\n    return { hasError: true, error };\n  }\n\n  public componentDidCatch(error: Error, errorInfo: ErrorInfo) {\n    console.error('Uncaught error:', error, errorInfo);\n  }\n\n  private handleReset = () => {\n    this.setState({ hasError: false, error: null });\n    window.location.reload();\n  };\n\n  public render() {\n    if (this.state.hasError) {\n      if (this.props.fallback) {\n        return this.props.fallback;\n      }\n\n      return (\n        <div className=\"flex h-full w-full flex-col items-center justify-center p-8\">\n          <div className=\"max-w-md text-center\">\n            <IconAlertTriangle size={48} className=\"mx-auto mb-4 text-red-500\" />\n            <h2 className=\"mb-2 text-xl font-semibold\">Something went wrong</h2>\n            <p className=\"mb-4 text-sm opacity-70\">\n              An unexpected error occurred. The application may not work correctly.\n            </p>\n            {process.env.NODE_ENV !== 'production' && this.state.error && (\n              <details className=\"mb-4 rounded-lg bg-gray-100 p-3 text-left dark:bg-gray-800\">\n                <summary className=\"cursor-pointer text-xs font-medium\">Error details</summary>\n                <pre className=\"mt-2 overflow-auto text-xs\">\n                  {this.state.error.toString()}\n                  {this.state.error.stack}\n                </pre>\n              </details>\n            )}\n            <Button\n              onClick={this.handleReset}\n              className=\"flex items-center gap-2\"\n            >\n              <IconRefresh size={16} />\n              Reload Application\n            </Button>\n          </div>\n        </div>\n      );\n    }\n\n    return this.props.children;\n  }\n}"
  },
  {
    "path": "renderer/components/LoadingSkeletons.tsx",
    "content": "import React from 'react';\nimport { Skeleton } from '@/components/ui/skeleton';\n\nexport function ArtistGridSkeleton({ count = 12, viewMode = 'grid-large' }: { count?: number; viewMode?: string }) {\n  const isLarge = viewMode === 'grid-large';\n  const gridClass = isLarge \n    ? \"grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6\"\n    : viewMode === 'grid-small'\n    ? \"grid grid-cols-4 gap-3 sm:grid-cols-5 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10\"\n    : \"space-y-1\";\n\n  if (viewMode === 'list') {\n    return (\n      <div className={gridClass}>\n        {Array.from({ length: count }).map((_, i) => (\n          <div key={i} className=\"flex items-center gap-4 p-3\">\n            <Skeleton className=\"h-12 w-12 rounded-lg\" />\n            <div className=\"flex-1\">\n              <Skeleton className=\"h-4 w-32 mb-2\" />\n              <Skeleton className=\"h-3 w-24\" />\n            </div>\n          </div>\n        ))}\n      </div>\n    );\n  }\n\n  return (\n    <div className={gridClass}>\n      {Array.from({ length: count }).map((_, i) => (\n        <div key={i}>\n          <Skeleton className={`aspect-square ${isLarge ? 'rounded-xl' : 'rounded-lg'}`} />\n          <Skeleton className={`h-4 w-3/4 ${isLarge ? 'mt-3' : 'mt-2'} mb-1`} />\n          <Skeleton className=\"h-3 w-1/2\" />\n        </div>\n      ))}\n    </div>\n  );\n}\n\nexport function AlbumGridSkeleton({ count = 12 }: { count?: number }) {\n  return (\n    <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6\">\n      {Array.from({ length: count }).map((_, i) => (\n        <div key={i}>\n          <Skeleton className=\"aspect-square rounded-lg\" />\n          <Skeleton className=\"h-4 w-3/4 mt-2 mb-1\" />\n          <Skeleton className=\"h-3 w-1/2\" />\n        </div>\n      ))}\n    </div>\n  );\n}\n\nexport function SongListSkeleton({ count = 10 }: { count?: number }) {\n  return (\n    <div className=\"space-y-1\">\n      {Array.from({ length: count }).map((_, i) => (\n        <div key={i} className=\"flex items-center gap-4 p-3\">\n          <Skeleton className=\"h-12 w-12 rounded\" />\n          <div className=\"flex-1\">\n            <Skeleton className=\"h-4 w-48 mb-2\" />\n            <Skeleton className=\"h-3 w-32\" />\n          </div>\n          <Skeleton className=\"h-4 w-12\" />\n        </div>\n      ))}\n    </div>\n  );\n}\n\nexport function ArtistDetailSkeleton() {\n  return (\n    <div>\n      <div className=\"relative h-96 w-full overflow-hidden rounded-2xl\">\n        <Skeleton className=\"h-full w-full\" />\n        <div className=\"absolute bottom-6 left-6\">\n          <div className=\"flex items-end gap-6\">\n            <Skeleton className=\"h-52 w-52 rounded-xl\" />\n            <div>\n              <Skeleton className=\"h-12 w-64 mb-2\" />\n              <Skeleton className=\"h-4 w-48\" />\n            </div>\n          </div>\n        </div>\n      </div>\n      <div className=\"mt-8 space-y-4\">\n        <Skeleton className=\"h-32 w-full rounded-lg\" />\n        <div className=\"grid grid-cols-2 gap-4 md:grid-cols-4\">\n          <Skeleton className=\"h-24 rounded-lg\" />\n          <Skeleton className=\"h-24 rounded-lg\" />\n          <Skeleton className=\"h-24 rounded-lg\" />\n          <Skeleton className=\"h-24 rounded-lg\" />\n        </div>\n      </div>\n    </div>\n  );\n}"
  },
  {
    "path": "renderer/components/PageTransition.tsx",
    "content": "import { motion, AnimatePresence, Transition } from 'framer-motion';\nimport { useRouter } from 'next/router';\nimport { ReactNode } from 'react';\n\ninterface PageTransitionProps {\n  children: ReactNode;\n}\n\n// Option 1: Cross-fade (no wait, instant transition)\nconst pageVariants = {\n  initial: {\n    opacity: 0,\n  },\n  in: {\n    opacity: 1,\n  },\n  out: {\n    opacity: 0,\n  },\n};\n\nconst pageTransition: Transition = {\n  type: 'tween',\n  ease: 'easeOut',\n  duration: 0.1, // Very fast\n};\n\nexport default function PageTransition({ children }: PageTransitionProps) {\n  const router = useRouter();\n\n  return (\n    <AnimatePresence mode=\"sync\" initial={false}>  {/* sync = crossfade, wait = sequential */}\n      <motion.div\n        key={router.asPath}\n        initial=\"initial\"\n        animate=\"in\"\n        exit=\"out\"\n        variants={pageVariants}\n        transition={pageTransition}\n        style={{ height: '100%' }}\n      >\n        {children}\n      </motion.div>\n    </AnimatePresence>\n  );\n}"
  },
  {
    "path": "renderer/components/PageTransitionMinimal.tsx",
    "content": "import { ReactNode } from 'react';\n\ninterface PageTransitionProps {\n  children: ReactNode;\n}\n\n// Minimal approach: No transition, just instant page swap\n// This is actually what many modern apps do (Spotify, Apple Music)\n// The smooth scroll restoration gives enough visual continuity\nexport default function PageTransitionMinimal({ children }: PageTransitionProps) {\n  return <>{children}</>;\n}"
  },
  {
    "path": "renderer/components/main/lyrics.tsx",
    "content": "import { LyricLine } from \"@/lib/helpers\";\nimport React, { useEffect, useRef } from \"react\";\nimport { Badge } from \"../ui/badge\";\nimport { scrollIntoView } from \"seamless-scroll-polyfill\";\nimport { cn } from \"@/lib/utils\";\n\ninterface LyricsProps {\n  lyrics: LyricLine[];\n  currentLyric: LyricLine | null;\n  onLyricClick: (time: number) => void;\n  isSyncedLyrics: boolean;\n}\n\nconst Lyrics: React.FC<LyricsProps> = React.memo(\n  ({ lyrics, currentLyric, onLyricClick, isSyncedLyrics }) => {\n    const lyricsRef = useRef<HTMLDivElement>(null);\n\n    useEffect(() => {\n      if (currentLyric && lyricsRef.current) {\n        const currentLine = document.getElementById(\n          `line-${currentLyric.time}`,\n        );\n        if (currentLine) {\n          scrollIntoView(\n            currentLine,\n            {\n              behavior: \"smooth\",\n              block: \"center\",\n            },\n            {\n              duration: 500,\n            },\n          );\n        }\n      }\n    }, [currentLyric]);\n\n    return (\n      <div className=\"wora-border relative h-full w-full rounded-2xl bg-white/70 backdrop-blur-xl dark:bg-black/70\">\n        <div className=\"absolute right-6 bottom-5 z-50 flex items-center gap-2\">\n          <Badge>{isSyncedLyrics ? \"Synced\" : \"Unsynced\"}</Badge>\n        </div>\n\n        <div className=\"h-utility mask flex w-full items-center overflow-y-auto mask-y-from-70% px-8 text-2xl font-medium text-balance\">\n          <div\n            ref={lyricsRef}\n            className=\"no-scrollbar h-full w-full py-[33vh]\"\n            style={{ overflowY: \"auto\" }}\n          >\n            {lyrics.map((line) => (\n              <p\n                key={line.time}\n                id={`line-${line.time}`}\n                className={cn(\n                  currentLyric?.time === line.time\n                    ? \"scale-125 font-semibold\"\n                    : \"opacity-40\",\n                  \"my-2 max-w-xl origin-left transform-gpu cursor-pointer rounded-xl p-4 lowercase transition-transform duration-700 hover:bg-black/5 dark:hover:bg-white/10\",\n                )}\n                onClick={() => onLyricClick(line.time)}\n              >\n                {line.text}\n              </p>\n            ))}\n          </div>\n        </div>\n      </div>\n    );\n  },\n);\n\nexport default Lyrics;\n"
  },
  {
    "path": "renderer/components/main/navbar.tsx",
    "content": "import {\n  IconDeviceDesktop,\n  IconFocusCentered,\n  IconInbox,\n  IconList,\n  IconMoon,\n  IconSearch,\n  IconSun,\n  IconVinyl,\n  IconUser,\n  IconArrowLeft,\n} from \"@tabler/icons-react\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { Avatar, AvatarImage } from \"@/components/ui/avatar\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Command,\n  CommandDialog,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/components/ui/command\";\nimport { useEffect, useState, useCallback } from \"react\";\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport { useRouter } from \"next/router\";\nimport { usePlayer } from \"@/context/playerContext\";\nimport Spinner from \"@/components/ui/spinner\";\nimport { useTheme } from \"next-themes\";\n\ntype Settings = {\n  name: string;\n  profilePicture: string;\n};\n\ntype NavLink = {\n  href: string;\n  icon: React.ReactNode;\n  label: string;\n};\n\nconst Navbar = () => {\n  const router = useRouter();\n  const [open, setOpen] = useState(false);\n  const [searchResults, setSearchResults] = useState([]);\n  const [settings, setSettings] = useState<Settings | null>(null);\n  const [search, setSearch] = useState(\"\");\n  const [loading, setLoading] = useState(false);\n  const { setQueueAndPlay } = usePlayer();\n  const { theme, setTheme } = useTheme();\n  const [mounted, setMounted] = useState(false);\n  const [canGoBack, setCanGoBack] = useState(false);\n  const [isBackButtonVisible, setIsBackButtonVisible] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n    const checkBackButton = () => {\n      const path = router.pathname;\n      const isDetailPage = path.includes('/artists/[') || path.includes('/albums/[') || path.includes('/playlists/[');\n      const shouldShow = isDetailPage && window.history.length > 1;\n      \n      if (shouldShow !== canGoBack) {\n        setCanGoBack(shouldShow);\n        if (shouldShow) {\n          setTimeout(() => setIsBackButtonVisible(true), 50);\n        } else {\n          setIsBackButtonVisible(false);\n        }\n      }\n    };\n    checkBackButton();\n    router.events.on('routeChangeComplete', checkBackButton);\n    return () => {\n      router.events.off('routeChangeComplete', checkBackButton);\n    };\n  }, [router, canGoBack]);\n\n  const navLinks: NavLink[] = [\n    {\n      href: \"/home\",\n      icon: <IconInbox stroke={2} className=\"w-5\" />,\n      label: \"Home\",\n    },\n    {\n      href: \"/playlists\",\n      icon: <IconVinyl stroke={2} size={20} />,\n      label: \"Playlists\",\n    },\n    {\n      href: \"/songs\",\n      icon: <IconList stroke={2} size={20} />,\n      label: \"Songs\",\n    },\n    {\n      href: \"/albums\",\n      icon: <IconFocusCentered stroke={2} size={20} />,\n      label: \"Albums\",\n    },\n    {\n      href: \"/artists\",\n      icon: <IconUser stroke={2} size={20} />,\n      label: \"Artists\",\n    },\n  ];\n\n  const handleThemeToggle = () => {\n    if (theme === \"light\") {\n      setTheme(\"dark\");\n    } else if (theme === \"dark\") {\n      setTheme(\"system\");\n    } else {\n      setTheme(\"light\");\n    }\n  };\n\n  const renderIcon = () => {\n    if (!mounted) {\n      return <IconDeviceDesktop stroke={2} className=\"w-5\" />;\n    }\n    if (theme === \"light\") {\n      return <IconSun stroke={2} className=\"w-5\" />;\n    } else if (theme === \"dark\") {\n      return <IconMoon stroke={2} className=\"w-5\" />;\n    } else {\n      return <IconDeviceDesktop stroke={2} className=\"w-5\" />;\n    }\n  };\n\n  const isActive = (href: string): boolean => {\n    if (href === \"/home\" && router.pathname === \"/\") {\n      return true;\n    }\n\n    return (\n      router.pathname === href ||\n      (href !== \"/home\" && router.pathname.startsWith(href))\n    );\n  };\n\n  const handleNavigation = useCallback(\n    (href: string, e: React.MouseEvent) => {\n      if (isActive(href)) {\n        e.preventDefault();\n\n        if (router.pathname === href) {\n          const viewport = document.querySelector('[data-radix-scroll-area-viewport]');\n          if (viewport) {\n            (viewport as HTMLElement).scrollTop = 0;\n          }\n          \n          if (href === \"/albums\") {\n            window.ipc.send(\"resetAlbumsPageState\", null);\n          } else if (href === \"/songs\") {\n            window.ipc.send(\"resetSongsPageState\", null);\n          } else if (href === \"/playlists\") {\n            window.ipc.send(\"resetPlaylistsPageState\", null);\n          } else if (href === \"/home\") {\n            window.ipc.send(\"resetHomePageState\", null);\n          } else if (href === \"/artists\") {\n            window.ipc.send(\"resetArtistsPageState\", null);\n          }\n        } else {\n          // If navigating to a different page, just push the route\n          router.push(href);\n        }\n      }\n    },\n    [router],\n  );\n\n  useEffect(() => {\n    const down = (e) => {\n      if (e.key === \"f\" && (e.metaKey || e.ctrlKey)) {\n        e.preventDefault();\n        setOpen((open) => !open);\n      }\n    };\n\n    document.addEventListener(\"keydown\", down);\n    return () => document.removeEventListener(\"keydown\", down);\n  }, []);\n\n  useEffect(() => {\n    setLoading(true);\n\n    if (!search) {\n      setSearchResults([]);\n      setLoading(false);\n      return;\n    }\n\n    const delayDebounceFn = setTimeout(() => {\n      window.ipc.invoke(\"search\", search).then((response) => {\n        const albums = response.searchAlbums;\n        const playlists = response.searchPlaylists;\n        const songs = response.searchSongs;\n        const artists = response.searchArtists || [];\n\n        setSearchResults([\n          ...artists.map((artist: any) => ({ ...artist, type: \"Artist\" })),\n          ...playlists.map((playlist: any) => ({\n            ...playlist,\n            type: \"Playlist\",\n          })),\n          ...albums.map((album: any) => ({ ...album, type: \"Album\" })),\n          ...songs.map((song: any) => ({ ...song, type: \"Song\" })),\n        ]);\n\n        setLoading(false);\n      });\n    }, 1000);\n\n    return () => clearTimeout(delayDebounceFn);\n  }, [search]);\n\n  const openSearch = () => setOpen(true);\n\n  const handleItemClick = (item: any) => {\n    if (item.type === \"Album\") {\n      router.push(`/albums/${item.id}`);\n    } else if (item.type === \"Song\") {\n      setQueueAndPlay([item], 0);\n    } else if (item.type === \"Playlist\") {\n      router.push(`/playlists/${item.id}`);\n    } else if (item.type === \"Artist\") {\n      router.push(`/artists/${encodeURIComponent(item.name)}`);\n    }\n    setOpen(false);\n  };\n\n  useEffect(() => {\n    window.ipc.invoke(\"getSettings\").then((response) => {\n      setSettings(response);\n    });\n\n    window.ipc.on(\"confirmSettingsUpdate\", () => {\n      window.ipc.invoke(\"getSettings\").then((response) => {\n        setSettings(response);\n      });\n    });\n  }, []);\n\n  return (\n    <>\n      <div className=\"flex h-full flex-col items-center justify-center gap-10\">\n        <TooltipProvider>\n          <Tooltip delayDuration={0}>\n            <TooltipTrigger>\n              <Link href=\"/settings\">\n                <Avatar className=\"h-8 w-8\">\n                  <AvatarImage\n                    src={`${settings && settings.profilePicture ? \"wora://\" + settings.profilePicture : \"/userPicture.png\"}`}\n                  />\n                </Avatar>\n              </Link>\n            </TooltipTrigger>\n            <TooltipContent side=\"right\" sideOffset={25}>\n              <p>{settings && settings.name ? settings.name : \"Wora User\"}</p>\n            </TooltipContent>\n          </Tooltip>\n          <div className=\"wora-border flex w-18 flex-col items-center gap-10 rounded-2xl p-8 transition-all duration-300 ease-in-out\">\n            <div\n              className={`transition-all duration-300 ease-in-out ${\n                canGoBack ? 'max-h-12 opacity-100' : 'max-h-0 opacity-0 -mt-10 overflow-hidden'\n              }`}\n            >\n              {(canGoBack || isBackButtonVisible) && (\n                <Tooltip delayDuration={0}>\n                  <TooltipTrigger asChild>\n                    <Button\n                      variant=\"ghost\"\n                      onClick={() => router.back()}\n                      className={`transition-all duration-300 ${\n                        isBackButtonVisible ? 'scale-100 opacity-100' : 'scale-95 opacity-0'\n                      }`}\n                    >\n                      <IconArrowLeft stroke={2} className=\"w-5\" />\n                    </Button>\n                  </TooltipTrigger>\n                  <TooltipContent side=\"right\" sideOffset={50}>\n                    <p>Back</p>\n                  </TooltipContent>\n                </Tooltip>\n              )}\n            </div>\n            \n            {navLinks.map((link) => (\n              <Tooltip key={link.href} delayDuration={0}>\n                <TooltipTrigger asChild>\n                  <Button\n                    variant=\"ghost\"\n                    className={isActive(link.href) && \"opacity-100\"}\n                  >\n                    <Link\n                      href={link.href}\n                      onClick={(e) => handleNavigation(link.href, e)}\n                      className=\"flex h-full w-full items-center justify-center\"\n                    >\n                      {link.icon}\n                    </Link>\n                  </Button>\n                </TooltipTrigger>\n                <TooltipContent side=\"right\" sideOffset={50}>\n                  <p>{link.label}</p>\n                </TooltipContent>\n              </Tooltip>\n            ))}\n\n            <Tooltip delayDuration={0}>\n              <TooltipTrigger asChild>\n                <Button variant=\"ghost\" onClick={openSearch}>\n                  <IconSearch stroke={2} className=\"w-5\" />\n                </Button>\n              </TooltipTrigger>\n              <TooltipContent side=\"right\" sideOffset={50}>\n                <p>Search</p>\n              </TooltipContent>\n            </Tooltip>\n          </div>\n          <Tooltip delayDuration={0}>\n            <TooltipTrigger asChild>\n              <Button variant=\"ghost\" onClick={handleThemeToggle}>\n                {renderIcon()}\n              </Button>\n            </TooltipTrigger>\n            <TooltipContent side=\"right\" sideOffset={25}>\n              <p className=\"capitalize\">Theme: {mounted ? theme : 'system'}</p>\n            </TooltipContent>\n          </Tooltip>\n        </TooltipProvider>\n      </div>\n\n      <CommandDialog open={open} onOpenChange={setOpen}>\n        <Command>\n          <CommandInput\n            placeholder=\"Search for a song, album or playlist...\"\n            value={search}\n            onValueChange={setSearch}\n          />\n          <CommandList>\n            {loading && (\n              <div className=\"flex h-[325px] w-full items-center justify-center\">\n                <Spinner className=\"h-6 w-6\" />\n              </div>\n            )}\n            {search && !loading ? (\n              <CommandGroup heading=\"Search Results\" className=\"pb-2\">\n                {searchResults.map((item) => (\n                  <CommandItem\n                    key={`${item.type}-${item.id || item.name}`}\n                    value={`${item.name}-${item.type}-${item.id || \"\"}`}\n                    onSelect={() => handleItemClick(item)}\n                    className=\"text-black dark:text-white\"\n                  >\n                    <div className=\"flex h-full w-full items-center gap-2.5 mask-r-from-70%\">\n                      {(item.type === \"Playlist\" || item.type === \"Album\") && (\n                        <div className=\"relative h-12 w-12 overflow-hidden rounded-lg shadow-xl transition duration-300\">\n                          <Image\n                            className=\"object-cover\"\n                            src={`wora://${item.cover}`}\n                            alt={item.name}\n                            fill\n                          />\n                        </div>\n                      )}\n                      {item.type === \"Artist\" && (\n                        <div className=\"dark:bg.white/10 flex h-12 w-12 items-center justify-center rounded-lg bg-black/10\">\n                          <IconUser stroke={1.5} size={24} />\n                        </div>\n                      )}\n                      <div>\n                        <p className=\"w-full overflow-hidden text-xs text-nowrap\">\n                          {item.name}\n                          <span className=\"ml-1 opacity-50\">({item.type})</span>\n                        </p>\n                        <p className=\"w-full text-xs opacity-50\">\n                          {item.type === \"Playlist\"\n                            ? item.description\n                            : item.type === \"Artist\"\n                              ? \"Artist\"\n                              : item.artist}\n                        </p>\n                      </div>\n                    </div>\n                  </CommandItem>\n                ))}\n              </CommandGroup>\n            ) : (\n              <div className=\"flex h-[325px] w-full items-center justify-center text-xs\">\n                <div className=\"dark:bg.white/10 ml-2 rounded-lg bg-black/5 px-1.5 py-1 shadow-xs\">\n                  ⌘ / Ctrl + F\n                </div>\n              </div>\n            )}\n          </CommandList>\n        </Command>\n      </CommandDialog>\n    </>\n  );\n};\n\nexport default Navbar;\n"
  },
  {
    "path": "renderer/components/main/player.tsx",
    "content": "import Image from \"next/image\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  IconArrowsShuffle2,\n  IconBrandLastfm,\n  IconCheck,\n  IconClock,\n  IconHeart,\n  IconInfoCircle,\n  IconList,\n  IconListTree,\n  IconMessage,\n  IconPlayerPause,\n  IconPlayerPlay,\n  IconPlayerSkipBack,\n  IconPlayerSkipForward,\n  IconPlus,\n  IconRepeat,\n  IconRipple,\n  IconVinyl,\n  IconVolume,\n  IconVolumeOff,\n  IconX,\n} from \"@tabler/icons-react\";\nimport React, { memo, useCallback, useEffect, useRef, useState } from \"react\";\nimport { Howl } from \"howler\";\nimport { FixedSizeList as List } from \"react-window\";\nimport { Slider } from \"@/components/ui/slider\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport Lyrics from \"@/components/main/lyrics\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport {\n  convertTime,\n  isSyncedLyrics,\n  parseLyrics,\n  updateDiscordState,\n  useAudioMetadata,\n} from \"@/lib/helpers\";\nimport { Song, usePlayer } from \"@/context/playerContext\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport Link from \"next/link\";\nimport { toast } from \"sonner\";\nimport {\n  ContextMenu,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuSub,\n  ContextMenuSubContent,\n  ContextMenuSubTrigger,\n  ContextMenuTrigger,\n} from \"@/components/ui/context-menu\";\nimport {\n  initializeLastFMWithSession,\n  scrobbleTrack,\n  updateNowPlaying,\n  isAuthenticated,\n} from \"@/lib/lastfm\";\nimport AutoSizer from \"react-virtualized-auto-sizer\";\nimport ErrorBoundary from \"@/components/ErrorBoundary\";\n\nconst NotificationToast = ({ success, message }: { success: boolean; message: string }) => (\n  <div className=\"flex w-fit items-center gap-2 text-xs\">\n    {success ? (\n      <IconCheck className=\"text-green-400\" stroke={2} size={16} />\n    ) : (\n      <IconX className=\"text-red-500\" stroke={2} size={16} />\n    )}\n    {message}\n  </div>\n);\n\nfunction getAlbumCoverUrl(song: Song | undefined): string {\n  const cover = song?.album?.cover;\n  if (!cover) return \"/coverArt.png\";\n  if (cover.includes(\"://\")) return cover;\n  return `wora://${cover}`;\n}\n\nconst QueuePanel = memo(({ queue, history, currentIndex, onSongSelect }: {\n  queue: Song[];\n  history: Song[];\n  currentIndex: number;\n  onSongSelect: (song: Song) => void;\n}) => {\n  const ITEM_HEIGHT = 80;\n\n  const VirtualizedSongListItem = ({ index, style, data }: {\n    index: number;\n    style: React.CSSProperties;\n    data: { songs: Song[]; onSongSelect: (song: Song) => void }\n  }) => {\n    const song = data.songs[index];\n\n    return (\n      <div style={style}>\n        <li\n          className=\"flex w-full items-center gap-4 overflow-hidden cursor-pointer hover:bg-black/5 dark:hover:bg-white/5 rounded-lg p-2 transition-colors\"\n          onClick={() => data.onSongSelect(song)}\n        >\n          <div className=\"relative min-h-14 min-w-14 overflow-hidden rounded-lg shadow-lg\">\n            <Image\n              alt={song.name || \"Track\"}\n              src={getAlbumCoverUrl(song)}\n              fill\n              priority={false}\n              className=\"object-cover\"\n            />\n          </div>\n          <div className=\"w-4/5 overflow-hidden\">\n            <p className=\"truncate text-sm font-medium\">{song.name}</p>\n            <p className=\"truncate opacity-50\">{song.artist}</p>\n          </div>\n        </li>\n      </div>\n    );\n  };\n\n  const queueSongs = queue.slice(currentIndex + 1);\n  const historySongs = [...history].reverse();\n\n  return (\n    <div className=\"wora-border relative h-full w-full rounded-2xl bg-white/70 backdrop-blur-xl dark:bg-black/70 pointer-events-auto\">\n      <div className=\"h-utility w-full max-w-3xl px-6 pt-6 pointer-events-auto\">\n        <Tabs\n          defaultValue=\"queue\"\n          className=\"flex h-full w-full flex-col gap-4 mask-b-from-70% pointer-events-auto\"\n        >\n          <TabsList className=\"w-full pointer-events-auto\">\n            <TabsTrigger value=\"queue\" className=\"w-full gap-2 cursor-pointer pointer-events-auto\">\n              <IconListTree stroke={2} size={15} /> Queue\n            </TabsTrigger>\n            <TabsTrigger value=\"history\" className=\"w-full gap-2 cursor-pointer pointer-events-auto\">\n              <IconClock stroke={2} size={15} /> History\n            </TabsTrigger>\n          </TabsList>\n\n          <TabsContent\n            value=\"queue\"\n            className=\"flex-1 min-h-0 pointer-events-auto\"\n          >\n            {queueSongs.length > 0 ? (\n              <ErrorBoundary>\n                <AutoSizer>\n                  {({ height, width }) => (\n                    <List\n                      height={height}\n                      width={width}\n                      itemCount={queueSongs.length}\n                      itemSize={ITEM_HEIGHT}\n                      itemData={{ songs: queueSongs, onSongSelect }}\n                      className=\"no-scrollbar pointer-events-auto\"\n                    >\n                      {VirtualizedSongListItem}\n                    </List>\n                  )}\n                </AutoSizer>\n              </ErrorBoundary>\n            ) : (\n              <div className=\"flex h-40 items-center justify-center text-sm opacity-50 pointer-events-none\">\n                Queue is empty\n              </div>\n            )}\n          </TabsContent>\n\n          <TabsContent\n            value=\"history\"\n            className=\"flex-1 min-h-0 pointer-events-auto\"\n          >\n            {historySongs.length > 0 ? (\n              <ErrorBoundary>\n                <AutoSizer>\n                  {({ height, width }) => (\n                    <List\n                      height={height}\n                      width={width}\n                      itemCount={historySongs.length}\n                      overscanCount={5}\n                      itemSize={ITEM_HEIGHT}\n                      itemData={{ songs: historySongs, onSongSelect }}\n                      className=\"no-scrollbar pointer-events-auto\"\n                    >\n                      {VirtualizedSongListItem}\n                    </List>\n                  )}\n                </AutoSizer>\n              </ErrorBoundary>\n            ) : (\n              <div className=\"flex h-40 items-center justify-center text-sm opacity-50 pointer-events-none\">\n                No playback history\n              </div>\n            )}\n          </TabsContent>\n        </Tabs>\n      </div>\n    </div>\n  );\n});\n\nexport const Player = () => {\n  // Player state\n  const [seekPosition, setSeekPosition] = useState(0);\n  const [volume, setVolume] = useState(0.5);\n  const [previousVolume, setPreviousVolume] = useState(0.5);\n  const [isMuted, setIsMuted] = useState(false);\n  const [currentLyric, setCurrentLyric] = useState(null);\n  const [showLyrics, setShowLyrics] = useState(false);\n  const [showQueue, setShowQueue] = useState(false);\n  const [isFavourite, setIsFavourite] = useState(false);\n  const [playlists, setPlaylists] = useState([]);\n  const [isClient, setIsClient] = useState(false);\n  const [lastFmSettings, setLastFmSettings] = useState({\n    lastFmUsername: null,\n    lastFmSessionKey: null,\n    enableLastFm: false,\n    scrobbleThreshold: 50,\n  });\n  const [lastFmStatus, setLastFmStatus] = useState({\n    isScrobbled: false,\n    isNowPlaying: false,\n    scrobbleTimerStarted: false,\n    error: null,\n    lastFmActive: false,\n  });\n  const scrobbleTimeout = useRef<NodeJS.Timeout | null>(null);\n\n  // References\n  const soundRef = useRef<Howl | null>(null);\n  const seekUpdateInterval = useRef<NodeJS.Timeout | null>(null);\n  const volumeSliderRef = useRef<HTMLDivElement | null>(null);\n\n  // Get player context and song metadata\n  const {\n    song,\n    nextSong,\n    previousSong,\n    queue,\n    history,\n    currentIndex,\n    repeat,\n    shuffle,\n    toggleShuffle,\n    toggleRepeat,\n    jumpToSong,\n    isPlaying,\n    setIsPlaying,\n  } = usePlayer();\n\n  const { metadata, lyrics, favourite } = useAudioMetadata(song?.filePath);\n\n  // Load Last.fm settings\n  useEffect(() => {\n    const loadLastFmSettings = async () => {\n      try {\n        const settings = await window.ipc.invoke(\"getLastFmSettings\");\n        setLastFmSettings(settings);\n\n        // Initialize Last.fm with session key if available\n        if (settings.lastFmSessionKey && settings.enableLastFm) {\n          initializeLastFMWithSession(\n            settings.lastFmSessionKey,\n            settings.lastFmUsername || \"\",\n          );\n          setLastFmStatus((prev) => ({ ...prev, lastFmActive: true }));\n          console.log(\"[Last.fm] Initialized with session key\");\n        } else {\n          // Clear Last.fm status if disabled or no session\n          setLastFmStatus((prev) => ({\n            ...prev,\n            lastFmActive: false,\n            isScrobbled: false,\n            isNowPlaying: false,\n          }));\n          console.log(\"[Last.fm] Disabled or no session key\");\n        }\n      } catch (error) {\n        console.error(\"[Last.fm] Error loading settings:\", error);\n      }\n    };\n\n    // Load settings initially\n    loadLastFmSettings();\n\n    // Set up listener for Last.fm settings changes\n    const removeListener = window.ipc.on(\n      \"lastFmSettingsChanged\",\n      loadLastFmSettings,\n    );\n\n    return () => {\n      removeListener();\n    };\n  }, []);\n\n  // Reset scrobble status when song changes\n  useEffect(() => {\n    setLastFmStatus({\n      isScrobbled: false,\n      isNowPlaying: false,\n      scrobbleTimerStarted: false,\n      error: null,\n      lastFmActive: lastFmStatus.lastFmActive,\n    });\n\n    if (scrobbleTimeout.current) {\n      clearInterval(scrobbleTimeout.current);\n      scrobbleTimeout.current = null;\n    }\n  }, [song]);\n\n  // Last.fm scrobble handler\n  const handleScrobble = useCallback(() => {\n    if (\n      !song ||\n      !lastFmSettings.enableLastFm ||\n      lastFmStatus.isScrobbled ||\n      !isAuthenticated()\n    ) {\n      // Skip scrobble checks without verbose logging\n      return;\n    }\n\n    // Clear existing timer if any\n    if (scrobbleTimeout.current) {\n      clearInterval(scrobbleTimeout.current);\n      scrobbleTimeout.current = null;\n    }\n\n    const scrobbleIfThresholdReached = () => {\n      if (!soundRef.current || lastFmStatus.isScrobbled) return;\n\n      const duration = soundRef.current.duration();\n      const currentPosition = soundRef.current.seek();\n      const playedPercentage = (currentPosition / duration) * 100;\n\n      // Only log in development\n      if (process.env.NODE_ENV !== \"production\") {\n        console.log(\n          `[Last.fm] Position: ${playedPercentage.toFixed(1)}%, threshold: ${lastFmSettings.scrobbleThreshold}%`,\n        );\n      }\n\n      if (playedPercentage >= lastFmSettings.scrobbleThreshold) {\n        // Clear the interval immediately to prevent multiple scrobbles\n        if (scrobbleTimeout.current) {\n          clearInterval(scrobbleTimeout.current);\n          scrobbleTimeout.current = null;\n        }\n\n        // Set scrobbled status immediately to prevent race conditions\n        setLastFmStatus((prev) => ({ ...prev, isScrobbled: true }));\n\n        // Minimal logging for production, log to file only for important events\n        try {\n          window.ipc.send(\"lastfm:log\", {\n            level: \"info\",\n            message: `Scrobbling track: ${song.artist} - ${song.name} (${playedPercentage.toFixed(1)}%)`,\n          });\n        } catch (err) {\n          // Silent error in production\n        }\n\n        // Scrobble the track\n        scrobbleTrack(song)\n          .then((success) => {\n            if (!success) {\n              setLastFmStatus((prev) => ({\n                ...prev,\n                error: \"Failed to scrobble track\",\n                isScrobbled: false, // Reset scrobbled state to allow retrying\n              }));\n            }\n          })\n          .catch((err) => {\n            // Log only the error message, not the entire error object\n            try {\n              window.ipc.send(\"lastfm:log\", {\n                level: \"error\",\n                message: `Scrobble error: ${err?.message || \"Unknown error\"}`,\n              });\n            } catch (logErr) {\n              // Silent fail in production\n            }\n\n            setLastFmStatus((prev) => ({\n              ...prev,\n              error: \"Error scrobbling track\",\n              isScrobbled: false, // Reset scrobbled state to allow retrying\n            }));\n          });\n      }\n    };\n\n    // Set timer to check scrobble threshold\n    const checkInterval = 2000; // Check every 2 seconds\n    scrobbleTimeout.current = setInterval(\n      scrobbleIfThresholdReached,\n      checkInterval,\n    );\n\n    return () => {\n      if (scrobbleTimeout.current) {\n        clearInterval(scrobbleTimeout.current);\n        scrobbleTimeout.current = null;\n      }\n    };\n  }, [song, lastFmSettings, lastFmStatus.isScrobbled]);\n\n  // Player control functions - Define handlePlayPause earlier to avoid reference error\n  const handlePlayPause = useCallback(() => {\n    if (!soundRef.current) return;\n\n    if (soundRef.current.playing()) {\n      soundRef.current.pause();\n    } else {\n      soundRef.current.play();\n    }\n  }, []);\n\n  const handleSeek = useCallback((value: number[]) => {\n    if (!soundRef.current) return;\n\n    soundRef.current.seek(value[0]);\n    setSeekPosition(value[0]);\n  }, []);\n\n  const handleVolume = useCallback((value: number[]) => {\n    // Store previous volume before muting (only if not currently muted)\n    if (!isMuted && value[0] > 0.01) {\n      setPreviousVolume(value[0]);\n    }\n\n    setIsMuted(value[0] === 0);\n    setVolume(value[0]);\n  }, [isMuted]);\n\n  const toggleMute = useCallback(() => {\n    if (!isMuted) {\n      // Store current volume before muting\n      if (volume > 0.01) {\n        setPreviousVolume(volume);\n      }\n\n      setVolume(0);\n      setIsMuted(true);\n\n      // Directly apply mute to audio\n      if (soundRef.current) {\n        soundRef.current.mute(true);\n      }\n    } else {\n      // Restore previous volume or default to 50%\n      const restoreVolume = previousVolume > 0.05 ? previousVolume : 0.5;\n      setVolume(restoreVolume);\n      setPreviousVolume(restoreVolume); // Update previousVolume to the restored value\n      setIsMuted(false);\n\n      // Directly apply volume and unmute to audio to avoid desync\n      if (soundRef.current) {\n        soundRef.current.volume(restoreVolume);\n        soundRef.current.mute(false);\n      }\n    }\n  }, [isMuted, volume, previousVolume]);\n\n  const handleVolumeWheel = useCallback((event: WheelEvent) => {\n    event.preventDefault();\n    const delta = event.deltaY > 0 ? -0.05 : 0.05; // Scroll down decreases, scroll up increases\n    const newVolume = Math.max(0, Math.min(1, Math.round((volume + delta) * 100) / 100));\n    console.log(`Volume changed: ${newVolume}`);\n    handleVolume([newVolume]);\n  }, [volume, handleVolume]);\n\n  const toggleFavourite = useCallback((id: number) => {\n    if (!id) return;\n\n    window.ipc.send(\"addToFavourites\", id);\n    setIsFavourite((prev) => !prev);\n  }, []);\n\n  const handleKeyDown = useCallback((event: KeyboardEvent) => {\n    // Only handle keyboard shortcuts if we're not focused on an input element\n    if (event.target instanceof HTMLElement &&\n      ['INPUT', 'TEXTAREA', 'SELECT'].includes(event.target.tagName)) {\n      return;\n    }\n\n    // Spacebar for play/pause (prevent page scroll)\n    if (event.code === 'Space') {\n      event.preventDefault();\n      handlePlayPause();\n      return;\n    }\n\n    // Like/Dislike Song: Alt + Shift + B\n    if (event.altKey && event.shiftKey && event.code === 'KeyB') {\n      // No preventDefault needed for this combo\n      if (song?.id) {\n        toggleFavourite(song.id);\n      }\n      return;\n    }\n\n    // Shuffle: Alt + S (Mac) | Ctrl/Cmd + S (Windows)\n    if (((event.altKey && navigator.platform.includes('Mac')) ||\n      (event.ctrlKey && !navigator.platform.includes('Mac'))) &&\n      event.code === 'KeyS') {\n      event.preventDefault(); // Prevent browser save dialog\n      toggleShuffle();\n      return;\n    }\n\n    // Repeat: Alt + R (Mac) | Ctrl/Cmd + R (Windows)\n    if (((event.altKey && navigator.platform.includes('Mac')) ||\n      (event.ctrlKey && !navigator.platform.includes('Mac'))) &&\n      event.code === 'KeyR') {\n      event.preventDefault(); // Prevent browser refresh\n      toggleRepeat();\n      return;\n    }\n\n    // Mute/Unmute: M\n    if (event.code === 'KeyM') {\n      // No preventDefault needed for M key\n      toggleMute();\n      return;\n    }\n\n    // Go to Previous: Up Arrow\n    if (event.code === 'ArrowUp') {\n      event.preventDefault(); // Prevent page scroll\n      previousSong();\n      return;\n    }\n\n    // Go to Next: Down Arrow\n    if (event.code === 'ArrowDown') {\n      event.preventDefault(); // Prevent page scroll\n      nextSong();\n      return;\n    }\n  }, [handlePlayPause, song, toggleFavourite, toggleShuffle, toggleRepeat, toggleMute, previousSong, nextSong]);\n\n  const handleLyricClick = useCallback((time: number) => {\n    if (!soundRef.current) return;\n\n    soundRef.current.seek(time);\n    setSeekPosition(time);\n  }, []);\n\n  const toggleLyrics = useCallback(() => {\n    setShowLyrics((prev) => !prev);\n  }, []);\n\n  const toggleQueue = useCallback(() => {\n    setShowQueue((prev) => !prev);\n  }, []);\n\n  const addSongToPlaylist = useCallback(\n    (playlistId: number, songId: number) => {\n      window.ipc\n        .invoke(\"addSongToPlaylist\", { playlistId, songId })\n        .then((response) => {\n          toast(\n            <NotificationToast\n              success={response === true}\n              message={\n                response === true\n                  ? \"Song added to playlist\"\n                  : \"Song already exists in playlist\"\n              }\n            />,\n          );\n        })\n        .catch(() => {\n          toast(\n            <NotificationToast\n              success={false}\n              message=\"Failed to add song to playlist\"\n            />,\n          );\n        });\n    },\n    [],\n  );\n\n  const handleSongSelect = useCallback((selectedSong: Song) => {\n    // Find the song in the current queue and jump to it\n    const songIndex = queue.findIndex(song => song.id === selectedSong.id);\n    if (songIndex !== -1) {\n      // Use the jumpToSong function which preserves history\n      jumpToSong(songIndex);\n    }\n  }, [queue, jumpToSong]);\n\n  // Enable client-side rendering\n  useEffect(() => {\n    setIsClient(true);\n\n    // Load playlists once on component mount\n    window.ipc\n      .invoke(\"getAllPlaylists\")\n      .then(setPlaylists)\n      .catch((err) => console.error(\"Failed to load playlists:\", err));\n\n    // Clean up on unmount\n    return () => {\n      if (seekUpdateInterval.current) {\n        clearInterval(seekUpdateInterval.current);\n      }\n\n      if (scrobbleTimeout.current) {\n        clearInterval(scrobbleTimeout.current);\n      }\n    };\n  }, []);\n\n  // Setup volume slider wheel event\n  useEffect(() => {\n    const volumeSlider = volumeSliderRef.current;\n    if (!volumeSlider) return;\n\n    volumeSlider.addEventListener('wheel', handleVolumeWheel, { passive: false });\n\n    return () => {\n      volumeSlider.removeEventListener('wheel', handleVolumeWheel);\n    };\n  }, [handleVolumeWheel]);\n\n  useEffect(() => {\n    document.addEventListener('keydown', handleKeyDown);\n\n    return () => {\n      document.removeEventListener('keydown', handleKeyDown);\n    };\n  }, [handleKeyDown]);\n\n  // Update favorite status when song changes\n  useEffect(() => {\n    if (song) {\n      setIsFavourite(favourite);\n    }\n  }, [song, favourite]);\n\n  // Reset scrobble status when song changes\n  useEffect(() => {\n    setLastFmStatus({\n      isScrobbled: false,\n      isNowPlaying: false,\n      scrobbleTimerStarted: false,\n      error: null,\n      lastFmActive: lastFmStatus.lastFmActive,\n    });\n\n    if (scrobbleTimeout.current) {\n      clearInterval(scrobbleTimeout.current);\n    }\n  }, [song]);\n\n  // Start scrobble timer when playing\n  useEffect(() => {\n    if (\n      isPlaying &&\n      song &&\n      lastFmSettings.enableLastFm &&\n      !lastFmStatus.scrobbleTimerStarted &&\n      isAuthenticated()\n    ) {\n      // Send now playing update to Last.fm\n      console.log(\"[Last.fm] Sending now playing update\");\n      updateNowPlaying(song)\n        .then((success) => {\n          setLastFmStatus((prev) => ({\n            ...prev,\n            isNowPlaying: success,\n            scrobbleTimerStarted: true,\n            error: success ? null : \"Failed to update now playing\",\n          }));\n          console.log(\"[Last.fm] Now playing update success:\", success);\n        })\n        .catch((err) => {\n          console.error(\"[Last.fm] Now playing error:\", err);\n          setLastFmStatus((prev) => ({\n            ...prev,\n            error: \"Error updating now playing\",\n          }));\n        });\n\n      // Start scrobble timer\n      handleScrobble();\n    }\n  }, [\n    isPlaying,\n    song,\n    lastFmSettings,\n    lastFmStatus.scrobbleTimerStarted,\n    handleScrobble,\n  ]);\n\n  // Initialize or update audio when song changes\n  useEffect(() => {\n    // Clean up previous audio and intervals\n    if (soundRef.current) {\n      soundRef.current.unload();\n    }\n\n    if (seekUpdateInterval.current) {\n      clearInterval(seekUpdateInterval.current);\n    }\n\n    // Reset seek position immediately when song changes\n    setSeekPosition(0);\n\n    // No song to play, exit early\n    if (!song?.filePath) return;\n\n    // Create new Howl instance\n    const sound = new Howl({\n      src: [`wora://${encodeURIComponent(song.filePath)}`],\n      format: [song.filePath.split(\".\").pop()],\n      html5: true,\n      autoplay: true,\n      preload: true,\n      volume: isMuted ? 0 : volume,\n      onload: () => {\n        setSeekPosition(0);\n        setIsPlaying(true);\n        updateDiscordState(1, song);\n        window.ipc.send(\"update-window\", [true, song?.artist, song?.name]);\n      },\n      onloaderror: (error) => {\n        console.error(\"Error loading audio:\", error);\n        setIsPlaying(false);\n        toast(\n          <NotificationToast success={false} message=\"Failed to load audio\" />,\n        );\n      },\n      onend: () => {\n        setIsPlaying(false);\n        window.ipc.send(\"update-window\", [false, null, null]);\n        if (!repeat) {\n          nextSong();\n        }\n      },\n      onplay: () => {\n        setIsPlaying(true);\n        window.ipc.send(\"update-window\", [true, song?.artist, song?.name]);\n      },\n      onpause: () => {\n        setIsPlaying(false);\n        window.ipc.send(\"update-window\", [false, false, false]);\n      },\n    });\n\n    soundRef.current = sound;\n\n    // Set up seek position updater\n    seekUpdateInterval.current = setInterval(() => {\n      if (sound.playing()) {\n        setSeekPosition(sound.seek());\n      }\n    }, 100);\n\n    // Clean up on unmount or when song changes\n    return () => {\n      sound.unload();\n      if (seekUpdateInterval.current) {\n        clearInterval(seekUpdateInterval.current);\n      }\n    };\n  }, [song, nextSong]); // Removed volume and isMuted from dependencies\n\n  // Handle lyrics updates\n  useEffect(() => {\n    if (!lyrics || !song || !isPlaying) return;\n\n    // Only parse lyrics if they exist and are synced\n    if (!isSyncedLyrics(lyrics)) return;\n\n    const parsedLyrics = parseLyrics(lyrics);\n    let lyricUpdateInterval: NodeJS.Timeout;\n\n    const updateCurrentLyric = () => {\n      if (!soundRef.current?.playing()) return;\n\n      const currentSeek = soundRef.current.seek();\n      const currentLyricLine = parsedLyrics.find((line, index) => {\n        const nextLine = parsedLyrics[index + 1];\n        return (\n          currentSeek >= line.time && (!nextLine || currentSeek < nextLine.time)\n        );\n      });\n\n      setCurrentLyric(currentLyricLine || null);\n    };\n\n    // Update lyrics less frequently than seek position (better performance)\n    lyricUpdateInterval = setInterval(updateCurrentLyric, 500);\n\n    return () => clearInterval(lyricUpdateInterval);\n  }, [song, lyrics, isPlaying]);\n\n  // Setup MediaSession API for media controls\n  useEffect(() => {\n    if (!song || !(\"mediaSession\" in navigator)) return;\n\n    const updateMediaSessionMetadata = async () => {\n      if (\"mediaSession\" in navigator && song) {\n        const toDataURL = (\n          url: string,\n          callback: (dataUrl: string) => void,\n        ) => {\n          const xhr = new XMLHttpRequest();\n          xhr.onload = () => {\n            const reader = new FileReader();\n            reader.onloadend = () => callback(reader.result as string);\n            reader.readAsDataURL(xhr.response);\n          };\n          xhr.open(\"GET\", url);\n          xhr.responseType = \"blob\";\n          xhr.send();\n        };\n\n        const coverUrl = song.album?.cover\n          ? song.album.cover.startsWith(\"/\") || song.album.cover.includes(\"://\")\n            ? song.album.cover\n            : `wora://${song.album.cover}`\n          : \"/coverArt.png\";\n\n        toDataURL(coverUrl, (dataUrl) => {\n          navigator.mediaSession.metadata = new MediaMetadata({\n            title: song?.name || \"Unknown Title\",\n            artist: song?.artist || \"Unknown Artist\",\n            album: song?.album?.name || \"Unknown Album\",\n            artwork: [{ src: dataUrl }],\n          });\n\n          // Set application name for Windows Media Controller\n          if (\"mediaSession\" in navigator) {\n            // @ts-ignore - applicationName is not in the official type definitions but works in Windows\n            navigator.mediaSession.metadata.applicationName = \"Wora\";\n          }\n\n          navigator.mediaSession.setActionHandler(\"play\", handlePlayPause);\n          navigator.mediaSession.setActionHandler(\"pause\", handlePlayPause);\n          navigator.mediaSession.setActionHandler(\n            \"previoustrack\",\n            previousSong,\n          );\n          navigator.mediaSession.setActionHandler(\"nexttrack\", nextSong);\n          navigator.mediaSession.setActionHandler(\"seekbackward\", () => {\n            if (soundRef.current) {\n              soundRef.current.seek(Math.max(0, soundRef.current.seek() - 10));\n            }\n          });\n          navigator.mediaSession.setActionHandler(\"seekforward\", () => {\n            if (soundRef.current) {\n              soundRef.current.seek(\n                Math.min(\n                  soundRef.current.duration(),\n                  soundRef.current.seek() + 10,\n                ),\n              );\n            }\n          });\n        });\n      }\n    };\n\n    updateMediaSessionMetadata();\n\n    const removeMediaControlListener = window.ipc.on(\n      \"media-control\",\n      (command) => {\n        switch (command) {\n          case \"play-pause\":\n            handlePlayPause();\n            break;\n          case \"previous\":\n            previousSong();\n            break;\n          case \"next\":\n            nextSong();\n            break;\n          default:\n            break;\n        }\n      },\n    );\n\n    return () => {\n      removeMediaControlListener();\n    };\n  }, [song, previousSong, nextSong]);\n\n  // Apply volume and mute settings when they change\n  useEffect(() => {\n    if (!soundRef.current) return;\n\n    // When unmuting, set volume first, then unmute\n    if (!isMuted) {\n      soundRef.current.volume(volume);\n      soundRef.current.mute(false);\n    } else {\n      soundRef.current.mute(true);\n    }\n  }, [volume, isMuted]);\n\n  // Apply repeat setting when it changes\n  useEffect(() => {\n    if (soundRef.current) {\n      soundRef.current.loop(repeat);\n    }\n  }, [repeat]);\n\n  // Server-side rendering placeholder\n  if (!isClient) {\n    return (\n      <div className=\"wora-border h-28 w-full overflow-hidden rounded-2xl p-6\">\n        <div className=\"relative flex h-full w-full items-center\">\n          {/* Empty placeholder to prevent hydration errors */}\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div>\n      <div className=\"absolute top-0 right-0 w-full\">\n        {showLyrics && lyrics && (\n          <Lyrics\n            lyrics={parseLyrics(lyrics)}\n            currentLyric={currentLyric}\n            onLyricClick={handleLyricClick}\n            isSyncedLyrics={isSyncedLyrics(lyrics)}\n          />\n        )}\n      </div>\n\n      <div className=\"!absolute top-0 right-0 w-96\">\n        {showQueue && <QueuePanel queue={queue} history={history} currentIndex={currentIndex} onSongSelect={handleSongSelect} />}\n      </div>\n\n      <div className=\"wora-border h-28 w-full overflow-hidden rounded-2xl p-6\">\n        <div className=\"relative flex h-full w-full items-center\">\n          <TooltipProvider>\n            <div className=\"absolute left-0 flex w-1/4 items-center justify-start gap-4 overflow-hidden\">\n              {song ? (\n                <ContextMenu>\n                  <ContextMenuTrigger>\n                    <Link\n                      href={song.album?.id ? `/albums/${song.album.id}` : \"#\"}\n                    >\n                      <div className=\"relative min-h-17 min-w-17 overflow-hidden rounded-lg shadow-lg transition\">\n                        <Image\n                          alt=\"Album Cover\"\n                          src={`wora://${song?.album.cover}`}\n                          fill\n                          priority={true}\n                          className=\"object-cover object-center\"\n                        />\n                      </div>\n                    </Link>\n                  </ContextMenuTrigger>\n\n                  <ContextMenuContent className=\"w-64\">\n                    <Link href={`/albums/${song.album?.id}`}>\n                      <ContextMenuItem className=\"flex items-center gap-2\">\n                        <IconVinyl stroke={2} size={14} />\n                        Go to Album\n                      </ContextMenuItem>\n                    </Link>\n                    <ContextMenuSub>\n                      <ContextMenuSubTrigger className=\"flex items-center gap-2\">\n                        <IconPlus stroke={2} size={14} />\n                        Add to Playlist\n                      </ContextMenuSubTrigger>\n                      <ContextMenuSubContent className=\"w-52\">\n                        {playlists.map((playlist) => (\n                          <ContextMenuItem\n                            key={playlist.id}\n                            onClick={() =>\n                              addSongToPlaylist(playlist.id, song.id)\n                            }\n                          >\n                            <p className=\"w-full truncate\">{playlist.name}</p>\n                          </ContextMenuItem>\n                        ))}\n                      </ContextMenuSubContent>\n                    </ContextMenuSub>\n                  </ContextMenuContent>\n                </ContextMenu>\n              ) : (\n                <div className=\"relative min-h-17 min-w-17 overflow-hidden rounded-lg shadow-lg\">\n                  <Image\n                    alt=\"Album Cover\"\n                    src=\"/coverArt.png\"\n                    fill\n                    priority={true}\n                    className=\"object-cover\"\n                  />\n                </div>\n              )}\n\n              <div className=\"w-full\">\n                <p className=\"truncate text-sm font-medium\">\n                  {song ? song.name : \"Echoes of Emptiness\"}\n                </p>\n                <Link\n                  href={\n                    song ? `/artists/${encodeURIComponent(song.artist)}` : \"#\"\n                  }\n                >\n                  <p className=\"cursor-pointer truncate opacity-50 hover:underline hover:opacity-80\">\n                    {song ? song.artist : \"The Void Ensemble\"}\n                  </p>\n                </Link>\n              </div>\n            </div>\n\n            <div className=\"absolute right-0 left-0 mx-auto flex h-full w-2/4 flex-col items-center justify-between gap-4\">\n              <div className=\"flex h-full w-full items-center justify-center gap-8\">\n                {metadata?.format?.lossless && (\n                  <div className=\"flex\">\n                    <Tooltip delayDuration={0}>\n                      <TooltipTrigger>\n                        <IconRipple\n                          stroke={2}\n                          className=\"w-3.5 cursor-pointer\"\n                        />\n                      </TooltipTrigger>\n                      <TooltipContent side=\"left\" sideOffset={25}>\n                        Lossless [{metadata.format.bitsPerSample}/\n                        {(metadata.format.sampleRate / 1000).toFixed(1)}kHz]\n                      </TooltipContent>\n                    </Tooltip>\n                  </div>\n                )}\n                <Button\n                  variant=\"ghost\"\n                  onClick={toggleShuffle}\n                  className=\"relative opacity-100!\"\n                >\n                  {!shuffle ? (\n                    <IconArrowsShuffle2\n                      stroke={2}\n                      size={16}\n                      className=\"wora-transition opacity-30! hover:opacity-100!\"\n                    />\n                  ) : (\n                    <div>\n                      <IconArrowsShuffle2 stroke={2} size={16} />\n                      <div className=\"absolute -top-2 right-0 left-0 mx-auto h-[1.5px] w-2/3 rounded-full bg-black dark:bg-white\"></div>\n                    </div>\n                  )}\n                </Button>\n\n                <Button variant=\"ghost\" onClick={previousSong}>\n                  <IconPlayerSkipBack\n                    stroke={2}\n                    className=\"fill-black dark:fill-white\"\n                    size={15}\n                  />\n                </Button>\n\n                <Button variant=\"ghost\" onClick={handlePlayPause}>\n                  {!isPlaying ? (\n                    <IconPlayerPlay\n                      stroke={2}\n                      className=\"h-6 w-6 fill-black dark:fill-white\"\n                    />\n                  ) : (\n                    <IconPlayerPause\n                      stroke={2}\n                      className=\"h-6 w-6 fill-black dark:fill-white\"\n                    />\n                  )}\n                </Button>\n\n                <Button variant=\"ghost\" onClick={nextSong}>\n                  <IconPlayerSkipForward\n                    stroke={2}\n                    className=\"h-4 w-4 fill-black dark:fill-white\"\n                  />\n                </Button>\n\n                <Button\n                  variant=\"ghost\"\n                  onClick={toggleRepeat}\n                  className=\"relative opacity-100!\"\n                >\n                  {!repeat ? (\n                    <IconRepeat\n                      stroke={2}\n                      size={15}\n                      className=\"wora-transition opacity-30! hover:opacity-100!\"\n                    />\n                  ) : (\n                    <div>\n                      <IconRepeat stroke={2} size={15} />\n                      <div className=\"absolute -top-2 right-0 left-0 mx-auto h-[1.5px] w-2/3 rounded-full bg-black dark:bg-white\"></div>\n                    </div>\n                  )}\n                </Button>\n\n                {lastFmSettings.enableLastFm &&\n                  lastFmSettings.lastFmSessionKey &&\n                  lastFmStatus.lastFmActive && (\n                    <div className=\"absolute left-28\">\n                      <Tooltip delayDuration={0}>\n                        <TooltipTrigger>\n                          <IconBrandLastfm\n                            stroke={2}\n                            size={14}\n                            className={`w-3.5 text-red-500 ${lastFmStatus.isScrobbled ? \"\" : lastFmStatus.isNowPlaying ? \"animate-pulse\" : \"opacity-30\"}`}\n                          />\n                        </TooltipTrigger>\n                        <TooltipContent side=\"left\" sideOffset={25}>\n                          {lastFmStatus.error ? (\n                            <p className=\"text-red-500\">\n                              Error: {lastFmStatus.error}\n                            </p>\n                          ) : lastFmStatus.isScrobbled ? (\n                            <p>Scrobbled to Last.fm</p>\n                          ) : lastFmStatus.isNowPlaying ? (\n                            <p>\n                              Now playing on Last.fm\n                              <br />\n                              Will scrobble at{\" \"}\n                              {lastFmSettings.scrobbleThreshold}%\n                            </p>\n                          ) : (\n                            <p>\n                              Will scrobble at{\" \"}\n                              {lastFmSettings.scrobbleThreshold}%\n                            </p>\n                          )}\n                        </TooltipContent>\n                      </Tooltip>\n                    </div>\n                  )}\n\n                <div className=\"flex\">\n                  <Tooltip delayDuration={0}>\n                    <TooltipTrigger asChild>\n                      <Button\n                        variant=\"ghost\"\n                        className=\"opacity-100! flex justify-center items-center\"\n                        onClick={() => toggleFavourite(song?.id)}\n                        disabled={!song}\n                      >\n                        <IconHeart\n                          stroke={2}\n                          className={`w-3.5 text-red-500 ${isFavourite ? \"fill-red-500\" : \"fill-none\"}`}\n                        />\n                      </Button>\n                    </TooltipTrigger>\n                    <TooltipContent side=\"right\" sideOffset={25}>\n                      <p>\n                        {!isFavourite\n                          ? \"Add to Favorites\"\n                          : \"Remove from Favorites\"}\n                      </p>\n                    </TooltipContent>\n                  </Tooltip>\n                </div>\n              </div>\n\n              <div className=\"relative flex h-full w-96 items-center px-4\">\n                <p className=\"absolute -left-8\">{convertTime(seekPosition)}</p>\n                <Slider\n                  value={[seekPosition]}\n                  onValueChange={handleSeek}\n                  max={soundRef.current?.duration() || 0}\n                  step={0.01}\n                />\n                <p className=\"absolute -right-8\">\n                  {convertTime(soundRef.current?.duration() || 0)}\n                </p>\n              </div>\n            </div>\n\n            <div className=\"absolute right-0 flex w-1/4 items-center justify-end gap-10\">\n              <div className=\"flex items-center gap-4\">\n                <Button\n                  variant=\"ghost\"\n                  onClick={toggleMute}\n                  className=\"opacity-100!\"\n                >\n                  {!isMuted ? (\n                    <IconVolume\n                      stroke={2}\n                      size={17.5}\n                      className=\"wora-transition opacity-30! hover:opacity-100!\"\n                    />\n                  ) : (\n                    <IconVolumeOff\n                      stroke={2}\n                      size={17.5}\n                      className=\"wora-transition opacity-30! hover:opacity-100!\"\n                    />\n                  )}\n                </Button>\n                <Slider\n                  ref={volumeSliderRef}\n                  onValueChange={handleVolume}\n                  value={[volume]}\n                  max={1}\n                  step={0.01}\n                  className=\"w-24\"\n                />\n              </div>\n\n              <div className=\"flex items-center gap-4\">\n                {lyrics ? (\n                  <Button variant=\"ghost\" onClick={toggleLyrics}>\n                    <IconMessage stroke={2} size={15} />\n                  </Button>\n                ) : (\n                  <IconMessage\n                    className=\"cursor-not-allowed text-red-500 opacity-75\"\n                    stroke={2}\n                    size={15}\n                  />\n                )}\n\n                <Dialog>\n                  <DialogTrigger\n                    className={\n                      song\n                        ? \"opacity-30 duration-500 hover:opacity-100 cursor-pointer\"\n                        : \"cursor-not-allowed text-red-500 opacity-75\"\n                    }\n                    disabled={!song}\n                  >\n                    <IconInfoCircle stroke={2} size={15} />\n                  </DialogTrigger>\n\n                  {song && (\n                    <DialogContent>\n                      <DialogHeader>\n                        <DialogTitle>Track Information</DialogTitle>\n                        <DialogDescription>\n                          Details for your currently playing song\n                        </DialogDescription>\n                      </DialogHeader>\n\n                      <div className=\"flex gap-4 overflow-hidden text-xs\">\n                        {/* Album cover */}\n                        <div className=\"h-full\">\n                          <div className=\"relative h-36 w-36 overflow-hidden rounded-xl\">\n                            <Image\n                              alt={song.name || \"Album\"}\n                              src={`wora://${song?.album.cover}`}\n                              fill\n                              className=\"object-cover\"\n                              quality={25}\n                            />\n                          </div>\n                        </div>\n\n                        {/* Track details */}\n                        <div className=\"flex h-full w-full flex-col gap-0.5\">\n                          <p className=\"mb-4 truncate\">\n                            → {metadata?.common?.title} [\n                            {metadata?.format?.codec || \"Unknown\"}]\n                          </p>\n\n                          <p className=\"truncate\">\n                            <span className=\"opacity-50\">Artist:</span>{\" \"}\n                            {metadata?.common?.artist || \"Unknown\"}\n                          </p>\n\n                          <p className=\"truncate\">\n                            <span className=\"opacity-50\">Album:</span>{\" \"}\n                            {metadata?.common?.album || \"Unknown\"}\n                          </p>\n\n                          <p className=\"truncate\">\n                            <span className=\"opacity-50\">Codec:</span>{\" \"}\n                            {metadata?.format?.codec || \"Unknown\"}\n                          </p>\n\n                          <p className=\"truncate\">\n                            <span className=\"opacity-50\">Sample:</span>{\" \"}\n                            {metadata?.format?.lossless\n                              ? `Lossless [${metadata.format.bitsPerSample}/${(metadata.format.sampleRate / 1000).toFixed(1)}kHz]`\n                              : \"Lossy Audio\"}\n                          </p>\n\n                          <p className=\"truncate\">\n                            <span className=\"opacity-50\">Duration:</span>{\" \"}\n                            {convertTime(soundRef.current?.duration() || 0)}\n                          </p>\n\n                          <p className=\"truncate\">\n                            <span className=\"opacity-50\">Genre:</span>{\" \"}\n                            {metadata?.common?.genre?.[0] || \"Unknown\"}\n                          </p>\n\n                          {lastFmSettings.enableLastFm &&\n                            lastFmStatus.lastFmActive && (\n                              <p className=\"truncate\">\n                                <span className=\"opacity-50\">Last.fm:</span>{\" \"}\n                                {lastFmStatus.error ? (\n                                  <span className=\"text-red-500\">\n                                    Error: {lastFmStatus.error}\n                                  </span>\n                                ) : lastFmStatus.isScrobbled ? (\n                                  \"Scrobbled\"\n                                ) : lastFmStatus.isNowPlaying ? (\n                                  <>\n                                    Now playing (will scrobble at{\" \"}\n                                    {lastFmSettings.scrobbleThreshold}%)\n                                  </>\n                                ) : (\n                                  <>\n                                    Waiting to scrobble at{\" \"}\n                                    {lastFmSettings.scrobbleThreshold}%\n                                  </>\n                                )}\n                              </p>\n                            )}\n                        </div>\n                      </div>\n                    </DialogContent>\n                  )}\n                </Dialog>\n\n                <Button variant=\"ghost\" onClick={toggleQueue}>\n                  <IconList stroke={2} size={15} />\n                </Button>\n              </div>\n            </div>\n          </TooltipProvider>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default Player;\n"
  },
  {
    "path": "renderer/components/themeProvider.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\";\nimport { type ThemeProviderProps } from \"next-themes\";\n\nexport function ThemeProvider({ children, ...props }: ThemeProviderProps) {\n  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;\n}\n"
  },
  {
    "path": "renderer/components/ui/actions.tsx",
    "content": "import {\n  IconBox,\n  IconLine,\n  IconLineDashed,\n  IconSquare,\n  IconX,\n} from \"@tabler/icons-react\";\nimport Image from \"next/image\";\nimport { useEffect, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\n\ntype Data = {\n  appVersion: string;\n  isNotMac: boolean;\n};\n\nfunction Actions() {\n  const [data, setData] = useState<Data>(null);\n  const [isMaximized, setIsMaximized] = useState(false);\n\n  useEffect(() => {\n    window.ipc.invoke(\"getActionsData\").then((response) => {\n      setData(response);\n    });\n  }, []);\n\n  return (\n    <div className=\"drag absolute top-0 z-50 flex h-11 w-full items-center justify-end px-8 py-2.5\">\n      <div className=\"relative flex h-full w-full items-center justify-center\">\n        <div className=\"flex h-full items-center gap-2\">\n          <Image\n            src={\"/assets/Logo [Dark].ico\"}\n            alt=\"logo\"\n            width={16}\n            height={16}\n            className=\"hidden dark:block\"\n          />\n          <Image\n            src={\"/assets/Logo.ico\"}\n            className=\"block dark:hidden\"\n            alt=\"logo\"\n            width={16}\n            height={16}\n          />\n          Wora\n        </div>\n        <div className=\"no-drag absolute -right-2 top-0 flex h-full items-center gap-2.5\">\n          {data && data.isNotMac && (\n            <>\n              <Button\n                variant=\"ghost\"\n                onClick={() => window.ipc.send(\"minimizeWindow\", true)}\n              >\n                <IconLineDashed size={14} stroke={2} />\n              </Button>\n              <Button\n                variant=\"ghost\"\n                onClick={() => {\n                  setIsMaximized(!isMaximized);\n                  window.ipc.send(\"maximizeWindow\", !isMaximized);\n                }}\n              >\n                <IconSquare size={11} stroke={2} />\n              </Button>\n              <Button\n                variant=\"ghost\"\n                onClick={() => window.ipc.send(\"quitApp\", true)}\n              >\n                <IconX size={14} stroke={2} />\n              </Button>\n            </>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport default Actions;\n"
  },
  {
    "path": "renderer/components/ui/album.tsx",
    "content": "import Image from \"next/image\";\nimport Link from \"next/link\";\nimport React from \"react\";\n\ntype Album = {\n  id: string;\n  name: string;\n  artist: string;\n  cover: string;\n};\n\ntype AlbumCardProps = {\n  album: Album;\n};\n\nconst AlbumCard: React.FC<AlbumCardProps> = ({ album }) => {\n  return (\n    <Link href={`/albums/${album.id}`}>\n      <div className=\"group/album wora-border wora-transition rounded-2xl p-5 hover:bg-black/5 dark:hover:bg-white/10\">\n        <div className=\"relative flex flex-col justify-between\">\n          <div className=\"relative w-full overflow-hidden rounded-xl pb-[100%] shadow-lg\">\n            <Image\n              alt={album ? album.name : \"Album Cover\"}\n              src={`wora://${album.cover}`}\n              fill\n              loading=\"lazy\"\n              className=\"z-10 cursor-pointer object-cover\"\n            />\n          </div>\n          <div className=\"mt-8 flex w-full flex-col overflow-clip\">\n            <p className=\"cursor-pointer mask-r-from-70% text-sm font-medium text-nowrap\">\n              {album.name}\n            </p>\n            <p className=\"mr-2 truncate opacity-50\">{album.artist}</p>\n          </div>\n        </div>\n      </div>\n    </Link>\n  );\n};\n\nexport default AlbumCard;\n"
  },
  {
    "path": "renderer/components/ui/avatar.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Avatar = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full shadow-lg\",\n      className,\n    )}\n    {...props}\n  />\n));\nAvatar.displayName = AvatarPrimitive.Root.displayName;\n\nconst AvatarImage = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Image>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Image\n    ref={ref}\n    className={cn(\"aspect-square h-full w-full\", className)}\n    {...props}\n  />\n));\nAvatarImage.displayName = AvatarPrimitive.Image.displayName;\n\nconst AvatarFallback = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Fallback>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Fallback\n    ref={ref}\n    className={cn(\n      \"flex h-full w-full items-center justify-center rounded-full bg-white/10\",\n      className,\n    )}\n    {...props}\n  />\n));\nAvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;\n\nexport { Avatar, AvatarImage, AvatarFallback };\n"
  },
  {
    "path": "renderer/components/ui/badge.tsx",
    "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst badgeVariants = cva(\n  \"inline-flex items-center rounded-full py-1 px-2 text-[0.675rem]\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-black/5 dark:bg-white/10\",\n        secondary:\n          \"border-transparent bg-neutral-100 text-neutral-900 hover:bg-neutral-100/80 dark:bg-neutral-800 dark:text-neutral-50 dark:hover:bg-neutral-800/80\",\n        destructive:\n          \"border-transparent bg-red-500 text-neutral-50 shadow-sm hover:bg-red-500/80 dark:bg-red-900 dark:text-neutral-50 dark:hover:bg-red-900/80\",\n        outline: \"text-neutral-950 dark:text-neutral-50\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nexport interface BadgeProps\n  extends React.HTMLAttributes<HTMLDivElement>,\n    VariantProps<typeof badgeVariants> {}\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n  return (\n    <div className={cn(badgeVariants({ variant }), className)} {...props} />\n  );\n}\n\nexport { Badge, badgeVariants };\n"
  },
  {
    "path": "renderer/components/ui/button.tsx",
    "content": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n  \"cursor-pointer active:scale-90 inline-flex py-2.5 px-4 items-center gap-2 rounded-xl wora-transition\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-white/70 dark:bg-black/30 hover:scale-95 wora-border\",\n        destructive: \"bg-red-500/10 hover:scale-95 border border-red-500/15\",\n        outline:\n          \"border border-neutral-200 bg-white shadow-xs hover:bg-neutral-100 hover:text-neutral-900 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50\",\n        ghost: \"wora-transition opacity-30 hover:opacity-100 p-0\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\";\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, className }))}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "renderer/components/ui/carousel.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport useEmblaCarousel, {\n  type UseEmblaCarouselType,\n} from \"embla-carousel-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  IconArrowLeft,\n  IconArrowRight,\n  IconChevronCompactLeft,\n  IconChevronLeft,\n  IconChevronRight,\n} from \"@tabler/icons-react\";\n\ntype CarouselApi = UseEmblaCarouselType[1];\ntype UseCarouselParameters = Parameters<typeof useEmblaCarousel>;\ntype CarouselOptions = UseCarouselParameters[0];\ntype CarouselPlugin = UseCarouselParameters[1];\n\ntype CarouselProps = {\n  opts?: CarouselOptions;\n  plugins?: CarouselPlugin;\n  orientation?: \"horizontal\" | \"vertical\";\n  setApi?: (api: CarouselApi) => void;\n};\n\ntype CarouselContextProps = {\n  carouselRef: ReturnType<typeof useEmblaCarousel>[0];\n  api: ReturnType<typeof useEmblaCarousel>[1];\n  scrollPrev: () => void;\n  scrollNext: () => void;\n  canScrollPrev: boolean;\n  canScrollNext: boolean;\n} & CarouselProps;\n\nconst CarouselContext = React.createContext<CarouselContextProps | null>(null);\n\nfunction useCarousel() {\n  const context = React.useContext(CarouselContext);\n\n  if (!context) {\n    throw new Error(\"useCarousel must be used within a <Carousel />\");\n  }\n\n  return context;\n}\n\nconst Carousel = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement> & CarouselProps\n>(\n  (\n    {\n      orientation = \"horizontal\",\n      opts,\n      setApi,\n      plugins,\n      className,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    const [carouselRef, api] = useEmblaCarousel(\n      {\n        ...opts,\n        axis: orientation === \"horizontal\" ? \"x\" : \"y\",\n      },\n      plugins,\n    );\n    const [canScrollPrev, setCanScrollPrev] = React.useState(false);\n    const [canScrollNext, setCanScrollNext] = React.useState(false);\n\n    const onSelect = React.useCallback((api: CarouselApi) => {\n      if (!api) {\n        return;\n      }\n\n      setCanScrollPrev(api.canScrollPrev());\n      setCanScrollNext(api.canScrollNext());\n    }, []);\n\n    const scrollPrev = React.useCallback(() => {\n      api?.scrollPrev();\n    }, [api]);\n\n    const scrollNext = React.useCallback(() => {\n      api?.scrollNext();\n    }, [api]);\n\n    const handleKeyDown = React.useCallback(\n      (event: React.KeyboardEvent<HTMLDivElement>) => {\n        if (event.key === \"ArrowLeft\") {\n          event.preventDefault();\n          scrollPrev();\n        } else if (event.key === \"ArrowRight\") {\n          event.preventDefault();\n          scrollNext();\n        }\n      },\n      [scrollPrev, scrollNext],\n    );\n\n    React.useEffect(() => {\n      if (!api || !setApi) {\n        return;\n      }\n\n      setApi(api);\n    }, [api, setApi]);\n\n    React.useEffect(() => {\n      if (!api) {\n        return;\n      }\n\n      onSelect(api);\n      api.on(\"reInit\", onSelect);\n      api.on(\"select\", onSelect);\n\n      return () => {\n        api?.off(\"select\", onSelect);\n      };\n    }, [api, onSelect]);\n\n    return (\n      <CarouselContext.Provider\n        value={{\n          carouselRef,\n          api: api,\n          opts,\n          orientation:\n            orientation || (opts?.axis === \"y\" ? \"vertical\" : \"horizontal\"),\n          scrollPrev,\n          scrollNext,\n          canScrollPrev,\n          canScrollNext,\n        }}\n      >\n        <div\n          ref={ref}\n          onKeyDownCapture={handleKeyDown}\n          className={cn(\"relative\", className)}\n          role=\"region\"\n          aria-roledescription=\"carousel\"\n          {...props}\n        >\n          {children}\n        </div>\n      </CarouselContext.Provider>\n    );\n  },\n);\nCarousel.displayName = \"Carousel\";\n\nconst CarouselContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n  const { carouselRef, orientation } = useCarousel();\n\n  return (\n    <div ref={carouselRef} className=\"overflow-hidden\">\n      <div\n        ref={ref}\n        className={cn(\n          \"flex\",\n          orientation === \"horizontal\" ? \"-ml-4\" : \"-mt-4 flex-col\",\n          className,\n        )}\n        {...props}\n      />\n    </div>\n  );\n});\nCarouselContent.displayName = \"CarouselContent\";\n\nconst CarouselItem = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n  const { orientation } = useCarousel();\n\n  return (\n    <div\n      ref={ref}\n      role=\"group\"\n      aria-roledescription=\"slide\"\n      className={cn(\n        \"min-w-0 shrink-0 grow-0 basis-full\",\n        orientation === \"horizontal\" ? \"pl-4\" : \"pt-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n});\nCarouselItem.displayName = \"CarouselItem\";\n\nconst CarouselPrevious = React.forwardRef<\n  HTMLButtonElement,\n  React.ComponentProps<typeof Button>\n>(({ className, variant = \"ghost\", ...props }, ref) => {\n  const { orientation, scrollPrev, canScrollPrev } = useCarousel();\n\n  return (\n    <Button\n      ref={ref}\n      variant={variant}\n      className={cn(\n        \"absolute h-8 w-8 rounded-full\",\n        orientation === \"horizontal\"\n          ? \"-left-12 top-1/2 -translate-y-1/2\"\n          : \"-top-12 left-1/2 -translate-x-1/2 rotate-90\",\n        className,\n      )}\n      disabled={!canScrollPrev}\n      onClick={scrollPrev}\n      {...props}\n    >\n      <IconChevronLeft stroke={2} size={28} />\n    </Button>\n  );\n});\nCarouselPrevious.displayName = \"CarouselPrevious\";\n\nconst CarouselNext = React.forwardRef<\n  HTMLButtonElement,\n  React.ComponentProps<typeof Button>\n>(({ className, variant = \"ghost\", ...props }, ref) => {\n  const { orientation, scrollNext, canScrollNext } = useCarousel();\n\n  return (\n    <Button\n      ref={ref}\n      variant={variant}\n      className={cn(\n        \"absolute h-8 w-8 rounded-full\",\n        orientation === \"horizontal\"\n          ? \"-right-12 top-1/2 -translate-y-1/2\"\n          : \"-bottom-12 left-1/2 -translate-x-1/2 rotate-90\",\n        className,\n      )}\n      disabled={!canScrollNext}\n      onClick={scrollNext}\n      {...props}\n    >\n      <IconChevronRight stroke={2} size={28} />\n    </Button>\n  );\n});\nCarouselNext.displayName = \"CarouselNext\";\n\nexport {\n  type CarouselApi,\n  Carousel,\n  CarouselContent,\n  CarouselItem,\n  CarouselPrevious,\n  CarouselNext,\n};\n"
  },
  {
    "path": "renderer/components/ui/command.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { type DialogProps } from \"@radix-ui/react-dialog\";\nimport { Command as CommandPrimitive } from \"cmdk\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Dialog, DialogContent } from \"@/components/ui/dialog\";\nimport { IconSearch } from \"@tabler/icons-react\";\n\nconst Command = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive\n    ref={ref}\n    className={cn(\n      \"wora-transition no-scrollar max-h-96 min-h-96 overflow-hidden rounded-xl\",\n      className,\n    )}\n    {...props}\n  />\n));\nCommand.displayName = CommandPrimitive.displayName;\n\ninterface CommandDialogProps extends DialogProps {}\n\nconst CommandDialog = ({ children, ...props }: CommandDialogProps) => {\n  return (\n    <Dialog {...props}>\n      <DialogContent className=\"overflow-hidden p-0\">\n        <Command>{children}</Command>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst CommandInput = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Input>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>\n>(({ className, ...props }, ref) => (\n  <div\n    className=\"flex items-center border-b border-black/5 p-3.5 text-black dark:border-white/10 dark:text-white\"\n    cmdk-input-wrapper=\"\"\n  >\n    <IconSearch className=\"mr-2 h-5 w-5 shrink-0 opacity-50\" />\n    <CommandPrimitive.Input\n      ref={ref}\n      className={cn(\n        \"flex w-full bg-transparent text-xs outline-hidden placeholder:text-black/50 dark:placeholder:text-white/50\",\n        className,\n      )}\n      {...props}\n    />\n  </div>\n));\n\nCommandInput.displayName = CommandPrimitive.Input.displayName;\n\nconst CommandList = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.List\n    ref={ref}\n    className={cn(\n      \"no-scrollbar max-h-[325px] overflow-y-auto overflow-x-hidden rounded-2xl\",\n      className,\n    )}\n    {...props}\n  />\n));\n\nCommandList.displayName = CommandPrimitive.List.displayName;\n\nconst CommandEmpty = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Empty>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>\n>((props, ref) => (\n  <CommandPrimitive.Empty\n    ref={ref}\n    className=\"flex h-full w-full items-center justify-center\"\n    {...props}\n  />\n));\n\nCommandEmpty.displayName = CommandPrimitive.Empty.displayName;\n\nconst CommandGroup = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Group>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Group\n    ref={ref}\n    className={cn(\n      \"overflow-hidden px-2 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-neutral-500 dark:[&_[cmdk-group-heading]]:text-neutral-400\",\n      className,\n    )}\n    {...props}\n  />\n));\n\nCommandGroup.displayName = CommandPrimitive.Group.displayName;\n\nconst CommandSeparator = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 h-px bg-neutral-200 dark:bg-neutral-800\", className)}\n    {...props}\n  />\n));\nCommandSeparator.displayName = CommandPrimitive.Separator.displayName;\n\nconst CommandItem = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"wora-transition relative flex cursor-pointer select-none items-center text-nowrap rounded-xl p-2 text-xs outline-hidden data-[disabled=true]:pointer-events-none data-[selected=true]:bg-black/5 data-[selected=true]:text-black data-[disabled=true]:opacity-50 dark:data-[selected=true]:bg-white/10 dark:data-[selected=true]:text-white\",\n      className,\n    )}\n    {...props}\n  />\n));\n\nCommandItem.displayName = CommandPrimitive.Item.displayName;\n\nconst CommandShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\n        \"ml-auto text-xs tracking-widest text-neutral-500 dark:text-neutral-400\",\n        className,\n      )}\n      {...props}\n    />\n  );\n};\nCommandShortcut.displayName = \"CommandShortcut\";\n\nexport {\n  Command,\n  CommandDialog,\n  CommandInput,\n  CommandList,\n  CommandEmpty,\n  CommandGroup,\n  CommandItem,\n  CommandShortcut,\n  CommandSeparator,\n};\n"
  },
  {
    "path": "renderer/components/ui/context-menu.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as ContextMenuPrimitive from \"@radix-ui/react-context-menu\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  IconCheck,\n  IconChevronRight,\n  IconCircleFilled,\n} from \"@tabler/icons-react\";\n\nconst ContextMenu = ContextMenuPrimitive.Root;\n\nconst ContextMenuTrigger = ContextMenuPrimitive.Trigger;\n\nconst ContextMenuGroup = ContextMenuPrimitive.Group;\n\nconst ContextMenuPortal = ContextMenuPrimitive.Portal;\n\nconst ContextMenuSub = ContextMenuPrimitive.Sub;\n\nconst ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;\n\nconst ContextMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {\n    inset?: boolean;\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <ContextMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      \"wora-transition flex cursor-pointer select-none items-center rounded-lg p-2 text-xs outline-hidden hover:bg-black/5 data-[state=open]:bg-black/5 data-[state=open]:text-black dark:hover:bg-white/10 dark:data-[state=open]:bg-white/10 dark:data-[state=open]:text-white\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <IconChevronRight className=\"ml-auto h-4 w-4\" />\n  </ContextMenuPrimitive.SubTrigger>\n));\nContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;\n\nconst ContextMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <ContextMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      \"wora-border z-50 min-w-20 overflow-hidden rounded-xl bg-white p-2 text-black shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:bg-black dark:text-white\",\n      className,\n    )}\n    {...props}\n  />\n));\nContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;\n\nconst ContextMenuContent = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <ContextMenuPrimitive.Portal>\n    <ContextMenuPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"wora-border z-50 min-w-20 overflow-hidden rounded-xl bg-white p-2 text-black shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:bg-black dark:text-white\",\n        className,\n      )}\n      {...props}\n    />\n  </ContextMenuPrimitive.Portal>\n));\nContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;\n\nconst ContextMenuItem = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <ContextMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"wora-transition flex cursor-pointer select-none items-center rounded-lg p-2 text-xs outline-hidden hover:bg-black/5 data-disabled:pointer-events-none data-disabled:opacity-50 dark:hover:bg-white/10 dark:focus:bg-neutral-800 dark:focus:text-neutral-50\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  />\n));\nContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;\n\nconst ContextMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <ContextMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-xs py-1.5 pl-8 pr-2 text-sm outline-hidden focus:bg-neutral-100 focus:text-neutral-900 data-disabled:pointer-events-none data-disabled:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50\",\n      className,\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <ContextMenuPrimitive.ItemIndicator>\n        <IconCheck className=\"h-4 w-4\" />\n      </ContextMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </ContextMenuPrimitive.CheckboxItem>\n));\nContextMenuCheckboxItem.displayName =\n  ContextMenuPrimitive.CheckboxItem.displayName;\n\nconst ContextMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <ContextMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-xs py-1.5 pl-8 pr-2 text-sm outline-hidden focus:bg-neutral-100 focus:text-neutral-900 data-disabled:pointer-events-none data-disabled:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50\",\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <ContextMenuPrimitive.ItemIndicator>\n        <IconCircleFilled className=\"h-4 w-4 fill-current\" />\n      </ContextMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </ContextMenuPrimitive.RadioItem>\n));\nContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;\n\nconst ContextMenuLabel = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <ContextMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      \"px-2 py-1.5 text-sm font-semibold text-neutral-950 dark:text-neutral-50\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  />\n));\nContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;\n\nconst ContextMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof ContextMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <ContextMenuPrimitive.Separator\n    ref={ref}\n    className={cn(\n      \"-mx-1 my-1 h-px bg-neutral-200 dark:bg-neutral-800\",\n      className,\n    )}\n    {...props}\n  />\n));\nContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;\n\nconst ContextMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\n        \"ml-auto text-xs tracking-widest text-neutral-500 dark:text-neutral-400\",\n        className,\n      )}\n      {...props}\n    />\n  );\n};\nContextMenuShortcut.displayName = \"ContextMenuShortcut\";\n\nexport {\n  ContextMenu,\n  ContextMenuTrigger,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuCheckboxItem,\n  ContextMenuRadioItem,\n  ContextMenuLabel,\n  ContextMenuSeparator,\n  ContextMenuShortcut,\n  ContextMenuGroup,\n  ContextMenuPortal,\n  ContextMenuSub,\n  ContextMenuSubContent,\n  ContextMenuSubTrigger,\n  ContextMenuRadioGroup,\n};\n"
  },
  {
    "path": "renderer/components/ui/dialog.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\n\nimport { cn } from \"@/lib/utils\";\nimport { IconX } from \"@tabler/icons-react\";\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-white/60 backdrop-blur-lg dark:bg-black/60\",\n      className,\n    )}\n    {...props}\n  />\n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"wora-border wora-transition data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-xl translate-x-[-50%] translate-y-[-50%] gap-4 rounded-2xl bg-white/50 p-6 dark:bg-black/50\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <DialogPrimitive.Close className=\"absolute top-4 right-4 rounded-xs opacity-50 outline-hidden transition-opacity hover:opacity-100 disabled:pointer-events-none\">\n        <IconX className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n));\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\"flex flex-col text-center sm:text-left\", className)}\n    {...props}\n  />\n);\nDialogHeader.displayName = \"DialogHeader\";\n\nconst DialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n      className,\n    )}\n    {...props}\n  />\n);\nDialogFooter.displayName = \"DialogFooter\";\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\"text-sm font-medium\", className)}\n    {...props}\n  />\n));\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description\n    ref={ref}\n    className={cn(\"text-xs text-neutral-500 dark:text-neutral-400\", className)}\n    {...props}\n  />\n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogTrigger,\n  DialogClose,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n};\n"
  },
  {
    "path": "renderer/components/ui/form.tsx",
    "content": "import * as React from \"react\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport {\n  Controller,\n  ControllerProps,\n  FieldPath,\n  FieldValues,\n  FormProvider,\n  useFormContext,\n} from \"react-hook-form\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Label } from \"@/components/ui/label\";\n\nconst Form = FormProvider;\n\ntype FormFieldContextValue<\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n> = {\n  name: TName;\n};\n\nconst FormFieldContext = React.createContext<FormFieldContextValue>(\n  {} as FormFieldContextValue,\n);\n\nconst FormField = <\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n>({\n  ...props\n}: ControllerProps<TFieldValues, TName>) => {\n  return (\n    <FormFieldContext.Provider value={{ name: props.name }}>\n      <Controller {...props} />\n    </FormFieldContext.Provider>\n  );\n};\n\nconst useFormField = () => {\n  const fieldContext = React.useContext(FormFieldContext);\n  const itemContext = React.useContext(FormItemContext);\n  const { getFieldState, formState } = useFormContext();\n\n  const fieldState = getFieldState(fieldContext.name, formState);\n\n  if (!fieldContext) {\n    throw new Error(\"useFormField should be used within <FormField>\");\n  }\n\n  const { id } = itemContext;\n\n  return {\n    id,\n    name: fieldContext.name,\n    formItemId: `${id}-form-item`,\n    formDescriptionId: `${id}-form-item-description`,\n    formMessageId: `${id}-form-item-message`,\n    ...fieldState,\n  };\n};\n\ntype FormItemContextValue = {\n  id: string;\n};\n\nconst FormItemContext = React.createContext<FormItemContextValue>(\n  {} as FormItemContextValue,\n);\n\nconst FormItem = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n  const id = React.useId();\n\n  return (\n    <FormItemContext.Provider value={{ id }}>\n      <div ref={ref} className={cn(\"space-y-2\", className)} {...props} />\n    </FormItemContext.Provider>\n  );\n});\nFormItem.displayName = \"FormItem\";\n\nconst FormLabel = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  const { error, formItemId } = useFormField();\n\n  return (\n    <Label\n      ref={ref}\n      className={cn(error && \"text-red-500 dark:text-red-900\", className)}\n      htmlFor={formItemId}\n      {...props}\n    />\n  );\n});\nFormLabel.displayName = \"FormLabel\";\n\nconst FormControl = React.forwardRef<\n  React.ElementRef<typeof Slot>,\n  React.ComponentPropsWithoutRef<typeof Slot>\n>(({ ...props }, ref) => {\n  const { error, formItemId, formDescriptionId, formMessageId } =\n    useFormField();\n\n  return (\n    <Slot\n      ref={ref}\n      id={formItemId}\n      aria-describedby={\n        !error\n          ? `${formDescriptionId}`\n          : `${formDescriptionId} ${formMessageId}`\n      }\n      aria-invalid={!!error}\n      {...props}\n    />\n  );\n});\nFormControl.displayName = \"FormControl\";\n\nconst FormDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => {\n  const { formDescriptionId } = useFormField();\n\n  return (\n    <p\n      ref={ref}\n      id={formDescriptionId}\n      className={cn(\n        \"text-[0.8rem] text-neutral-500 dark:text-neutral-400\",\n        className,\n      )}\n      {...props}\n    />\n  );\n});\nFormDescription.displayName = \"FormDescription\";\n\nconst FormMessage = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, children, ...props }, ref) => {\n  const { error, formMessageId } = useFormField();\n  const body = error ? String(error?.message) : children;\n\n  if (!body) {\n    return null;\n  }\n\n  return (\n    <p\n      ref={ref}\n      id={formMessageId}\n      className={cn(\n        \"text-[0.8rem] font-medium text-red-500 dark:text-red-900\",\n        className,\n      )}\n      {...props}\n    >\n      {body}\n    </p>\n  );\n});\nFormMessage.displayName = \"FormMessage\";\n\nexport {\n  useFormField,\n  Form,\n  FormItem,\n  FormLabel,\n  FormControl,\n  FormDescription,\n  FormMessage,\n  FormField,\n};\n"
  },
  {
    "path": "renderer/components/ui/input.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface InputProps\n  extends React.InputHTMLAttributes<HTMLInputElement> {}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ className, type, ...props }, ref) => {\n    return (\n      <input\n        type={type}\n        className={cn(\n          \"flex h-9 w-full rounded-xl bg-black/5 px-3 py-1 text-xs ring-inset transition duration-300 placeholder:text-black/50 focus:outline-hidden focus:ring-2 focus:ring-black dark:bg-white/10 dark:placeholder:text-white/50 dark:focus:ring-white\",\n          className,\n        )}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nInput.displayName = \"Input\";\n\nexport { Input };\n"
  },
  {
    "path": "renderer/components/ui/label.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst labelVariants = cva(\n  \"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\",\n);\n\nconst Label = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &\n    VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n  <LabelPrimitive.Root\n    ref={ref}\n    className={cn(labelVariants(), className)}\n    {...props}\n  />\n));\nLabel.displayName = LabelPrimitive.Root.displayName;\n\nexport { Label };\n"
  },
  {
    "path": "renderer/components/ui/scroll-area.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\";\nimport { useRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst ScrollArea = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>\n>(({ className, children, ...props }, ref) => {\n  const viewportRef = useRef<HTMLDivElement>(null);\n\n  return (\n    <ScrollAreaPrimitive.Root\n      ref={ref}\n      className={cn(\"relative overflow-hidden\", className)}\n      {...props}\n    >\n      <ScrollAreaPrimitive.Viewport\n        ref={viewportRef}\n        className=\"h-full w-full rounded-2xl\"\n      >\n        {children}\n      </ScrollAreaPrimitive.Viewport>\n      <ScrollBar />\n      <ScrollAreaPrimitive.Corner />\n    </ScrollAreaPrimitive.Root>\n  );\n});\nScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;\n\nconst ScrollBar = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>\n>(({ className, orientation = \"vertical hidden\", ...props }, ref) => (\n  <ScrollAreaPrimitive.ScrollAreaScrollbar\n    ref={ref}\n    orientation={orientation as \"horizontal\" | \"vertical\"}\n    className={cn(\n      \"touch-none select-none transition-colors\",\n      orientation === \"vertical\" && \"h-full w-2 p-px\",\n      orientation === \"horizontal\" && \"h-2 flex-col p-px\",\n      className,\n    )}\n    {...props}\n  >\n    <ScrollAreaPrimitive.ScrollAreaThumb className=\"relative flex-1 rounded-full bg-white/50\" />\n  </ScrollAreaPrimitive.ScrollAreaScrollbar>\n));\nScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;\n\nexport { ScrollArea, ScrollBar };\n"
  },
  {
    "path": "renderer/components/ui/select.tsx",
    "content": "import * as React from \"react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport {\n  IconCheck,\n  IconChevronDown,\n  IconChevronRight,\n} from \"@tabler/icons-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Select = SelectPrimitive.Root;\n\nconst SelectGroup = SelectPrimitive.Group;\n\nconst SelectValue = SelectPrimitive.Value;\n\nconst SelectTrigger = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"wora-transition flex h-9 w-full items-center justify-between rounded-lg bg-black/5 px-3 py-2 text-xs ring-offset-white placeholder:text-neutral-500 focus:outline-hidden focus:ring-1 focus:ring-neutral-950 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-white/10 dark:ring-offset-neutral-950 dark:focus:ring-neutral-300\",\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <SelectPrimitive.Icon asChild>\n      <IconChevronDown className=\"h-4 w-4 opacity-50\" />\n    </SelectPrimitive.Icon>\n  </SelectPrimitive.Trigger>\n));\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName;\n\nconst SelectContent = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n  <SelectPrimitive.Portal>\n    <SelectPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"wora-border relative z-50 min-w-32 overflow-hidden rounded-lg bg-white p-1 text-neutral-950 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:bg-black dark:text-neutral-50\",\n        position === \"popper\" &&\n          \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n        className,\n      )}\n      position={position}\n      {...props}\n    >\n      <SelectPrimitive.Viewport\n        className={cn(\n          \"p-1\",\n          position === \"popper\" &&\n            \"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)\",\n        )}\n      >\n        {children}\n      </SelectPrimitive.Viewport>\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n));\nSelectContent.displayName = SelectPrimitive.Content.displayName;\n\nconst SelectLabel = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label\n    ref={ref}\n    className={cn(\"py-1.5 pl-8 pr-2 text-xs font-medium\", className)}\n    {...props}\n  />\n));\nSelectLabel.displayName = SelectPrimitive.Label.displayName;\n\nconst SelectItem = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"wora-transition relative flex w-full cursor-default select-none items-center rounded-lg py-1.5 pl-8 pr-2 text-xs outline-hidden focus:bg-neutral-100 data-disabled:pointer-events-none data-disabled:opacity-50 dark:focus:bg-neutral-800 dark:focus:text-neutral-50\",\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n      <SelectPrimitive.ItemIndicator>\n        <IconCheck className=\"h-4 w-4\" />\n      </SelectPrimitive.ItemIndicator>\n    </span>\n\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n  </SelectPrimitive.Item>\n));\nSelectItem.displayName = SelectPrimitive.Item.displayName;\n\nconst SelectSeparator = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator\n    ref={ref}\n    className={cn(\n      \"-mx-1 my-1 h-px bg-neutral-100 dark:bg-neutral-800\",\n      className,\n    )}\n    {...props}\n  />\n));\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName;\n\nexport {\n  Select,\n  SelectGroup,\n  SelectValue,\n  SelectTrigger,\n  SelectContent,\n  SelectLabel,\n  SelectItem,\n  SelectSeparator,\n};\n"
  },
  {
    "path": "renderer/components/ui/skeleton.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nfunction Skeleton({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      className={cn(\"animate-pulse rounded-md bg-gray-200 dark:bg-gray-800\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Skeleton };"
  },
  {
    "path": "renderer/components/ui/slider.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SliderPrimitive from \"@radix-ui/react-slider\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Slider = React.forwardRef<\n  React.ElementRef<typeof SliderPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <SliderPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"group/slider wora-transition relative flex w-full cursor-pointer touch-none select-none items-center\",\n      className,\n    )}\n    {...props}\n  >\n    <SliderPrimitive.Track className=\"relative h-1 w-full grow origin-center overflow-hidden rounded-full bg-black/5 duration-300 hover:h-2 active:h-3 dark:bg-white/10\">\n      <SliderPrimitive.Range className=\"absolute h-full rounded-full bg-black duration-300 dark:bg-white\" />\n    </SliderPrimitive.Track>\n  </SliderPrimitive.Root>\n));\nSlider.displayName = SliderPrimitive.Root.displayName;\n\nexport { Slider };\n"
  },
  {
    "path": "renderer/components/ui/songs.tsx",
    "content": "import React, {\n  useEffect,\n  useState,\n  useCallback,\n  memo,\n  useMemo,\n  useRef,\n  forwardRef,\n  useImperativeHandle,\n} from \"react\";\nimport Image from \"next/image\";\nimport Link from \"next/link\";\nimport { FixedSizeList as List } from \"react-window\";\nimport AutoSizer from \"react-virtualized-auto-sizer\";\nimport {\n  ContextMenu,\n  ContextMenuTrigger,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuSub,\n  ContextMenuSubTrigger,\n  ContextMenuSubContent,\n} from \"@/components/ui/context-menu\";\nimport {\n  IconCheck,\n  IconClock,\n  IconHeart,\n  IconPlayerPlay,\n  IconPlus,\n  IconSquare,\n  IconX,\n  IconUser,\n} from \"@tabler/icons-react\";\nimport { convertTime } from \"@/lib/helpers\";\nimport { Song, usePlayer } from \"@/context/playerContext\";\nimport { toast } from \"sonner\";\n\ntype Playlist = {\n  id: number;\n  name: string;\n};\n\ntype SongsProps = {\n  library: Song[];\n  renderAdditionalMenuItems?: (song: Song, index: number) => React.ReactNode;\n  limit?: number;\n  disableScroll?: boolean;\n  onLoadMore?: () => void;\n  hasMore?: boolean;\n  loadingMore?: boolean;\n};\n\n// Skeleton loader for song covers\nconst SongSkeleton = () => (\n  <div className=\"absolute inset-0 h-full w-full animate-pulse bg-neutral-200 dark:bg-neutral-800\" />\n);\n\n// Memoized song item component to prevent unnecessary re-renders\nconst SongItem = memo(\n  ({\n    song,\n    index,\n    handleMusicClick,\n    playNext,\n    addToQueue,\n    playlists,\n    addSongToPlaylist,\n    renderAdditionalMenuItems,\n  }: {\n    song: Song;\n    index: number;\n    handleMusicClick: (index: number) => void;\n    playNext: (song: Song) => void;\n    addToQueue: (song: Song) => void;\n    playlists: Playlist[];\n    addSongToPlaylist: (playlistId: number, songId: number) => void;\n    renderAdditionalMenuItems?: (song: Song, index: number) => React.ReactNode;\n  }) => {\n    const [imgLoaded, setImgLoaded] = useState(false);\n    const { song: currentSong, isPlaying } = usePlayer();\n    const isCurrentSong = currentSong?.id === song.id;\n\n    return (\n      <ContextMenu>\n        <ContextMenuTrigger>\n          <div\n            className={`wora-transition flex w-full cursor-pointer items-center justify-between rounded-xl p-3 hover:bg-black/5 dark:hover:bg-white/10 ${isCurrentSong ? 'bg-black/5 dark:bg-white/5' : ''\n              }`}\n            onClick={() => handleMusicClick(index)}\n          >\n            <div className=\"flex items-center gap-4\">\n              <div className=\"relative h-12 w-12 overflow-hidden rounded-lg shadow-lg transition duration-300\">\n                {!imgLoaded && <SongSkeleton />}\n                <Image\n                  alt={song.album.name}\n                  src={`wora://${song.album.cover}`}\n                  fill\n                  loading=\"lazy\"\n                  className={`object-cover transition-opacity duration-500 ${imgLoaded ? \"opacity-100\" : \"opacity-0\"}`}\n                  quality={10}\n                  sizes=\"48px\"\n                  priority={false}\n                  onLoad={() => setImgLoaded(true)}\n                />\n                {isCurrentSong && (\n                  <div className=\"absolute inset-0 flex items-center rounded-lg justify-center bg-black/20 backdrop-blur-sm\">\n                    <div className=\"flex items-end space-x-0.5\">\n                      <div\n                        className=\"w-0.5 h-2 bg-white rounded-full origin-bottom\"\n                        style={{\n                          animation: isPlaying ? 'music-bar 0.8s ease-in-out infinite' : 'none',\n                          transform: isPlaying ? undefined : 'scaleY(0.15)',\n                          animationDelay: '0ms'\n                        }}\n                      />\n                      <div\n                        className=\"w-0.5 h-3 bg-white rounded-full origin-bottom\"\n                        style={{\n                          animation: isPlaying ? 'music-bar 0.6s ease-in-out infinite' : 'none',\n                          transform: isPlaying ? undefined : 'scaleY(0.1)',\n                          animationDelay: '100ms'\n                        }}\n                      />\n                      <div\n                        className=\"w-0.5 h-1.5 bg-white rounded-full origin-bottom\"\n                        style={{\n                          animation: isPlaying ? 'music-bar 0.9s ease-in-out infinite' : 'none',\n                          transform: isPlaying ? undefined : 'scaleY(0.2)',\n                          animationDelay: '200ms'\n                        }}\n                      />\n                      <div\n                        className=\"w-0.5 h-2.5 bg-white rounded-full origin-bottom\"\n                        style={{\n                          animation: isPlaying ? 'music-bar 0.7s ease-in-out infinite' : 'none',\n                          transform: isPlaying ? undefined : 'scaleY(0.1)',\n                          animationDelay: '150ms'\n                        }}\n                      />\n                    </div>\n                  </div>\n                )}\n              </div>\n              <div className=\"flex flex-col\">\n                <p className={`text-sm font-medium`}>\n                  {song.name}\n                </p>\n                <Link\n                  href={`/artists/${encodeURIComponent(song.artist)}`}\n                  passHref\n                  onClick={(e) => {\n                    e.preventDefault();\n                    e.stopPropagation();\n                    // Use router to navigate without stopping song playback\n                    const router = require(\"next/router\").default;\n                    router.push(`/artists/${encodeURIComponent(song.artist)}`);\n                  }}\n                >\n                  <p className={`cursor-pointer opacity-50 hover:underline hover:opacity-80`}>\n                    {song.artist}\n                  </p>\n                </Link>\n              </div>\n            </div>\n            <div className=\"flex items-center gap-2\">\n              <p className=\"flex items-center gap-1 opacity-50\">\n                <IconClock stroke={2} size={15} />\n                {convertTime(song.duration)}\n              </p>\n            </div>\n          </div>\n        </ContextMenuTrigger>\n        <ContextMenuContent className=\"w-64\">\n          <ContextMenuItem\n            className=\"flex items-center gap-2\"\n            onClick={() => handleMusicClick(index)}\n          >\n            <IconPlayerPlay className=\"fill-white\" stroke={2} size={14} />\n            Play Song\n          </ContextMenuItem>\n          <ContextMenuItem\n            className=\"flex items-center gap-2\"\n            onClick={() => playNext(song)}\n          >\n            <IconSquare stroke={2} size={14} />\n            Play Next\n          </ContextMenuItem>\n          <ContextMenuItem\n            className=\"flex items-center gap-2\"\n            onClick={() => addToQueue(song)}\n          >\n            <IconPlus className=\"fill-white\" stroke={2} size={14} />\n            Add to Queue\n          </ContextMenuItem>\n          <Link href={`/artists/${encodeURIComponent(song.artist)}`}>\n            <ContextMenuItem className=\"flex items-center gap-2\">\n              <IconUser stroke={2} size={14} />\n              Go to Artist\n            </ContextMenuItem>\n          </Link>\n          <ContextMenuSub>\n            <ContextMenuSubTrigger className=\"flex items-center gap-2\">\n              <IconHeart stroke={2} size={14} />\n              Add to Playlist\n            </ContextMenuSubTrigger>\n            <ContextMenuSubContent className=\"w-52\">\n              {playlists.map((playlist) => (\n                <ContextMenuItem\n                  key={playlist.id}\n                  onClick={() => addSongToPlaylist(playlist.id, song.id)}\n                >\n                  <p className=\"w-full mask-r-from-70% text-nowrap\">\n                    {playlist.name}\n                  </p>\n                </ContextMenuItem>\n              ))}\n            </ContextMenuSubContent>\n          </ContextMenuSub>\n          {renderAdditionalMenuItems && renderAdditionalMenuItems(song, index)}\n        </ContextMenuContent>\n      </ContextMenu>\n    );\n  },\n);\n\nSongItem.displayName = \"SongItem\";\n\n// Row renderer for virtualized list\nconst Row = memo(\n  ({\n    data,\n    index,\n    style,\n  }: {\n    data: any;\n    index: number;\n    style: React.CSSProperties;\n  }) => {\n    const {\n      library,\n      handleMusicClick,\n      playNext,\n      addToQueue,\n      playlists,\n      addSongToPlaylist,\n      renderAdditionalMenuItems,\n    } = data;\n    const song = library[index];\n\n    return (\n      <div style={style} className=\"px-0 pb-2\">\n        <SongItem\n          song={song}\n          index={index}\n          handleMusicClick={handleMusicClick}\n          playNext={playNext}\n          addToQueue={addToQueue}\n          playlists={playlists}\n          addSongToPlaylist={addSongToPlaylist}\n          renderAdditionalMenuItems={renderAdditionalMenuItems}\n        />\n      </div>\n    );\n  },\n);\n\nRow.displayName = \"Row\";\n\n// Loading indicator row that appears at the bottom of the list\nconst LoadingRow = memo(({ style }: { style: React.CSSProperties }) => {\n  return (\n    <div style={style} className=\"flex items-center justify-center py-4\">\n      <div className=\"h-5 w-5 animate-spin rounded-full border-2 border-black/10 border-t-black dark:border-white/10 dark:border-t-white\"></div>\n    </div>\n  );\n});\n\nLoadingRow.displayName = \"LoadingRow\";\n\n// Adding imperative handle to expose methods for scrolling\nconst Songs = forwardRef<\n  { scrollToTop: () => void }, // Methods exposed via ref\n  SongsProps\n>(\n  (\n    {\n      library,\n      renderAdditionalMenuItems,\n      limit,\n      disableScroll,\n      onLoadMore,\n      hasMore = false,\n      loadingMore = false,\n    },\n    ref,\n  ) => {\n    const { setQueueAndPlay, playNext, addToQueue } = usePlayer();\n    const [playlists, setPlaylists] = useState<Playlist[]>([]);\n    const containerRef = useRef<HTMLDivElement>(null);\n    const listRef = useRef<any>(null);\n    const isNearBottomRef = useRef(false);\n\n    // Expose methods to parent component via ref\n    useImperativeHandle(ref, () => ({\n      scrollToTop: () => {\n        if (listRef.current) {\n          listRef.current.scrollTo(0);\n        }\n      },\n    }));\n\n    // Load playlists data only once\n    useEffect(() => {\n      window.ipc.invoke(\"getAllPlaylists\").then((response) => {\n        setPlaylists(response);\n      });\n    }, []);\n\n    const handleMusicClick = useCallback(\n      (index: number) => {\n        setQueueAndPlay(library, index);\n      },\n      [library, setQueueAndPlay],\n    );\n\n    const addSongToPlaylist = useCallback(\n      (playlistId: number, songId: number) => {\n        window.ipc\n          .invoke(\"addSongToPlaylist\", {\n            playlistId,\n            songId,\n          })\n          .then((response) => {\n            if (response === true) {\n              toast(\n                <div className=\"flex w-fit items-center gap-2 text-xs\">\n                  <IconCheck className=\"text-green-400\" stroke={2} size={16} />\n                  Song is added to playlist.\n                </div>,\n              );\n            } else {\n              toast(\n                <div className=\"flex w-fit items-center gap-2 text-xs\">\n                  <IconX className=\"text-red-500\" stroke={2} size={16} />\n                  Song already exists in playlist.\n                </div>,\n              );\n            }\n          });\n      },\n      [],\n    );\n\n    // Function to handle scrolling events and trigger loading more content\n    const handleScroll = useCallback(\n      ({ scrollOffset, scrollUpdateWasRequested }) => {\n        if (!onLoadMore || !hasMore || loadingMore || disableScroll) return;\n\n        // Only calculate when this is a user scroll, not a programmatic one\n        if (!scrollUpdateWasRequested && listRef.current) {\n          const list = listRef.current;\n          const clientHeight = list._outerRef.clientHeight;\n          const scrollHeight = list._outerRef.scrollHeight;\n\n          // Trigger loading when we're 200px from the bottom\n          const threshold = 200;\n          const isNearBottom =\n            scrollHeight - scrollOffset - clientHeight < threshold;\n\n          if (isNearBottom && !isNearBottomRef.current) {\n            isNearBottomRef.current = true;\n            onLoadMore();\n          } else if (!isNearBottom && isNearBottomRef.current) {\n            isNearBottomRef.current = false;\n          }\n        }\n      },\n      [onLoadMore, hasMore, loadingMore, disableScroll],\n    );\n\n    // Memoized data for the virtualized list\n    const itemData = useMemo(\n      () => ({\n        library,\n        handleMusicClick,\n        playNext,\n        addToQueue,\n        playlists,\n        addSongToPlaylist,\n        renderAdditionalMenuItems,\n      }),\n      [\n        library,\n        handleMusicClick,\n        playNext,\n        addToQueue,\n        playlists,\n        addSongToPlaylist,\n        renderAdditionalMenuItems,\n      ],\n    );\n\n    // Limit the number of songs if a limit is specified\n    const limitedLibrary = useMemo(() => {\n      if (!library) return [];\n      return limit ? library.slice(0, limit) : library;\n    }, [library, limit]);\n\n    // Calculate the total number of items including the loading indicator\n    const itemCount = useMemo(() => {\n      if (!library) return 0;\n      // Add an extra row for the loading indicator if we have more to load\n      return hasMore && !disableScroll ? library.length + 1 : library.length;\n    }, [library, hasMore, disableScroll]);\n\n    // Row renderer that handles both song items and the loading indicator\n    const rowRenderer = useCallback(\n      (props) => {\n        const { index, style } = props;\n\n        // If this is the last item and we have more to load, show loading indicator\n        if (index === library.length && hasMore) {\n          return <LoadingRow style={style} />;\n        }\n\n        // Otherwise, render a regular song row\n        return <Row {...props} />;\n      },\n      [library, hasMore],\n    );\n\n    // If disableScroll is true, render static song items instead of virtualized list\n    if (disableScroll) {\n      return (\n        <div className=\"relative flex w-full flex-col\" ref={containerRef}>\n          {limitedLibrary && limitedLibrary.length > 0 ? (\n            <div className=\"space-y-1\">\n              {limitedLibrary.map((song, index) => (\n                <SongItem\n                  key={song.id}\n                  song={song}\n                  index={index}\n                  handleMusicClick={handleMusicClick}\n                  playNext={playNext}\n                  addToQueue={addToQueue}\n                  playlists={playlists}\n                  addSongToPlaylist={addSongToPlaylist}\n                  renderAdditionalMenuItems={renderAdditionalMenuItems}\n                />\n              ))}\n            </div>\n          ) : (\n            <div className=\"flex items-center justify-center p-10 text-gray-500\">\n              No songs found\n            </div>\n          )}\n        </div>\n      );\n    }\n\n    // Default behavior: render virtualized list with scrolling\n    return (\n      <div\n        className=\"relative flex h-[calc(100vh-350px)] w-full flex-col\"\n        ref={containerRef}\n      >\n        {library && library.length > 0 ? (\n          <AutoSizer>\n            {({ height, width }) => (\n              <List\n                height={height || 600}\n                width={width || \"100%\"}\n                itemCount={itemCount}\n                itemSize={80}\n                itemData={itemData}\n                className=\"no-scrollbar\"\n                ref={listRef}\n                overscanCount={5} // Render more items above and below the visible area\n                onScroll={handleScroll}\n              >\n                {rowRenderer}\n              </List>\n            )}\n          </AutoSizer>\n        ) : (\n          <div className=\"flex items-center justify-center p-10 text-gray-500\">\n            No songs found\n          </div>\n        )}\n      </div>\n    );\n  },\n);\n\nSongs.displayName = \"Songs\";\n\nexport default memo(Songs);\n"
  },
  {
    "path": "renderer/components/ui/sonner.tsx",
    "content": "\"use client\";\n\nimport { useTheme } from \"next-themes\";\nimport { Toaster as Sonner } from \"sonner\";\n\ntype ToasterProps = React.ComponentProps<typeof Sonner>;\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n  const { theme = \"system\" } = useTheme();\n\n  return (\n    <Sonner\n      theme={theme as ToasterProps[\"theme\"]}\n      className=\"toaster group mx-4 my-8\"\n      toastOptions={{\n        classNames: {\n          toast:\n            \"font-sans py-3 px-4 group toast group-[.toaster]:bg-white/70 group-[.toaster]:shadow-lg dark:group-[.toaster]:bg-black/30 group-[.toaster]:backdrop-blur-xl rounded-2xl wora-border group-[.toaster]:text-black dark:group-[.toaster]:text-white\",\n          description:\n            \"group-[.toast]:text-black/50 dark:group-[.toast]:text-white/50\",\n          actionButton:\n            \"group-[.toast]:bg-neutral-900 group-[.toast]:text-neutral-50 dark:group-[.toast]:bg-neutral-50 dark:group-[.toast]:text-neutral-900\",\n          cancelButton:\n            \"group-[.toast]:bg-neutral-100 group-[.toast]:text-neutral-500 dark:group-[.toast]:bg-neutral-800 dark:group-[.toast]:text-neutral-400\",\n        },\n      }}\n      {...props}\n    />\n  );\n};\n\nexport { Toaster };\n"
  },
  {
    "path": "renderer/components/ui/spinner.tsx",
    "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/utils\"; // Ensure the path to cn utility is correct\n\nexport interface SpinnerProps extends React.HTMLAttributes<SVGElement> {}\n\nconst Spinner = React.forwardRef<SVGSVGElement, SpinnerProps>(\n  ({ className, ...props }, ref) => {\n    return (\n      <div role=\"status\">\n        <svg\n          aria-hidden=\"true\"\n          className={cn(\n            \"animate-spin fill-black text-black/10 duration-700 dark:fill-white dark:text-white/10\",\n            className,\n          )}\n          viewBox=\"0 0 100 101\"\n          fill=\"none\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          ref={ref}\n          {...props}\n        >\n          <path\n            d=\"M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z\"\n            fill=\"currentColor\"\n          />\n          <path\n            d=\"M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z\"\n            fill=\"currentFill\"\n          />\n        </svg>\n      </div>\n    );\n  },\n);\n\nexport default Spinner;\n"
  },
  {
    "path": "renderer/components/ui/switch.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Switch = React.forwardRef<\n  React.ElementRef<typeof SwitchPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>\n>(({ className, ...props }, ref) => (\n  <SwitchPrimitives.Root\n    className={cn(\n      \"focus-visible:ring-ring focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-xs transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n      className,\n    )}\n    {...props}\n    ref={ref}\n  >\n    <SwitchPrimitives.Thumb\n      className={cn(\n        \"bg-background pointer-events-none block h-4 w-4 rounded-full shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0\",\n      )}\n    />\n  </SwitchPrimitives.Root>\n));\nSwitch.displayName = SwitchPrimitives.Root.displayName;\n\nexport { Switch };\n"
  },
  {
    "path": "renderer/components/ui/tabs.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Tabs = TabsPrimitive.Root;\n\nconst TabsList = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.List\n    ref={ref}\n    className={cn(\n      \"inline-flex min-h-9 items-center justify-center rounded-lg bg-black/5 p-1 dark:bg-white/10\",\n      className,\n    )}\n    {...props}\n  />\n));\nTabsList.displayName = TabsPrimitive.List.displayName;\n\nconst TabsTrigger = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"wora-transition inline-flex items-center justify-center whitespace-nowrap rounded-lg px-3 py-1.5 text-xs font-medium ring-offset-white focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white/70 data-[state=active]:text-black data-[state=active]:shadow-sm dark:data-[state=active]:bg-black/30 dark:data-[state=active]:text-white\",\n      className,\n    )}\n    {...props}\n  />\n));\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName;\n\nconst TabsContent = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Content\n    ref={ref}\n    className={cn(\n      \"mt-2 ring-offset-white focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-300\",\n      className,\n    )}\n    {...props}\n  />\n));\nTabsContent.displayName = TabsPrimitive.Content.displayName;\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent };\n"
  },
  {
    "path": "renderer/components/ui/tooltip.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst TooltipProvider = TooltipPrimitive.Provider;\n\nconst Tooltip = TooltipPrimitive.Root;\n\nconst TooltipTrigger = TooltipPrimitive.Trigger;\n\nconst TooltipContent = React.forwardRef<\n  React.ElementRef<typeof TooltipPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <TooltipPrimitive.Content\n    ref={ref}\n    sideOffset={sideOffset}\n    className={cn(\n      \"wora-border z-50 rounded-lg bg-white/70 px-3 py-1.5 shadow-sm backdrop-blur-xl animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:bg-black/70\",\n      className,\n    )}\n    {...props}\n  />\n));\nTooltipContent.displayName = TooltipPrimitive.Content.displayName;\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };\n"
  },
  {
    "path": "renderer/context/playerContext.tsx",
    "content": "import { shuffleArray } from \"@/lib/helpers\";\nimport React, {\n  createContext,\n  useState,\n  useContext,\n  ReactNode,\n  useEffect,\n  useCallback,\n  useMemo,\n} from \"react\";\n\nexport interface Song {\n  id: number;\n  name: string;\n  artist: string;\n  duration: number;\n  filePath: string;\n  album: {\n    id: number;\n    name: string;\n    artist: string;\n    cover: string;\n  };\n}\n\ninterface PlayerState {\n  song: Song | null;\n  queue: Song[];\n  originalQueue: Song[];\n  history: Song[];\n  currentIndex: number;\n  repeat: boolean;\n  shuffle: boolean;\n  isPlaying: boolean;\n}\n\ninterface PlayerContextType extends PlayerState {\n  setSong: (song: Song) => void;\n  setQueueAndPlay: (\n    songs: Song[],\n    startIndex?: number,\n    shuffle?: boolean,\n  ) => void;\n  nextSong: () => void;\n  previousSong: () => void;\n  toggleRepeat: () => void;\n  toggleShuffle: () => void;\n  playNext: (song: Song) => void;\n  addToQueue: (song: Song) => void;\n  jumpToSong: (songIndex: number) => void;\n  setIsPlaying: (isPlaying: boolean) => void;\n}\n\nconst initialPlayerState: PlayerState = {\n  song: null,\n  queue: [],\n  originalQueue: [],\n  history: [],\n  currentIndex: 0,\n  repeat: false,\n  shuffle: false,\n  isPlaying: false,\n};\n\n// Helper to safely access localStorage (only in browser)\nconst getStorageItem = (key: string): string | null => {\n  if (typeof window !== \"undefined\") {\n    return localStorage.getItem(key);\n  }\n  return null;\n};\n\nconst setStorageItem = (key: string, value: string): void => {\n  if (typeof window !== \"undefined\") {\n    localStorage.setItem(key, value);\n  }\n};\n\nconst PlayerContext = createContext<PlayerContextType | undefined>(undefined);\n\n// Cache for song lookups to avoid repeated array searches\nconst songCache = new Map<number, Song>();\n\n// Helper function to efficiently find song index by ID\nfunction findSongIndexById(songs: Song[], id: number): number {\n  for (let i = 0; i < songs.length; i++) {\n    if (songs[i].id === id) return i;\n  }\n  return -1;\n}\n\nexport const PlayerProvider = ({ children }: { children: ReactNode }) => {\n  const [playerState, setPlayerState] = useState<PlayerState>(() => {\n    // Initialize state with stored preferences - wrapped in a function for lazy initial state\n    const savedRepeat = getStorageItem(\"repeat\");\n    const savedShuffle = getStorageItem(\"shuffle\");\n\n    return {\n      ...initialPlayerState,\n      repeat: savedRepeat ? JSON.parse(savedRepeat) : false,\n      shuffle: savedShuffle ? JSON.parse(savedShuffle) : false,\n    };\n  });\n\n  // Save preferences when they change but batch the saves to reduce writes\n  useEffect(() => {\n    // Only run in browser environment\n    if (typeof window === \"undefined\") return;\n\n    const savePreferences = () => {\n      setStorageItem(\"repeat\", JSON.stringify(playerState.repeat));\n      setStorageItem(\"shuffle\", JSON.stringify(playerState.shuffle));\n    };\n\n    const timeoutId = setTimeout(savePreferences, 300);\n    return () => clearTimeout(timeoutId);\n  }, [playerState.repeat, playerState.shuffle]);\n\n  // Clear song cache when component unmounts\n  useEffect(() => {\n    return () => {\n      songCache.clear();\n    };\n  }, []);\n\n  const setQueueAndPlay = useCallback(\n    (songs: Song[], startIndex: number = 0, shuffle: boolean = false) => {\n      // Update cache with new songs for faster lookups\n      songs.forEach((song) => {\n        songCache.set(song.id, song);\n      });\n\n      // Ensure all songs have proper album data with covers preserved\n      const processedSongs = songs.map((song) => {\n        // Make sure album is defined\n        const album = song.album || {\n          id: null,\n          name: \"Unknown Album\",\n          artist: \"Unknown Artist\",\n          cover: null,\n        };\n\n        return {\n          ...song,\n          album: {\n            ...album,\n            // Ensure cover path is properly formatted\n            cover: album.cover || null,\n          },\n        };\n      });\n\n      let shuffledQueue = [...processedSongs];\n      if (shuffle) {\n        shuffledQueue = shuffleArray([...processedSongs]);\n      }\n\n      setPlayerState({\n        ...initialPlayerState,\n        queue: shuffledQueue,\n        originalQueue: processedSongs,\n        currentIndex: startIndex,\n        song: shuffledQueue[startIndex],\n        shuffle,\n      });\n    },\n    [],\n  );\n\n  const nextSong = useCallback(() => {\n    setPlayerState((prevState) => {\n      const { currentIndex, queue, repeat, history } = prevState;\n\n      if (repeat) {\n        // Just replay current song without state change\n        return { ...prevState };\n      } else {\n        const nextIndex = currentIndex + 1;\n        if (nextIndex < queue.length) {\n          // Add current song to history and move to next\n          return {\n            ...prevState,\n            currentIndex: nextIndex,\n            history:\n              currentIndex >= 0 && queue[currentIndex]\n                ? [...history, queue[currentIndex]]\n                : history,\n            song: queue[nextIndex],\n          };\n        }\n        return prevState; // No more songs in queue\n      }\n    });\n  }, []);\n\n  const previousSong = useCallback(() => {\n    setPlayerState((prevState) => {\n      const { queue, repeat, history } = prevState;\n\n      if (repeat) {\n        // Just replay current song without state change\n        return { ...prevState };\n      } else if (history.length > 0) {\n        // Get last song from history\n        const previous = history[history.length - 1];\n        // Find index efficiently using ID instead of indexOf (which is O(n))\n        const prevIndex = findSongIndexById(queue, previous.id);\n\n        return {\n          ...prevState,\n          history: history.slice(0, -1),\n          song: previous,\n          currentIndex: prevIndex >= 0 ? prevIndex : prevState.currentIndex,\n        };\n      }\n      return prevState;\n    });\n  }, []);\n\n  const toggleRepeat = useCallback(() => {\n    setPlayerState((prevState) => ({\n      ...prevState,\n      repeat: !prevState.repeat,\n      // Disable shuffle if turning on repeat\n      shuffle: !prevState.repeat ? false : prevState.shuffle,\n    }));\n  }, []);\n\n  const toggleShuffle = useCallback(() => {\n    setPlayerState((prevState) => {\n      const newShuffle = !prevState.shuffle;\n      const currentSong = prevState.song;\n\n      if (!currentSong) return prevState;\n\n      let newQueue;\n      let newIndex;\n\n      if (newShuffle) {\n        // Create a new shuffled queue but keep current song as first\n        const otherSongs = prevState.originalQueue.filter(\n          (song) => song.id !== currentSong.id,\n        );\n\n        // Ensure all songs have complete album data including cover paths\n        const songWithCompleteData = {\n          ...currentSong,\n          album: {\n            ...currentSong.album,\n            cover: currentSong.album?.cover || null,\n          },\n        };\n\n        // Make sure each shuffled song has its complete album data\n        const shuffledOtherSongs = shuffleArray([...otherSongs]).map(\n          (song) => ({\n            ...song,\n            album: {\n              ...song.album,\n              cover: song.album?.cover || null,\n            },\n          }),\n        );\n\n        newQueue = [songWithCompleteData, ...shuffledOtherSongs];\n        newIndex = 0; // Current song is now first\n      } else {\n        // Restore original queue with complete album data\n        newQueue = prevState.originalQueue.map((song) => ({\n          ...song,\n          album: {\n            ...song.album,\n            cover: song.album?.cover || null,\n          },\n        }));\n\n        // Find index efficiently using ID\n        newIndex = findSongIndexById(newQueue, currentSong.id);\n        if (newIndex < 0) newIndex = 0;\n      }\n\n      return {\n        ...prevState,\n        shuffle: newShuffle,\n        queue: newQueue,\n        currentIndex: newIndex,\n        // Disable repeat if enabling shuffle\n        repeat: newShuffle ? false : prevState.repeat,\n      };\n    });\n  }, []);\n\n  const playNext = useCallback((song: Song) => {\n    // Add to cache for faster lookups\n    songCache.set(song.id, song);\n\n    // Ensure song has complete album data\n    const songWithCompleteData = {\n      ...song,\n      album: {\n        ...song.album,\n        cover: song.album?.cover || null,\n      },\n    };\n\n    setPlayerState((prevState) => {\n      const { currentIndex, queue, originalQueue } = prevState;\n      const insertIndex = currentIndex + 1;\n\n      // Insert efficiently without creating unnecessary copies\n      const newQueue = [\n        ...queue.slice(0, insertIndex),\n        songWithCompleteData,\n        ...queue.slice(insertIndex),\n      ];\n\n      const originalInsertIndex =\n        findSongIndexById(\n          originalQueue,\n          currentIndex >= 0 && currentIndex < queue.length\n            ? queue[currentIndex].id\n            : -1,\n        ) + 1;\n\n      const newOriginalQueue = [\n        ...originalQueue.slice(0, originalInsertIndex),\n        songWithCompleteData,\n        ...originalQueue.slice(originalInsertIndex),\n      ];\n\n      return {\n        ...prevState,\n        queue: newQueue,\n        originalQueue: newOriginalQueue,\n      };\n    });\n  }, []);\n\n  const addToQueue = useCallback((song: Song) => {\n    // Add to cache for faster lookups\n    songCache.set(song.id, song);\n\n    // Ensure song has complete album data\n    const songWithCompleteData = {\n      ...song,\n      album: {\n        ...song.album,\n        cover: song.album?.cover || null,\n      },\n    };\n\n    setPlayerState((prevState) => ({\n      ...prevState,\n      queue: [...prevState.queue, songWithCompleteData],\n      originalQueue: [...prevState.originalQueue, songWithCompleteData],\n    }));\n  }, []);\n\n  const jumpToSong = useCallback((songIndex: number) => {\n    setPlayerState((prevState) => {\n      const { queue, currentIndex, song: currentSong, history } = prevState;\n\n      if (songIndex < 0 || songIndex >= queue.length) {\n        return prevState; // Invalid index\n      }\n\n      // Add current song to history if we have one and we're jumping to a different song\n      const newHistory = currentSong && currentIndex !== songIndex\n        ? [...history, currentSong]\n        : history;\n\n      return {\n        ...prevState,\n        currentIndex: songIndex,\n        song: queue[songIndex],\n        history: newHistory,\n      };\n    });\n  }, []);\n\n  // Memoize context value to prevent unnecessary re-renders\n  const contextValue = useMemo<PlayerContextType>(\n    () => ({\n      ...playerState,\n      setSong: (song: Song) => {\n        songCache.set(song.id, song);\n\n        // Ensure song has complete album data\n        const songWithCompleteData = {\n          ...song,\n          album: {\n            ...song.album,\n            cover: song.album?.cover || null,\n          },\n        };\n\n        setPlayerState((prev) => ({ ...prev, song: songWithCompleteData }));\n      },\n      setQueueAndPlay,\n      nextSong,\n      previousSong,\n      toggleRepeat,\n      toggleShuffle,\n      playNext,\n      addToQueue,\n      jumpToSong,\n      setIsPlaying: (isPlaying: boolean) => {\n        setPlayerState((prev) => ({ ...prev, isPlaying }));\n      },\n    }),\n    [\n      playerState,\n      setQueueAndPlay,\n      nextSong,\n      previousSong,\n      toggleRepeat,\n      toggleShuffle,\n      playNext,\n      addToQueue,\n      jumpToSong,\n    ],\n  );\n\n  return (\n    <PlayerContext.Provider value={contextValue}>\n      {children}\n    </PlayerContext.Provider>\n  );\n};\n\nexport const usePlayer = (): PlayerContextType => {\n  const context = useContext(PlayerContext);\n  if (!context) {\n    throw new Error(\"usePlayer must be used within a PlayerProvider\");\n  }\n  return context;\n};\n"
  },
  {
    "path": "renderer/hooks/useDebounce.ts",
    "content": "import { useEffect, useState } from 'react';\n\nexport function useDebounce<T>(value: T, delay: number = 300): T {\n  const [debouncedValue, setDebouncedValue] = useState<T>(value);\n\n  useEffect(() => {\n    const handler = setTimeout(() => {\n      setDebouncedValue(value);\n    }, delay);\n\n    return () => {\n      clearTimeout(handler);\n    };\n  }, [value, delay]);\n\n  return debouncedValue;\n}"
  },
  {
    "path": "renderer/hooks/useScrollAreaRestoration.ts",
    "content": "import { useEffect, useRef } from 'react';\nimport { useRouter } from 'next/router';\n\nconst DEBUG = false;\nconst MAX_RESTORE_ATTEMPTS = 10;\nconst RESTORE_ATTEMPT_DELAY = 100; // ms\nconst MAX_RESTORE_TIME = 2000; // 2 seconds max timeout\n\nexport function useScrollAreaRestoration(key: string) {\n  const router = useRouter();\n  const scrollPositions = useRef<{ [path: string]: number }>({});\n  const isRestoring = useRef(false);\n  const lastKnownScrollTop = useRef(0);\n  const restoreTimeoutRef = useRef<NodeJS.Timeout | null>(null);\n\n  useEffect(() => {\n    const saved = sessionStorage.getItem(`scrollAreaPositions_${key}`);\n    if (saved) {\n      try {\n        scrollPositions.current = JSON.parse(saved);\n      } catch (e) {\n        console.error('Failed to parse scroll positions:', e);\n      }\n    }\n\n    const findScrollViewport = (): HTMLElement | null => {\n      return document.querySelector('[data-radix-scroll-area-viewport]');\n    };\n\n    let scrollViewport: HTMLElement | null = null;\n\n    const handleScroll = (e: Event): void => {\n      if (!isRestoring.current && e.target) {\n        const scrollTop = (e.target as HTMLElement).scrollTop;\n        lastKnownScrollTop.current = scrollTop;\n        scrollPositions.current[router.asPath] = scrollTop;\n      }\n    };\n\n    const handleRouteChangeStart = (url: string): void => {\n      const viewport = findScrollViewport();\n      const scrollTop = viewport?.scrollTop ?? lastKnownScrollTop.current;\n      \n      if (DEBUG) {\n        console.log(`[${key}] Saving position for ${router.asPath}: ${scrollTop}`);\n      }\n      \n      scrollPositions.current[router.asPath] = scrollTop;\n      sessionStorage.setItem(\n        `scrollAreaPositions_${key}`,\n        JSON.stringify(scrollPositions.current)\n      );\n    };\n\n    const handleRouteChangeComplete = (url: string): void => {\n      const savedPosition = scrollPositions.current[url];\n      \n      if (DEBUG) {\n        console.log(`[${key}] Route complete to ${url}, saved position: ${savedPosition}`);\n      }\n      \n      if (savedPosition && !isRestoring.current) {\n        isRestoring.current = true;\n\n        if (restoreTimeoutRef.current) {\n          clearTimeout(restoreTimeoutRef.current);\n        }\n        restoreTimeoutRef.current = setTimeout(() => {\n          if (DEBUG) {\n            console.log(`[${key}] Max restore time reached, aborting restore`);\n          }\n          isRestoring.current = false;\n        }, MAX_RESTORE_TIME);\n\n        const attemptRestore = (attempts = 0): void => {\n          if (!isRestoring.current) return;\n\n          if (attempts > MAX_RESTORE_ATTEMPTS) {\n            if (DEBUG) {\n              console.log(`[${key}] Max attempts reached, aborting restore`);\n            }\n            isRestoring.current = false;\n            if (restoreTimeoutRef.current) {\n              clearTimeout(restoreTimeoutRef.current);\n            }\n            return;\n          }\n\n          const viewport = findScrollViewport();\n          if (viewport) {\n            viewport.scrollTop = savedPosition;\n\n            setTimeout(() => {\n              const actualScroll = viewport.scrollTop;\n\n              if (Math.abs(actualScroll - savedPosition) > 50) {\n                viewport.scrollTop = savedPosition;\n\n                setTimeout(() => {\n                  isRestoring.current = false;\n                  lastKnownScrollTop.current = viewport.scrollTop;\n                  if (restoreTimeoutRef.current) {\n                    clearTimeout(restoreTimeoutRef.current);\n                  }\n                }, 50);\n              } else {\n                isRestoring.current = false;\n                lastKnownScrollTop.current = actualScroll;\n                if (restoreTimeoutRef.current) {\n                  clearTimeout(restoreTimeoutRef.current);\n                }\n              }\n            }, 50);\n          } else {\n            setTimeout(() => attemptRestore(attempts + 1), RESTORE_ATTEMPT_DELAY);\n          }\n        };\n\n        setTimeout(() => attemptRestore(), RESTORE_ATTEMPT_DELAY);\n      }\n    };\n\n    const handlePopState = (): void => {\n      setTimeout(() => {\n        const position = scrollPositions.current[router.asPath];\n        if (position > 0) {\n          const viewport = findScrollViewport();\n          if (viewport) {\n            viewport.scrollTop = position;\n          }\n        }\n      }, 100);\n    };\n\n    const setupViewportListener = (): void => {\n      scrollViewport = findScrollViewport();\n      if (scrollViewport) {\n        scrollViewport.addEventListener('scroll', handleScroll, { passive: true });\n        \n        const currentPosition = scrollPositions.current[router.asPath];\n        if (currentPosition > 0) {\n          requestAnimationFrame(() => {\n            if (scrollViewport) {\n              scrollViewport.scrollTop = currentPosition;\n            }\n          });\n        }\n      } else {\n        setTimeout(setupViewportListener, 100);\n      }\n    };\n\n    setupViewportListener();\n    \n    router.events.on('routeChangeStart', handleRouteChangeStart);\n    router.events.on('routeChangeComplete', handleRouteChangeComplete);\n    window.addEventListener('popstate', handlePopState);\n\n    return () => {\n      if (scrollViewport) {\n        scrollViewport.removeEventListener('scroll', handleScroll);\n      }\n      if (restoreTimeoutRef.current) {\n        clearTimeout(restoreTimeoutRef.current);\n      }\n      router.events.off('routeChangeStart', handleRouteChangeStart);\n      router.events.off('routeChangeComplete', handleRouteChangeComplete);\n      window.removeEventListener('popstate', handlePopState);\n    };\n  }, [router, key]);\n\n  const saveScrollPosition = (): void => {\n    const viewport = document.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement;\n    if (viewport) {\n      scrollPositions.current[router.asPath] = viewport.scrollTop;\n      sessionStorage.setItem(\n        `scrollAreaPositions_${key}`,\n        JSON.stringify(scrollPositions.current)\n      );\n    }\n  };\n\n  return { saveScrollPosition };\n}"
  },
  {
    "path": "renderer/lib/albumCache.ts",
    "content": "// Global album cache to persist album data between page navigations\ninterface Album {\n  id: number;\n  name: string;\n  artist: string;\n  cover: string | null;\n  year: number | null;\n  duration?: number; // Adding optional duration property\n}\n\n// Define the type for our global album cache\ninterface AlbumCacheStore {\n  allAlbums: Album[];\n  filteredAlbums: Album[];\n  searchResults: Album[];\n  lastSearchQuery: string;\n  sortBy: string;\n  sortOrder: string;\n  viewMode: \"grid\" | \"compact-grid\" | \"list\";\n  page: number;\n  hasMore: boolean;\n  lastFetchTime: number; // To determine if cache is stale\n  isInitialized: boolean;\n  albumsWithSongs: Record<number, any>; // Cache for albums with songs\n}\n\n// Default initial state\nconst initialState: AlbumCacheStore = {\n  allAlbums: [],\n  filteredAlbums: [],\n  searchResults: [],\n  lastSearchQuery: \"\",\n  sortBy: \"name\",\n  sortOrder: \"asc\",\n  viewMode: \"grid\",\n  page: 1,\n  hasMore: true,\n  lastFetchTime: 0,\n  isInitialized: false,\n  albumsWithSongs: {},\n};\n\n// Cache invalidation interval (5 minutes)\nconst CACHE_TTL = 5 * 60 * 1000;\n\nclass AlbumCache {\n  private static instance: AlbumCache;\n  private store: AlbumCacheStore = { ...initialState };\n\n  private constructor() {\n    // Initialize with saved cache if available\n    if (typeof window !== \"undefined\") {\n      const savedCache = localStorage.getItem(\"albumCache\");\n      if (savedCache) {\n        try {\n          const parsedCache = JSON.parse(savedCache);\n          // Only restore if the cache isn't stale\n          if (Date.now() - parsedCache.lastFetchTime < CACHE_TTL) {\n            this.store = parsedCache;\n          }\n        } catch (error) {\n          console.error(\"Error parsing album cache from localStorage:\", error);\n        }\n      }\n    }\n  }\n\n  // Get singleton instance\n  public static getInstance(): AlbumCache {\n    if (!AlbumCache.instance) {\n      AlbumCache.instance = new AlbumCache();\n    }\n    return AlbumCache.instance;\n  }\n\n  // Function to sort albums\n  public sortAlbums(\n    albums: Album[],\n    sortBy: string,\n    sortOrder: string,\n  ): Album[] {\n    return [...albums].sort((a, b) => {\n      let comparison = 0;\n\n      switch (sortBy) {\n        case \"name\":\n          comparison = a.name.localeCompare(b.name);\n          break;\n        case \"artist\":\n          comparison = a.artist.localeCompare(b.artist);\n          break;\n        case \"year\":\n          // Handle null years by considering them as \"0\" for sorting purposes\n          const yearA = a.year || 0;\n          const yearB = b.year || 0;\n          comparison = yearA - yearB;\n          break;\n        case \"duration\":\n          // First check if the album objects have duration properties\n          if (a.duration && b.duration) {\n            comparison = a.duration - b.duration;\n            break;\n          }\n\n          // Fall back to calculating duration from songs if not available directly\n          // Get album durations from cache\n          const albumWithSongsA = this.store.albumsWithSongs[a.id];\n          const albumWithSongsB = this.store.albumsWithSongs[b.id];\n\n          // Calculate total duration for each album\n          const durationA =\n            a.duration ||\n            albumWithSongsA?.songs?.reduce(\n              (total, song) => total + (song.duration || 0),\n              0,\n            ) ||\n            0;\n          const durationB =\n            b.duration ||\n            albumWithSongsB?.songs?.reduce(\n              (total, song) => total + (song.duration || 0),\n              0,\n            ) ||\n            0;\n\n          comparison = durationA - durationB;\n          break;\n        default:\n          comparison = a.name.localeCompare(b.name);\n      }\n\n      return sortOrder === \"asc\" ? comparison : -comparison;\n    });\n  }\n\n  // Get all albums\n  public getAllAlbums(): Album[] {\n    return this.store.allAlbums;\n  }\n\n  // Get filtered albums\n  public getFilteredAlbums(): Album[] {\n    return this.store.filteredAlbums;\n  }\n\n  // Get search results\n  public getSearchResults(): Album[] {\n    return this.store.searchResults;\n  }\n\n  // Get current pagination page\n  public getPage(): number {\n    return this.store.page;\n  }\n\n  // Check if cache has been initialized\n  public isInitialized(): boolean {\n    return this.store.isInitialized;\n  }\n\n  // Check if there are more albums to load\n  public hasMore(): boolean {\n    return this.store.hasMore;\n  }\n\n  // Get current sort settings\n  public getSortSettings(): { sortBy: string; sortOrder: string } {\n    return {\n      sortBy: this.store.sortBy,\n      sortOrder: this.store.sortOrder,\n    };\n  }\n\n  // Get current view mode\n  public getViewMode(): \"grid\" | \"compact-grid\" | \"list\" {\n    return this.store.viewMode;\n  }\n\n  // Get last search query\n  public getLastSearchQuery(): string {\n    return this.store.lastSearchQuery;\n  }\n\n  // Check if the cache is stale\n  public isStale(): boolean {\n    return Date.now() - this.store.lastFetchTime > CACHE_TTL;\n  }\n\n  // Set all albums\n  public setAllAlbums(albums: Album[]): void {\n    this.store.allAlbums = albums;\n    this.store.lastFetchTime = Date.now();\n    this.saveToLocalStorage();\n  }\n\n  // Add more albums to the existing collection (for pagination)\n  public addAlbums(newAlbums: Album[]): void {\n    // Filter out duplicates based on album ID\n    const existingIds = new Set(this.store.allAlbums.map((album) => album.id));\n    const uniqueNewAlbums = newAlbums.filter(\n      (album) => !existingIds.has(album.id),\n    );\n\n    this.store.allAlbums = [...this.store.allAlbums, ...uniqueNewAlbums];\n    this.store.lastFetchTime = Date.now();\n    this.saveToLocalStorage();\n  }\n\n  // Set filtered albums\n  public setFilteredAlbums(albums: Album[]): void {\n    this.store.filteredAlbums = albums;\n    this.saveToLocalStorage();\n  }\n\n  // Set search results\n  public setSearchResults(albums: Album[], query: string): void {\n    this.store.searchResults = albums;\n    this.store.lastSearchQuery = query;\n    this.saveToLocalStorage();\n  }\n\n  // Update pagination state\n  public updatePagination(page: number, hasMore: boolean): void {\n    this.store.page = page;\n    this.store.hasMore = hasMore;\n    this.saveToLocalStorage();\n  }\n\n  // Update sort settings\n  public updateSortSettings(sortBy: string, sortOrder: string): void {\n    this.store.sortBy = sortBy;\n    this.store.sortOrder = sortOrder;\n    this.saveToLocalStorage();\n  }\n\n  // Update view mode\n  public updateViewMode(viewMode: \"grid\" | \"compact-grid\" | \"list\"): void {\n    this.store.viewMode = viewMode;\n    this.saveToLocalStorage();\n  }\n\n  // Get album with songs (for duration calculation)\n  public async getAlbumWithSongs(albumId: number): Promise<any> {\n    if (this.store.albumsWithSongs[albumId]) {\n      return this.store.albumsWithSongs[albumId];\n    }\n\n    try {\n      // Fetch album with songs from the main process\n      const albumWithSongs = await window.ipc.invoke(\n        \"getAlbumWithSongs\",\n        albumId,\n      );\n\n      // Cache the result\n      this.store.albumsWithSongs[albumId] = albumWithSongs;\n      this.saveToLocalStorage();\n\n      return albumWithSongs;\n    } catch (error) {\n      console.error(\n        `Error fetching album with songs for ID ${albumId}:`,\n        error,\n      );\n      return null;\n    }\n  }\n\n  // Mark cache as initialized\n  public setInitialized(): void {\n    this.store.isInitialized = true;\n    this.saveToLocalStorage();\n  }\n\n  // Reset cache\n  public resetCache(): void {\n    this.store = { ...initialState };\n    if (typeof window !== \"undefined\") {\n      localStorage.removeItem(\"albumCache\");\n    }\n  }\n\n  // Reset state with specific values (for page resets)\n  public resetState(resetData: Partial<AlbumCacheStore>): void {\n    // Apply default values from initial state and then override with any provided resetData\n    const currentAlbums = this.store.allAlbums; // Keep the albums data\n\n    // Reset specific fields while preserving album data\n    this.store = {\n      ...initialState,\n      allAlbums: currentAlbums,\n      filteredAlbums: currentAlbums,\n      viewMode: resetData.viewMode || initialState.viewMode,\n      sortBy: resetData.sortBy || initialState.sortBy,\n      sortOrder: resetData.sortOrder || initialState.sortOrder,\n      page: resetData.page || initialState.page,\n      lastFetchTime: Date.now(),\n      isInitialized: true, // Keep initialized status\n      albumsWithSongs: this.store.albumsWithSongs, // Keep album details\n    };\n\n    // If reset includes specific sort options, apply sorting to filtered albums\n    if (currentAlbums.length > 0) {\n      this.store.filteredAlbums = this.sortAlbums(\n        currentAlbums,\n        this.store.sortBy,\n        this.store.sortOrder,\n      );\n    }\n\n    // Clear localStorage to ensure values are truly reset\n    if (typeof window !== \"undefined\") {\n      localStorage.removeItem(\"albumCache\");\n      // Save the new state\n      this.saveToLocalStorage();\n    }\n\n    console.log(\"Album cache reset successfully with settings:\", {\n      viewMode: this.store.viewMode,\n      sortBy: this.store.sortBy,\n      sortOrder: this.store.sortOrder,\n      page: this.store.page,\n    });\n  }\n\n  // Save cache to localStorage\n  private saveToLocalStorage(): void {\n    if (typeof window !== \"undefined\") {\n      try {\n        localStorage.setItem(\"albumCache\", JSON.stringify(this.store));\n      } catch (error) {\n        console.error(\"Error saving album cache to localStorage:\", error);\n      }\n    }\n  }\n}\n\nexport const albumCache = AlbumCache.getInstance();\nexport default albumCache;\n"
  },
  {
    "path": "renderer/lib/apiConfig.ts",
    "content": "// Configure API URLs based on environment\nconst isDev = process.env.NODE_ENV === \"development\";\n\n// Your Vercel deployment URL\nconst PROD_API_URL = \"https://wora-ten.vercel.app\";\nconst DEV_API_URL = \"http://localhost:3000\";\n\n// Use localhost for development, the Vercel URL for production\nexport const API_BASE_URL = isDev ? DEV_API_URL : PROD_API_URL;\n\n// Helper function to construct API endpoints\nexport const apiEndpoint = (path: string): string => {\n  return `${API_BASE_URL}${path}`;\n};\n"
  },
  {
    "path": "renderer/lib/helpers.ts",
    "content": "import { Song } from \"@/context/playerContext\";\nimport axios from \"axios\";\nimport { IAudioMetadata } from \"music-metadata\";\nimport { useState, useEffect } from \"react\";\n\ninterface MetadataResponse {\n  metadata: IAudioMetadata;\n  favourite: boolean;\n}\n\nexport interface LyricLine {\n  time: number;\n  text: string;\n}\n\nexport const convertTime = (seconds: number) => {\n  if (!seconds) return \"--:--\";\n\n  const hours = Math.floor(seconds / 3600);\n  const minutes = Math.floor((seconds % 3600) / 60);\n  const secs = Math.floor(seconds % 60)\n    .toString()\n    .padStart(2, \"0\");\n\n  if (hours > 0) {\n    return `${hours}:${minutes.toString().padStart(2, \"0\")}:${secs}`;\n  }\n  return `${minutes}:${secs}`;\n};\n\nasync function fetchCover(artist: string, album: string) {\n  const queryTerms = [\n    !artist || artist === \"Various Artist\"\n      ? \"\"\n      : `artist:\"${artist.replace(/\"/g, '\\\\\"')}\"`,\n    !album || album === \"Unknown Album\" || album === \"\"\n      ? \"\"\n      : `release:\"${album.replace(/\"/g, '\\\\\"')}\"`,\n  ];\n\n  const query = queryTerms.join(\" \").trim();\n\n  try {\n    const response = await axios.get(\"https://musicbrainz.org/ws/2/release\", {\n      params: {\n        fmt: \"json\",\n        limit: \"3\",\n        query,\n      },\n    });\n\n    for (const release of response.data.releases) {\n      const coverUrl = `https://coverartarchive.org/release/${release.id}/front-250`;\n\n      try {\n        const headResponse = await axios.head(coverUrl);\n        if (headResponse.status >= 200 && headResponse.status < 300) {\n          return coverUrl;\n        }\n      } catch {\n        continue;\n      }\n    }\n    return \"logo\";\n  } catch {\n    return \"logo\";\n  }\n}\n\nexport const fetchLyrics = async (query: string, duration: number) => {\n  try {\n    const response = await axios.get(\"https://lrclib.net/api/search\", {\n      params: { q: query },\n    });\n\n    const songs = response.data;\n\n    const matchedSongs = songs.filter(\n      (song: any) =>\n        Math.abs(song.duration - duration) <= 5 &&\n        (song.syncedLyrics !== null || song.plainLyrics !== null),\n    );\n\n    if (matchedSongs.length === 0) {\n      return null;\n    }\n\n    matchedSongs.sort((a: any, b: any) => {\n      if (a.syncedLyrics && !b.syncedLyrics) {\n        return -1;\n      } else if (!a.syncedLyrics && b.syncedLyrics) {\n        return 1;\n      } else {\n        return 0;\n      }\n    });\n\n    if (matchedSongs[0].syncedLyrics) {\n      return matchedSongs[0].syncedLyrics;\n    } else if (matchedSongs[0].plainLyrics) {\n      return matchedSongs[0].plainLyrics;\n    }\n  } catch (error) {\n    return null;\n  }\n};\n\nexport const parseLyrics = (lyrics: string): LyricLine[] => {\n  return lyrics\n    .split(\"\\n\")\n    .filter((line) => line.trim() !== \"\")\n    .map((line) => {\n      const match = line.match(/^\\[(\\d{2}):(\\d{2}\\.\\d{2})\\] (.*)$/);\n      if (match) {\n        const minutes = parseInt(match[1], 10);\n        const seconds = parseFloat(match[2]);\n        const time = minutes * 60 + seconds - 1;\n        let text = match[3].trim();\n        if (text === \"\") {\n          text = \"...\";\n        }\n        return { time, text };\n      }\n      return null;\n    })\n    .filter((line) => line !== null) as LyricLine[];\n};\n\nexport const isSyncedLyrics = (lyrics: string): boolean => {\n  return /\\[\\d{2}:\\d{2}\\.\\d{2}\\]/.test(lyrics);\n};\n\nexport const fetchMetadata = async (\n  file: string,\n): Promise<MetadataResponse> => {\n  let metadata: IAudioMetadata | null;\n  let favourite: boolean;\n\n  if (file) {\n    await window.ipc\n      .invoke(\"getSongMetadata\", file)\n      .then((response) => {\n        metadata = response.metadata;\n        favourite = response.favourite;\n      })\n      .catch((error) => {\n        console.log(\"Error fetching metadata:\", error.message);\n      });\n  } else {\n    metadata = null;\n    favourite = false;\n  }\n\n  return { metadata, favourite };\n};\n\ninterface DiscordState {\n  details: string;\n  state?: string;\n  timestamp?: boolean;\n}\n\nconst defaultState: DiscordState = {\n  details: \"Idle...\",\n  timestamp: true,\n};\n\nexport const updateDiscordState = async (seek: number, song: Song) => {\n  if (!song) return;\n\n  const details = song.name;\n  const state = song.artist.split(/[,&(]/)[0].trim();\n  const duration = song.duration;\n\n  const cover = await fetchCover(song.album.artist, song.album.name);\n\n  window.ipc.send(\"set-rpc-state\", { details, state, seek, duration, cover });\n};\n\nexport const resetDiscordState = (): void => {\n  window.ipc.send(\"set-rpc-state\", defaultState);\n};\n\nexport const useAudioMetadata = (file: string) => {\n  const [metadata, setMetadata] = useState<IAudioMetadata | null>(null);\n  const [lyrics, setLyrics] = useState<string | null>(null);\n  const [favourite, setFavourite] = useState<boolean>(false);\n\n  useEffect(() => {\n    const getData = async () => {\n      try {\n        const response = await fetchMetadata(file);\n        setMetadata(response.metadata);\n        if (response.metadata) {\n          const fetchedLyrics = await fetchLyrics(\n            `${response.metadata.common.title} ${response.metadata.common.artist}`,\n            response.metadata.format.duration,\n          );\n\n          setLyrics(fetchedLyrics);\n        }\n        setFavourite(response.favourite);\n      } catch (error) {\n        console.error(\"Failed to fetch metadata: \", error.message);\n      }\n    };\n\n    getData();\n  }, [file]);\n\n  return { metadata, lyrics, favourite };\n};\n\nexport const shuffleArray = (array: any[]): any[] => {\n  const newArray = array.slice();\n  for (let i = newArray.length - 1; i > 0; i--) {\n    const j = Math.floor(Math.random() * (i + 1));\n    [newArray[i], newArray[j]] = [newArray[j], newArray[i]];\n  }\n  return newArray;\n};\n"
  },
  {
    "path": "renderer/lib/lastfm-client.ts",
    "content": "import { Song } from \"@/context/playerContext\";\nimport { API_BASE_URL } from \"./apiConfig\";\n\n// Session information\nlet sessionKey: string | null = null;\nlet username: string | null = null;\nlet isLoggedIn: boolean = false;\n\n// Base URL for API calls - use the apiConfig for proper URL resolution\nconst LASTFM_API_BASE = `${API_BASE_URL}/api/lastfm`;\n\n// Debug logging\nconst enableDebug = true;\nconst logLastFm = (message: string, data?: any) => {\n  if (enableDebug) {\n    console.log(`[Last.fm Client] ${message}`, data || \"\");\n  }\n};\n\n/**\n * Authenticate with Last.fm through our backend API\n * @param username Last.fm username\n * @param password Last.fm password\n */\nexport const initializeLastFM = async (\n  user: string,\n  password: string,\n): Promise<boolean> => {\n  try {\n    logLastFm(`Authenticating user: ${user}`);\n\n    const response = await fetch(`${LASTFM_API_BASE}/auth`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify({ username: user, password }),\n    });\n\n    const data = await response.json();\n\n    if (!data.success) {\n      logLastFm(\"Authentication error:\", data.error);\n      return false;\n    }\n\n    // Store the session key\n    sessionKey = data.session?.key;\n    username = user;\n    isLoggedIn = !!sessionKey;\n\n    logLastFm(`Authentication successful for ${user}`, {\n      sessionKeyLength: sessionKey ? sessionKey.length : 0,\n    });\n\n    return isLoggedIn;\n  } catch (error) {\n    logLastFm(\"Error initializing Last.fm:\", error);\n    return false;\n  }\n};\n\n/**\n * Initialize Last.fm with an existing session key (from database)\n */\nexport const initializeLastFMWithSession = (\n  existingSessionKey: string,\n  existingUsername: string,\n): void => {\n  sessionKey = existingSessionKey;\n  username = existingUsername;\n  isLoggedIn = !!existingSessionKey;\n  logLastFm(`Initialized with existing session for ${existingUsername}`, {\n    isLoggedIn,\n  });\n};\n\n/**\n * Get the current session key (for saving to database)\n */\nexport const getSessionKey = (): string | null => {\n  return sessionKey;\n};\n\n/**\n * Get the current username\n */\nexport const getUsername = (): string | null => {\n  return username;\n};\n\n/**\n * Check if user is logged in\n */\nexport const isAuthenticated = (): boolean => {\n  return isLoggedIn;\n};\n\n/**\n * Logout from Last.fm\n */\nexport const logout = (): void => {\n  sessionKey = null;\n  username = null;\n  isLoggedIn = false;\n  logLastFm(\"User logged out\");\n};\n\n/**\n * Update the \"Now Playing\" status on Last.fm\n * @param song The currently playing song\n */\nexport const updateNowPlaying = async (song: any): Promise<boolean> => {\n  if (!sessionKey || !song) {\n    logLastFm(\"Cannot update now playing - no session key or song\", {\n      hasSessionKey: !!sessionKey,\n      hasSong: !!song,\n    });\n    return false;\n  }\n\n  try {\n    logLastFm(`Updating now playing: \"${song.artist} - ${song.name}\"`);\n\n    const payload = {\n      sessionKey,\n      artist: song.artist,\n      track: song.name,\n      album: song.album?.name,\n      duration: song.duration\n        ? Math.round(song.duration).toString()\n        : undefined,\n    };\n\n    const response = await fetch(`${LASTFM_API_BASE}/now-playing`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify(payload),\n    });\n\n    const data = await response.json();\n\n    if (!data.success) {\n      logLastFm(\"Error updating now playing:\", data.error);\n      return false;\n    }\n\n    logLastFm(\"Now playing updated successfully\");\n    return true;\n  } catch (error) {\n    logLastFm(\"Error updating now playing status:\", error);\n    return false;\n  }\n};\n\n/**\n * Scrobble a track to Last.fm\n * @param song The song to scrobble\n */\nexport const scrobbleTrack = async (song: any): Promise<boolean> => {\n  if (!sessionKey || !song) {\n    logLastFm(\"Cannot scrobble - no session key or song\", {\n      hasSessionKey: !!sessionKey,\n      hasSong: !!song,\n    });\n    return false;\n  }\n\n  try {\n    logLastFm(`Scrobbling track: \"${song.artist} - ${song.name}\"`);\n\n    const timestamp = Math.floor(Date.now() / 1000);\n\n    const payload = {\n      sessionKey,\n      artist: song.artist,\n      track: song.name,\n      album: song.album?.name,\n      timestamp: timestamp.toString(),\n      duration: song.duration\n        ? Math.round(song.duration).toString()\n        : undefined,\n    };\n\n    const response = await fetch(`${LASTFM_API_BASE}/scrobble`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify(payload),\n    });\n\n    const data = await response.json();\n\n    if (!data.success) {\n      logLastFm(\"Error scrobbling track:\", data.error);\n      return false;\n    }\n\n    logLastFm(\"Track scrobbled successfully\");\n    return true;\n  } catch (error) {\n    logLastFm(\"Error scrobbling track:\", error);\n    return false;\n  }\n};\n\n/**\n * Get user info from LastFM\n */\nexport const getUserInfo = async (): Promise<any | null> => {\n  if (!sessionKey || !username) {\n    logLastFm(\"Cannot get user info - not authenticated\");\n    return null;\n  }\n\n  try {\n    logLastFm(`Getting user info for: ${username}`);\n\n    const url = new URL(`${LASTFM_API_BASE}/user-info`);\n    url.searchParams.append(\"username\", username);\n    url.searchParams.append(\"sessionKey\", sessionKey);\n\n    const response = await fetch(url.toString());\n    const data = await response.json();\n\n    if (!data.success) {\n      logLastFm(\"Error getting user info:\", data.error);\n      return null;\n    }\n\n    logLastFm(\"User info retrieved successfully\");\n    return data.user || null;\n  } catch (error) {\n    logLastFm(\"Failed to get user info:\", error);\n    return null;\n  }\n};\n\n/**\n * Get track info from LastFM\n */\nexport const getTrackInfo = async (\n  artist: string,\n  track: string,\n): Promise<any | null> => {\n  try {\n    const url = new URL(`${LASTFM_API_BASE}/track-info`);\n    url.searchParams.append(\"artist\", artist);\n    url.searchParams.append(\"track\", track);\n\n    if (username) {\n      url.searchParams.append(\"username\", username);\n    }\n\n    const response = await fetch(url.toString());\n    const data = await response.json();\n\n    if (!data.success) {\n      return null;\n    }\n\n    return data.track || null;\n  } catch (error) {\n    console.error(\"Failed to get track info:\", error);\n    return null;\n  }\n};\n\n/**\n * Love a track on LastFM\n */\nexport const loveTrack = async (\n  artist: string,\n  track: string,\n): Promise<boolean> => {\n  if (!sessionKey) return false;\n\n  try {\n    const response = await fetch(`${LASTFM_API_BASE}/track-info`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify({\n        sessionKey,\n        artist,\n        track,\n        action: \"love\",\n      }),\n    });\n\n    const data = await response.json();\n    return data.success === true;\n  } catch (error) {\n    console.error(\"Failed to love track:\", error);\n    return false;\n  }\n};\n\n/**\n * Unlove a track on LastFM\n */\nexport const unloveTrack = async (\n  artist: string,\n  track: string,\n): Promise<boolean> => {\n  if (!sessionKey) return false;\n\n  try {\n    const response = await fetch(`${LASTFM_API_BASE}/track-info`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify({\n        sessionKey,\n        artist,\n        track,\n        action: \"unlove\",\n      }),\n    });\n\n    const data = await response.json();\n    return data.success === true;\n  } catch (error) {\n    console.error(\"Failed to unlove track:\", error);\n    return false;\n  }\n};\n\n/**\n * Check if a track is loved by the user\n */\nexport const isTrackLoved = async (\n  artist: string,\n  track: string,\n): Promise<boolean> => {\n  if (!sessionKey) return false;\n\n  try {\n    const info = await getTrackInfo(artist, track);\n    return info?.userloved === \"1\";\n  } catch (error) {\n    console.error(\"Failed to check if track is loved:\", error);\n    return false;\n  }\n};\n"
  },
  {
    "path": "renderer/lib/lastfm.ts",
    "content": "// Last.fm Client for Electron - uses IPC to securely communicate with the backend API\n\n// Types\ninterface Song {\n  id?: number;\n  name: string;\n  artist: string;\n  album?: {\n    id?: number;\n    name: string;\n    artist?: string;\n    cover?: string;\n  };\n  duration?: number;\n}\n\n// Cache interface\ninterface LastFmUserCache {\n  user: any;\n  username: string;\n  sessionKey: string;\n  timestamp: number;\n  expiry: number;\n}\n\n// Internal state\nlet sessionKey: string | null = null;\nlet username: string | null = null;\nconst CACHE_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours\n\n// Simplified logging with log levels\nconst logLastFm = (\n  message: string,\n  data?: any,\n  level: \"info\" | \"error\" | \"warn\" = \"info\",\n) => {\n  // Only log to console in development\n  const isDev = process.env.NODE_ENV !== \"production\";\n\n  // Format the message\n  const formattedMessage = `[Last.fm] ${message}`;\n\n  // Log to console in development\n  if (isDev) {\n    switch (level) {\n      case \"error\":\n        console.error(formattedMessage, data || \"\");\n        break;\n      case \"warn\":\n        console.warn(formattedMessage, data || \"\");\n        break;\n      default:\n        console.log(formattedMessage, data || \"\");\n    }\n  }\n\n  // Always send to main process for file logging in production\n  if (window.ipc && window.ipc.send) {\n    try {\n      window.ipc.send(\"lastfm:log\", {\n        level,\n        message: data\n          ? `${formattedMessage}: ${typeof data === \"object\" ? JSON.stringify(data) : data}`\n          : formattedMessage,\n      });\n    } catch (err) {\n      // Only log failed IPC in development\n      if (isDev) console.error(\"Failed to send log to main process\", err);\n    }\n  }\n};\n\n/**\n * Store user info in cache\n */\nconst cacheUserInfo = (user: any): void => {\n  if (!username || !sessionKey || !user) return;\n\n  try {\n    const cacheData: LastFmUserCache = {\n      user,\n      username: username,\n      sessionKey: sessionKey,\n      timestamp: Date.now(),\n      expiry: Date.now() + CACHE_EXPIRY_MS,\n    };\n\n    localStorage.setItem(\"lastfm_user_cache\", JSON.stringify(cacheData));\n    logLastFm(\"User info cached successfully\");\n  } catch (error) {\n    // Silent fail - caching is non-critical\n    logLastFm(\"Failed to cache user info\", error, \"warn\");\n  }\n};\n\n/**\n * Get cached user info\n */\nconst getCachedUserInfo = (): any | null => {\n  try {\n    const cacheJson = localStorage.getItem(\"lastfm_user_cache\");\n    if (!cacheJson) return null;\n\n    const cache: LastFmUserCache = JSON.parse(cacheJson);\n\n    // Check if cache is expired or belongs to a different user/session\n    if (\n      cache.expiry < Date.now() ||\n      cache.username !== username ||\n      cache.sessionKey !== sessionKey\n    ) {\n      localStorage.removeItem(\"lastfm_user_cache\");\n      return null;\n    }\n\n    logLastFm(\"Using cached user info\");\n    return cache.user;\n  } catch (error) {\n    // If any error occurs reading cache, ignore and return null\n    localStorage.removeItem(\"lastfm_user_cache\");\n    return null;\n  }\n};\n\n/**\n * Clear the user info cache\n */\nconst clearUserCache = (): void => {\n  try {\n    localStorage.removeItem(\"lastfm_user_cache\");\n  } catch (error) {\n    // Silent fail\n  }\n};\n\n/**\n * Initialize Last.fm with username and password\n */\nexport const initializeLastFM = async (\n  lastfmUsername: string,\n  password: string,\n): Promise<boolean> => {\n  try {\n    const response = await window.ipc.invoke(\n      \"lastfm:authenticate\",\n      lastfmUsername,\n      password,\n    );\n\n    if (response.success && response.session) {\n      sessionKey = response.session.key;\n      username = response.session.name;\n      logLastFm(`Authentication successful`);\n      return true;\n    } else {\n      logLastFm(\"Authentication failed\", response.error, \"error\");\n      return false;\n    }\n  } catch (error) {\n    logLastFm(\"Error initializing\", error, \"error\");\n    return false;\n  }\n};\n\n/**\n * Initialize Last.fm with existing session key\n */\nexport const initializeLastFMWithSession = (\n  key: string,\n  user: string,\n): void => {\n  sessionKey = key;\n  username = user;\n};\n\n/**\n * Check if Last.fm is authenticated\n */\nexport const isAuthenticated = (): boolean => {\n  return !!sessionKey;\n};\n\n/**\n * Get the current session key\n */\nexport const getSessionKey = (): string | null => {\n  return sessionKey;\n};\n\n/**\n * Clear the Last.fm session\n */\nexport const logout = (): void => {\n  sessionKey = null;\n  username = null;\n  clearUserCache();\n};\n\n/**\n * Update Now Playing status on Last.fm\n */\nexport const updateNowPlaying = async (song: Song): Promise<boolean> => {\n  if (!sessionKey) {\n    return false;\n  }\n\n  try {\n    const response = await window.ipc.invoke(\"lastfm:updateNowPlaying\", {\n      sessionKey,\n      artist: song.artist,\n      track: song.name,\n      album: song.album?.name,\n      duration: song.duration\n        ? Math.floor(song.duration).toString()\n        : undefined,\n    });\n\n    if (!response.success) {\n      logLastFm(\"Failed to update now playing\", response.error, \"warn\");\n    }\n    return response.success;\n  } catch (error) {\n    logLastFm(\"Error updating now playing\", error, \"error\");\n    return false;\n  }\n};\n\n/**\n * Scrobble a track to Last.fm\n */\nexport const scrobbleTrack = async (song: Song): Promise<boolean> => {\n  if (!sessionKey) {\n    return false;\n  }\n\n  try {\n    const response = await window.ipc.invoke(\"lastfm:scrobbleTrack\", {\n      sessionKey,\n      artist: song.artist,\n      track: song.name,\n      album: song.album?.name,\n      timestamp: Math.floor(Date.now() / 1000).toString(),\n      duration: song.duration\n        ? Math.floor(song.duration).toString()\n        : undefined,\n    });\n\n    if (!response.success) {\n      logLastFm(\"Failed to scrobble track\", response.error, \"warn\");\n    }\n    return response.success;\n  } catch (error) {\n    logLastFm(\"Error scrobbling track\", error, \"error\");\n    return false;\n  }\n};\n\n/**\n * Get user information from Last.fm\n */\nexport const getUserInfo = async (): Promise<any> => {\n  if (!username || !sessionKey) {\n    return null;\n  }\n\n  // First, try to get user info from cache\n  const cachedUserInfo = getCachedUserInfo();\n  if (cachedUserInfo) {\n    return cachedUserInfo;\n  }\n\n  // If cache miss or expired, fetch from API\n  try {\n    const response = await window.ipc.invoke(\n      \"lastfm:getUserInfo\",\n      username,\n      sessionKey,\n    );\n\n    if (response.success) {\n      // Cache the successful response for future use\n      cacheUserInfo(response.user);\n      return response.user;\n    } else {\n      logLastFm(\"Failed to get user info\", response.error, \"warn\");\n      return null;\n    }\n  } catch (error) {\n    logLastFm(\"Error getting user info\", error, \"error\");\n    return null;\n  }\n};\n\n/**\n * Get track information from Last.fm\n */\nexport const getTrackInfo = async (\n  artist: string,\n  track: string,\n): Promise<any> => {\n  try {\n    const response = await window.ipc.invoke(\n      \"lastfm:getTrackInfo\",\n      artist,\n      track,\n      username,\n    );\n\n    if (response.success) {\n      return response.track;\n    }\n    return null;\n  } catch (error) {\n    logLastFm(\"Error getting track info\", error, \"error\");\n    return null;\n  }\n};\n\n/**\n * Love a track on Last.fm\n */\nexport const loveTrack = async (\n  artist: string,\n  track: string,\n): Promise<boolean> => {\n  if (!sessionKey) return false;\n\n  try {\n    const response = await window.ipc.invoke(\"lastfm:loveTrack\", {\n      sessionKey,\n      artist,\n      track,\n      love: true,\n    });\n\n    return response.success;\n  } catch (error) {\n    logLastFm(\"Error loving track\", error, \"error\");\n    return false;\n  }\n};\n\n/**\n * Unlove a track on Last.fm\n */\nexport const unloveTrack = async (\n  artist: string,\n  track: string,\n): Promise<boolean> => {\n  if (!sessionKey) return false;\n\n  try {\n    const response = await window.ipc.invoke(\"lastfm:loveTrack\", {\n      sessionKey,\n      artist,\n      track,\n      love: false,\n    });\n\n    return response.success;\n  } catch (error) {\n    logLastFm(\"Error unloving track\", error, \"error\");\n    return false;\n  }\n};\n\n/**\n * Check if a track is loved by the user\n */\nexport const isTrackLoved = async (\n  artist: string,\n  track: string,\n): Promise<boolean> => {\n  try {\n    const trackInfo = await getTrackInfo(artist, track);\n    return trackInfo && trackInfo.userloved === \"1\";\n  } catch (error) {\n    logLastFm(\"Error checking if track is loved\", error, \"error\");\n    return false;\n  }\n};\n"
  },
  {
    "path": "renderer/lib/songCache.ts",
    "content": "// Global song cache to persist song data between page navigations\nimport { Song } from \"@/context/playerContext\";\n\n// Define the type for our global song cache\ninterface SongCacheStore {\n  allSongs: Song[]; // Complete song library (when fetched)\n  filteredSongs: Song[]; // Current filtered/sorted view\n  searchResults: Song[]; // Recent search results\n  lastSearchQuery: string;\n  sortBy: string;\n  sortOrder: string;\n  page: number;\n  hasMore: boolean;\n  lastFetchTime: number; // To determine if cache is stale\n  isInitialized: boolean;\n}\n\n// Default initial state\nconst initialState: SongCacheStore = {\n  allSongs: [],\n  filteredSongs: [],\n  searchResults: [],\n  lastSearchQuery: \"\",\n  sortBy: \"name\",\n  sortOrder: \"asc\",\n  page: 1,\n  hasMore: true,\n  lastFetchTime: 0,\n  isInitialized: false,\n};\n\n// Cache invalidation interval (5 minutes)\nconst CACHE_TTL = 5 * 60 * 1000;\n\nclass SongCache {\n  private static instance: SongCache;\n  private store: SongCacheStore = { ...initialState };\n\n  private constructor() {\n    // Initialize with saved cache if available\n    if (typeof window !== \"undefined\") {\n      const savedCache = localStorage.getItem(\"songCache\");\n      if (savedCache) {\n        try {\n          const parsedCache = JSON.parse(savedCache);\n          // Only restore if the cache isn't stale\n          if (Date.now() - parsedCache.lastFetchTime < CACHE_TTL) {\n            this.store = parsedCache;\n          }\n        } catch (error) {\n          console.error(\"Error parsing song cache from localStorage:\", error);\n        }\n      }\n    }\n  }\n\n  // Get singleton instance\n  public static getInstance(): SongCache {\n    if (!SongCache.instance) {\n      SongCache.instance = new SongCache();\n    }\n    return SongCache.instance;\n  }\n\n  // Function to sort songs\n  public sortSongs(songs: Song[], sortBy: string, sortOrder: string): Song[] {\n    return [...songs].sort((a, b) => {\n      let comparison = 0;\n\n      switch (sortBy) {\n        case \"name\":\n          comparison = a.name.localeCompare(b.name);\n          break;\n        case \"artist\":\n          comparison = a.artist.localeCompare(b.artist);\n          break;\n        case \"album\":\n          comparison = a.album.name.localeCompare(b.album.name);\n          break;\n        case \"duration\":\n          comparison = a.duration - b.duration;\n          break;\n        default:\n          comparison = a.name.localeCompare(b.name);\n      }\n\n      return sortOrder === \"asc\" ? comparison : -comparison;\n    });\n  }\n\n  // Get all songs\n  public getAllSongs(): Song[] {\n    return this.store.allSongs;\n  }\n\n  // Get filtered songs\n  public getFilteredSongs(): Song[] {\n    return this.store.filteredSongs;\n  }\n\n  // Get search results\n  public getSearchResults(): Song[] {\n    return this.store.searchResults;\n  }\n\n  // Get current pagination page\n  public getPage(): number {\n    return this.store.page;\n  }\n\n  // Check if cache has been initialized\n  public isInitialized(): boolean {\n    return this.store.isInitialized;\n  }\n\n  // Check if there are more songs to load\n  public hasMore(): boolean {\n    return this.store.hasMore;\n  }\n\n  // Get current sort settings\n  public getSortSettings(): { sortBy: string; sortOrder: string } {\n    return {\n      sortBy: this.store.sortBy,\n      sortOrder: this.store.sortOrder,\n    };\n  }\n\n  // Get last search query\n  public getLastSearchQuery(): string {\n    return this.store.lastSearchQuery;\n  }\n\n  // Check if the cache is stale\n  public isStale(): boolean {\n    return Date.now() - this.store.lastFetchTime > CACHE_TTL;\n  }\n\n  // Set all songs\n  public setAllSongs(songs: Song[]): void {\n    this.store.allSongs = songs;\n    this.store.lastFetchTime = Date.now();\n    this.saveToLocalStorage();\n  }\n\n  // Add more songs to the existing collection (for pagination)\n  public addSongs(newSongs: Song[]): void {\n    // Filter out duplicates based on song ID\n    const existingIds = new Set(this.store.allSongs.map((song) => song.id));\n    const uniqueNewSongs = newSongs.filter((song) => !existingIds.has(song.id));\n\n    this.store.allSongs = [...this.store.allSongs, ...uniqueNewSongs];\n    this.store.lastFetchTime = Date.now();\n    this.saveToLocalStorage();\n  }\n\n  // Set filtered songs\n  public setFilteredSongs(songs: Song[]): void {\n    this.store.filteredSongs = songs;\n    this.saveToLocalStorage();\n  }\n\n  // Set search results\n  public setSearchResults(songs: Song[], query: string): void {\n    this.store.searchResults = songs;\n    this.store.lastSearchQuery = query;\n    this.saveToLocalStorage();\n  }\n\n  // Update pagination state\n  public updatePagination(page: number, hasMore: boolean): void {\n    this.store.page = page;\n    this.store.hasMore = hasMore;\n    this.saveToLocalStorage();\n  }\n\n  // Update sort settings\n  public updateSortSettings(sortBy: string, sortOrder: string): void {\n    this.store.sortBy = sortBy;\n    this.store.sortOrder = sortOrder;\n    this.saveToLocalStorage();\n  }\n\n  // Mark cache as initialized\n  public setInitialized(): void {\n    this.store.isInitialized = true;\n    this.saveToLocalStorage();\n  }\n\n  // Reset cache\n  public resetCache(): void {\n    this.store = { ...initialState };\n    if (typeof window !== \"undefined\") {\n      localStorage.removeItem(\"songCache\");\n    }\n  }\n\n  // Reset state with specific values (for page resets)\n  public resetState(resetData: Partial<SongCacheStore>): void {\n    // Apply default values from initial state and then override with any provided resetData\n    const currentSongs = this.store.allSongs; // Keep the songs data\n\n    // Reset specific fields while preserving song data\n    this.store = {\n      ...initialState,\n      allSongs: currentSongs,\n      filteredSongs: currentSongs,\n      sortBy: resetData.sortBy || initialState.sortBy,\n      sortOrder: resetData.sortOrder || initialState.sortOrder,\n      page: resetData.page || initialState.page,\n      lastFetchTime: Date.now(),\n      isInitialized: true, // Keep initialized status\n    };\n\n    // If reset includes specific sort options, apply sorting to filtered songs\n    if (currentSongs.length > 0) {\n      this.store.filteredSongs = this.sortSongs(\n        currentSongs,\n        this.store.sortBy,\n        this.store.sortOrder,\n      );\n    }\n\n    // Clear localStorage to ensure values are truly reset\n    if (typeof window !== \"undefined\") {\n      localStorage.removeItem(\"songCache\");\n      // Save the new state\n      this.saveToLocalStorage();\n    }\n\n    console.log(\"Song cache reset successfully with settings:\", {\n      sortBy: this.store.sortBy,\n      sortOrder: this.store.sortOrder,\n      page: this.store.page,\n    });\n  }\n\n  // Save cache to localStorage\n  private saveToLocalStorage(): void {\n    if (typeof window !== \"undefined\") {\n      try {\n        localStorage.setItem(\"songCache\", JSON.stringify(this.store));\n      } catch (error) {\n        console.error(\"Error saving song cache to localStorage:\", error);\n      }\n    }\n  }\n}\n\nexport const songCache = SongCache.getInstance();\nexport default songCache;\n"
  },
  {
    "path": "renderer/lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "renderer/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "renderer/next.config.js",
    "content": "/** @type {import('next').NextConfig} */\n\nmodule.exports = {\n  // Use output: 'export' since we're integrating with Electron\n  output: \"export\",\n  distDir: process.env.NODE_ENV === \"production\" ? \"../app\" : \".next\",\n  trailingSlash: true,\n  images: {\n    unoptimized: true, // Required for static export\n    domains: [\"lastfm.freetls.fastly.net\"], // Allow Last.fm images\n  },\n  webpack: (config) => {\n    return config;\n  },\n};\n"
  },
  {
    "path": "renderer/pages/_app.tsx",
    "content": "import \"@/styles/globals.css\";\nimport Actions from \"@/components/ui/actions\";\nimport Navbar from \"@/components/main/navbar\";\nimport Player from \"@/components/main/player\";\nimport { PlayerProvider } from \"@/context/playerContext\";\nimport { useRouter } from \"next/router\";\nimport { Toaster } from \"@/components/ui/sonner\";\nimport { ThemeProvider } from \"@/components/themeProvider\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { useEffect, useRef } from \"react\";\nimport ErrorBoundary from \"@/components/ErrorBoundary\";\n// import PageTransition from \"@/components/PageTransition\";  // Optional: Re-enable for transitions\n\nconst SPECIAL_LAYOUTS = [\"/setup\"];\n\nexport default function App({ Component, pageProps }) {\n  const router = useRouter();\n  const scrollAreaRef = useRef<HTMLDivElement>(null);\n\n  const isSpecialLayout = SPECIAL_LAYOUTS.includes(router.pathname);\n\n  useEffect(() => {\n    if (!isSpecialLayout) {\n      Promise.all([\n        window.ipc\n          .invoke(\"getSettings\")\n          .catch((err) => console.error(\"Error loading settings:\", err)),\n        window.ipc\n          .invoke(\"getRandomLibraryItems\")\n          .catch((err) => console.error(\"Error loading library items:\", err)),\n      ]).catch((err) => console.error(\"Error in data preloading:\", err));\n    }\n  }, [isSpecialLayout, router.pathname]);\n\n  if (isSpecialLayout) {\n    return (\n      <ThemeProvider attribute=\"class\" defaultTheme=\"system\" enableSystem>\n        <main className=\"bg-white text-xs text-black antialiased select-none dark:bg-black dark:text-white\">\n          <Component {...pageProps} />\n        </main>\n      </ThemeProvider>\n    );\n  }\n\n  return (\n    <ThemeProvider\n      attribute=\"class\"\n      defaultTheme=\"system\"\n      disableTransitionOnChange\n      enableSystem\n    >\n      <PlayerProvider>\n        <main className=\"bg-white text-xs text-black antialiased select-none dark:bg-black dark:text-white\">\n          <div className=\"h-dvh w-dvw\">\n            <Actions />\n            <Toaster position=\"top-right\" />\n\n            <div className=\"flex gap-8\">\n              <div className=\"sticky top-0 z-50 h-dvh p-8 pt-12 pr-0\">\n                <Navbar />\n              </div>\n\n              <div className=\"h-dvh grow p-8 pt-12 pl-0\">\n                <div className=\"wora-transition relative flex h-full w-full flex-col\">\n                  <ScrollArea\n                    ref={scrollAreaRef}\n                    className=\"h-full w-full mask-b-from-40%\"\n                  >\n                    <ErrorBoundary>\n                      <Component {...pageProps} />\n                      <div className=\"h-[20vh] w-full\" />\n                    </ErrorBoundary>\n                  </ScrollArea>\n\n                  <Player />\n                </div>\n              </div>\n            </div>\n          </div>\n        </main>\n      </PlayerProvider>\n    </ThemeProvider>\n  );\n}\n"
  },
  {
    "path": "renderer/pages/albums/[slug].tsx",
    "content": "import React, { useEffect, useState, useRef } from \"react\";\nimport Image from \"next/image\";\nimport { useRouter } from \"next/router\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  IconCircleFilled,\n  IconPlayerPlay,\n  IconArrowsShuffle2,\n  IconClock,\n} from \"@tabler/icons-react\";\nimport { usePlayer } from \"@/context/playerContext\";\nimport Songs from \"@/components/ui/songs\";\nimport Link from \"next/link\";\nimport { convertTime } from \"@/lib/helpers\";\n\ntype Album = {\n  name: string;\n  artist: string;\n  year: number;\n  cover: string;\n  songs: any[];\n  duration?: number;\n};\n\nexport default function Album() {\n  const router = useRouter();\n  const [album, setAlbum] = useState<Album | null>(null);\n  const { setQueueAndPlay } = usePlayer();\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  // Disable scroll restoration on mount and route change\n  useEffect(() => {\n    if (typeof window !== \"undefined\") {\n      // Force scroll to top\n      window.scrollTo(0, 0);\n\n      // Disable scroll restoration for this page\n      if (history.scrollRestoration) {\n        history.scrollRestoration = \"manual\";\n      }\n    }\n\n    // Cleanup - reset when leaving the page\n    return () => {\n      if (history.scrollRestoration) {\n        history.scrollRestoration = \"auto\";\n      }\n    };\n  }, [router.asPath]);\n\n  useEffect(() => {\n    if (!router.query.slug) return;\n\n    window.ipc\n      .invoke(\"getAlbumWithSongs\", router.query.slug)\n      .then((response) => {\n        setAlbum(response);\n      });\n  }, [router.query.slug]);\n\n  const playAlbum = () => {\n    if (album) {\n      setQueueAndPlay(album.songs, 0);\n    }\n  };\n\n  const playAlbumAndShuffle = () => {\n    if (album) {\n      setQueueAndPlay(album.songs, 0, true);\n    }\n  };\n\n  // Calculate total duration from songs if not provided by the backend\n  const calculateTotalDuration = () => {\n    if (!album) return 0;\n\n    if (album.duration) return album.duration;\n\n    return (\n      album.songs?.reduce((total, song) => total + (song.duration || 0), 0) || 0\n    );\n  };\n\n  return (\n    <>\n      <div className=\"relative h-96 w-full overflow-hidden rounded-2xl\">\n        <Image\n          alt={album ? album.name : \"Album Cover\"}\n          src={album ? `wora://${album.cover}` : \"/coverArt.png\"}\n          fill\n          loading=\"lazy\"\n          className=\"mask-b-from-30% object-cover object-center blur-xl\"\n        />\n        <div className=\"absolute bottom-6 left-6\">\n          <div className=\"flex items-end gap-4\">\n            <div className=\"relative h-52 w-52 overflow-hidden rounded-xl shadow-lg transition duration-300\">\n              <Image\n                alt={album ? album.name : \"Album Cover\"}\n                src={album ? `wora://${album.cover}` : \"/coverArt.png\"}\n                fill\n                loading=\"lazy\"\n                className=\"scale-[1.01] object-cover\"\n              />\n            </div>\n            <div className=\"flex flex-col gap-4\">\n              <div>\n                <h1 className=\"text-2xl font-medium\">{album && album.name}</h1>\n                <p className=\"flex items-center gap-2 text-sm\">\n                  <Link\n                    href={\n                      album\n                        ? `/artists/${encodeURIComponent(album.artist)}`\n                        : \"#\"\n                    }\n                  >\n                    <span className=\"text-primary cursor-pointer underline-offset-2 hover:underline hover:opacity-80\">\n                      {album && album.artist}\n                    </span>\n                  </Link>\n                  <IconCircleFilled stroke={2} size={5} />{\" \"}\n                  {album && album.year ? album.year : \"Unknown\"}\n                  <IconCircleFilled stroke={2} size={5} />{\" \"}\n                  <span className=\"flex items-center gap-1\">\n                    <IconClock size={14} stroke={2} />\n                    {album ? convertTime(calculateTotalDuration()) : \"--:--\"}\n                  </span>\n                </p>\n              </div>\n              <div className=\"flex gap-2\">\n                <Button onClick={playAlbum} className=\"w-fit\">\n                  <IconPlayerPlay\n                    className=\"fill-black dark:fill-white\"\n                    stroke={2}\n                    size={16}\n                  />{\" \"}\n                  Play\n                </Button>\n                <Button className=\"w-fit\" onClick={playAlbumAndShuffle}>\n                  <IconArrowsShuffle2 stroke={2} size={16} /> Shuffle\n                </Button>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n      <div className=\"pt-2\">\n        <Songs library={album?.songs} disableScroll={true} />\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "renderer/pages/albums.tsx",
    "content": "import React, { useEffect, useState, useCallback, useRef } from \"react\";\nimport { useScrollAreaRestoration } from \"@/hooks/useScrollAreaRestoration\";\nimport AlbumCard from \"@/components/ui/album\";\nimport Spinner from \"@/components/ui/spinner\";\nimport { useRouter } from \"next/router\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport {\n  IconSearch,\n  IconX,\n  IconSortAscending,\n  IconSortDescending,\n  IconLayoutGrid,\n  IconLayoutList,\n  IconGridDots,\n} from \"@tabler/icons-react\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport Image from \"next/image\";\nimport Link from \"next/link\";\nimport albumCache from \"@/lib/albumCache\";\n\ntype ViewMode = \"grid-large\" | \"grid-small\" | \"list\";\n\n\nexport default function Albums() {\n  const [albums, setAlbums] = useState(albumCache.getAllAlbums());\n  const [filteredAlbums, setFilteredAlbums] = useState(\n    albumCache.getFilteredAlbums(),\n  );\n  const [searchTerm, setSearchTerm] = useState(albumCache.getLastSearchQuery());\n  const [loading, setLoading] = useState(false);\n  const [searchLoading, setSearchLoading] = useState(false);\n  const [page, setPage] = useState(albumCache.getPage());\n  const [hasMore, setHasMore] = useState(albumCache.hasMore());\n  \n  // Use scroll restoration hook for ScrollArea\n  useScrollAreaRestoration('albums');\n\n  const [sortBy, setSortBy] = useState(() => {\n    if (typeof window !== \"undefined\") {\n      return localStorage.getItem(\"albumsSortBy\") || albumCache.getSortSettings().sortBy;\n    }\n    return albumCache.getSortSettings().sortBy;\n  });\n  \n  const [sortOrder, setSortOrder] = useState(() => {\n    if (typeof window !== \"undefined\") {\n      return localStorage.getItem(\"albumsSortOrder\") || albumCache.getSortSettings().sortOrder;\n    }\n    return albumCache.getSortSettings().sortOrder;\n  });\n\n  const [viewMode, setViewMode] = useState<ViewMode>(() => {\n    if (typeof window !== \"undefined\") {\n      return (localStorage.getItem(\"albumsViewMode\") as ViewMode) || \"grid-large\";\n    }\n    return \"grid-large\";\n  });\n\n  const router = useRouter();\n  const searchTimeout = useRef<NodeJS.Timeout | null>(null);\n  const gridRef = useRef(null);\n  \n\n  useEffect(() => {\n    const resetListener = window.ipc.on(\"resetAlbumsState\", () => {\n      setSearchTerm(\"\");\n      setSortBy(\"name\");\n      setSortOrder(\"asc\");\n      setViewMode(\"grid-large\");\n      albumCache.setSearchResults([], \"\");\n      setPage(1);\n      setHasMore(true);\n      if (gridRef.current && gridRef.current.scrollToItem) {\n        gridRef.current.scrollToItem(0);\n      }\n    });\n\n    return () => {\n      resetListener();\n    };\n  }, []);\n\n  useEffect(() => {\n    if (!albumCache.isInitialized() || albumCache.isStale()) {\n      loadAlbums();\n      albumCache.setInitialized();\n    } else {\n      setAlbums(albumCache.getAllAlbums());\n      setFilteredAlbums(albumCache.getFilteredAlbums());\n      if (searchTerm) {\n        setFilteredAlbums(albumCache.getSearchResults());\n      }\n    }\n  }, []);\n\n  // Load albums from the database with pagination\n  const loadAlbums = useCallback(\n    async (isReset = false) => {\n      // When resetting, ignore hasMore/loading restrictions\n      if (!isReset && (loading || !hasMore) && !searchTerm) return;\n\n      setLoading(true);\n\n      // If this is a reset load, reset the album state\n      const pageToLoad = isReset ? 1 : page;\n\n      try {\n        // Get paginated albums with durations\n        const newAlbums = await window.ipc.invoke(\n          \"getAlbumsWithDuration\",\n          pageToLoad,\n        );\n\n        if (newAlbums.length === 0) {\n          setHasMore(false);\n          albumCache.updatePagination(pageToLoad, false);\n        } else {\n          // For reset operations, replace the entire album list\n          if (isReset) {\n            setAlbums(newAlbums);\n\n            // Sort with default settings\n            const sortedAlbums = albumCache.sortAlbums(\n              newAlbums,\n              sortBy,\n              sortOrder,\n            );\n            setFilteredAlbums(sortedAlbums);\n            albumCache.setFilteredAlbums(sortedAlbums);\n            albumCache.setAllAlbums(newAlbums);\n\n            // Update pagination\n            setPage(2); // Set to 2 because we just loaded page 1\n            albumCache.updatePagination(2, true);\n          } else {\n            // Regular pagination behavior follows\n            // Create a Set of existing album IDs for efficient lookups\n            const existingAlbumIds = new Set(albums.map((album) => album.id));\n\n            // Filter out any duplicates from the newly loaded albums\n            const uniqueNewAlbums = newAlbums.filter(\n              (album) => !existingAlbumIds.has(album.id),\n            );\n\n            // Only update if we have unique albums to add\n            if (uniqueNewAlbums.length > 0) {\n              const updatedAlbums = [...albums, ...uniqueNewAlbums];\n              setAlbums(updatedAlbums);\n\n              // Only update filtered albums if not searching\n              if (!searchTerm) {\n                // Important: preserve reference to previous filtered albums to avoid scroll jumps\n                const sortedAlbums = albumCache.sortAlbums(\n                  updatedAlbums,\n                  sortBy,\n                  sortOrder,\n                );\n\n                // Use a stable state update to avoid scroll reset\n                setFilteredAlbums((prev) => {\n                  // If the length is significantly different, just use the new sorted albums\n                  if (\n                    Math.abs(prev.length - sortedAlbums.length) >\n                    uniqueNewAlbums.length\n                  ) {\n                    return sortedAlbums;\n                  }\n\n                  // Otherwise, append the new albums to the existing array for a smoother update\n                  const existingIds = new Set(prev.map((album) => album.id));\n                  const newItems = sortedAlbums.filter(\n                    (album) => !existingIds.has(album.id),\n                  );\n                  return [...prev, ...newItems];\n                });\n\n                albumCache.setFilteredAlbums(sortedAlbums);\n              }\n\n              // Update global cache with only the unique new albums\n              albumCache.addAlbums(uniqueNewAlbums);\n            }\n\n            // Always increment page counter for next fetch\n            setPage(page + 1);\n            albumCache.updatePagination(page + 1, true);\n          }\n        }\n      } catch (error) {\n        console.error(\"Error loading albums:\", error);\n      } finally {\n        setLoading(false);\n      }\n    },\n    [page, loading, hasMore, albums, searchTerm, sortBy, sortOrder],\n  );\n\n  // Load more function with debounce to prevent too frequent calls\n  const loadMoreAlbums = useCallback(() => {\n    // Using a ref to track loading state locally to avoid excessive re-renders\n    if (!loading && hasMore && !searchTerm) {\n      console.log(\"Loading more albums from scroll trigger\");\n      loadAlbums();\n    }\n  }, [loadAlbums, loading, hasMore, searchTerm]);\n\n  // When the user scrolls near the end, load more albums\n  const onItemsRendered = useCallback(\n    ({ visibleStopIndex }) => {\n      if (\n        !loading &&\n        hasMore &&\n        !searchTerm &&\n        visibleStopIndex >= filteredAlbums.length - 10\n      ) {\n        loadAlbums();\n      }\n    },\n    [loadAlbums, filteredAlbums.length, loading, hasMore, searchTerm],\n  );\n\n  // Handle search with debounce\n  const handleSearch = useCallback(\n    async (term) => {\n      // Clear any pending search\n      if (searchTimeout.current) {\n        clearTimeout(searchTimeout.current);\n      }\n\n      if (!term) {\n        // If search is cleared, show all sorted albums\n        const sortedAlbums = albumCache.sortAlbums(albums, sortBy, sortOrder);\n        setFilteredAlbums(sortedAlbums);\n        albumCache.setFilteredAlbums(sortedAlbums);\n        albumCache.setSearchResults([], \"\");\n        return;\n      }\n\n      setSearchLoading(true);\n\n      // Debounce search to avoid too many requests\n      searchTimeout.current = setTimeout(async () => {\n        try {\n          // Call the search function to find matching albums\n          const results = await window.ipc.invoke(\"search\", term);\n\n          if (\n            results &&\n            results.searchAlbums &&\n            results.searchAlbums.length > 0\n          ) {\n            // If we got results from the server, use those\n            const processedResults = results.searchAlbums;\n            const sortedResults = albumCache.sortAlbums(\n              processedResults,\n              sortBy,\n              sortOrder,\n            );\n\n            setFilteredAlbums(sortedResults);\n\n            // Update cache with search results\n            albumCache.setSearchResults(sortedResults, term);\n          } else {\n            // If no specific album search results from server, filter locally\n            const searchTermLower = term.toLowerCase().trim();\n            const localFilteredAlbums = albums.filter(\n              (album) =>\n                (album.name &&\n                  album.name.toLowerCase().includes(searchTermLower)) ||\n                (album.artist &&\n                  album.artist.toLowerCase().includes(searchTermLower)),\n            );\n\n            const sortedResults = albumCache.sortAlbums(\n              localFilteredAlbums,\n              sortBy,\n              sortOrder,\n            );\n            setFilteredAlbums(sortedResults);\n\n            // Update cache with search results\n            albumCache.setSearchResults(sortedResults, term);\n          }\n        } catch (error) {\n          console.error(\"Error searching albums:\", error);\n          // Fall back to local filtering as last resort\n          const searchTermLower = term.toLowerCase().trim();\n          const localFilteredAlbums = albums.filter(\n            (album) =>\n              (album.name &&\n                album.name.toLowerCase().includes(searchTermLower)) ||\n              (album.artist &&\n                album.artist.toLowerCase().includes(searchTermLower)),\n          );\n\n          const sortedResults = albumCache.sortAlbums(\n            localFilteredAlbums,\n            sortBy,\n            sortOrder,\n          );\n          setFilteredAlbums(sortedResults);\n\n          // Update cache with search results\n          albumCache.setSearchResults(sortedResults, term);\n        } finally {\n          setSearchLoading(false);\n        }\n      }, 300);\n    },\n    [albums, sortBy, sortOrder],\n  );\n\n  // Handle navigation to artist page\n  const navigateToArtist = useCallback(\n    (artist: string, e: React.MouseEvent) => {\n      e.preventDefault();\n      e.stopPropagation();\n      if (artist) {\n        router.push(`/artists/${encodeURIComponent(artist)}`);\n      }\n    },\n    [router],\n  );\n\n  // Update search when term changes\n  useEffect(() => {\n    handleSearch(searchTerm);\n  }, [searchTerm, handleSearch]);\n\n  // Update sorting when sort parameters change\n  useEffect(() => {\n    // Update the global cache with new sort settings\n    albumCache.updateSortSettings(sortBy, sortOrder);\n\n    if (searchTerm) {\n      // If we're searching, sort the existing search results\n      const searchResults = albumCache.getSearchResults();\n      if (searchResults.length > 0) {\n        const sortedResults = albumCache.sortAlbums(\n          searchResults,\n          sortBy,\n          sortOrder,\n        );\n        setFilteredAlbums(sortedResults);\n        albumCache.setSearchResults(sortedResults, searchTerm);\n      }\n    } else {\n      // If not searching, fetch and sort all albums\n      const fetchAndSortAllAlbums = async () => {\n        setSearchLoading(true);\n        try {\n          // Use cached all albums if available and not stale\n          let allAlbums = albumCache.getAllAlbums();\n\n          // If the cache doesn't have all albums or is stale, fetch them\n          if (allAlbums.length === 0 || albumCache.isStale()) {\n            // Fetch as many pages as needed to get all albums\n            let tempPage = 1;\n            let hasMoreAlbums = true;\n            let allFetchedAlbums = [];\n\n            while (hasMoreAlbums) {\n              const fetchedAlbums = await window.ipc.invoke(\n                \"getAlbums\",\n                tempPage,\n              );\n              if (fetchedAlbums.length === 0) {\n                hasMoreAlbums = false;\n              } else {\n                allFetchedAlbums = [...allFetchedAlbums, ...fetchedAlbums];\n                tempPage++;\n              }\n            }\n\n            if (allFetchedAlbums.length > 0) {\n              albumCache.setAllAlbums(allFetchedAlbums);\n              allAlbums = allFetchedAlbums;\n            }\n          }\n\n          if (allAlbums.length > 0) {\n            // Sort and display all albums with the new sort parameters\n            const sortedAlbums = albumCache.sortAlbums(\n              allAlbums,\n              sortBy,\n              sortOrder,\n            );\n            setFilteredAlbums(sortedAlbums);\n            albumCache.setFilteredAlbums(sortedAlbums);\n\n            // Update local state\n            setAlbums(allAlbums);\n          } else {\n            // Fall back to sorting just the loaded albums\n            const sortedAlbums = albumCache.sortAlbums(\n              albums,\n              sortBy,\n              sortOrder,\n            );\n            setFilteredAlbums(sortedAlbums);\n            albumCache.setFilteredAlbums(sortedAlbums);\n          }\n        } catch (error) {\n          console.error(\"Error fetching all albums for sorting:\", error);\n          // Fall back to loaded albums\n          const sortedAlbums = albumCache.sortAlbums(albums, sortBy, sortOrder);\n          setFilteredAlbums(sortedAlbums);\n          albumCache.setFilteredAlbums(sortedAlbums);\n        } finally {\n          setSearchLoading(false);\n        }\n      };\n\n      fetchAndSortAllAlbums();\n    }\n\n    // Reset the grid scroll position when sort changes\n    if (gridRef.current && gridRef.current.scrollToItem) {\n      gridRef.current.scrollToItem(0);\n    }\n  }, [sortBy, sortOrder]);\n\n  useEffect(() => {\n    const cacheViewMode = viewMode === \"grid-large\" ? \"grid\" : viewMode === \"grid-small\" ? \"compact-grid\" : \"list\";\n    albumCache.updateViewMode(cacheViewMode);\n\n    // Reset the grid when view mode changes\n    if (gridRef.current && gridRef.current.resetAfterIndex) {\n      gridRef.current.resetAfterIndex(0, true);\n    }\n  }, [viewMode]);\n\n  // Calculate total album duration from album ID - updated to use duration property first\n  const calculateAlbumDuration = async (album) => {\n    // Use the duration property directly if available\n    if (album.duration) {\n      return formatDuration(album.duration);\n    }\n\n    try {\n      // Get album with songs from cache - properly await the Promise\n      const cachedAlbum = await albumCache.getAlbumWithSongs(album.id);\n\n      if (cachedAlbum && cachedAlbum.songs && cachedAlbum.songs.length > 0) {\n        // Sum up all song durations\n        const totalSeconds = cachedAlbum.songs.reduce(\n          (total, song) => total + (song.duration || 0),\n          0,\n        );\n        return formatDuration(totalSeconds);\n      }\n    } catch (error) {\n      console.error(\"Error calculating album duration:\", error);\n    }\n\n    // Default value if no songs or duration data is available\n    return \"--:--\";\n  };\n\n  // Format seconds into MM:SS or HH:MM:SS format\n  const formatDuration = (seconds) => {\n    if (!seconds) return \"--:--\";\n\n    const hours = Math.floor(seconds / 3600);\n    const minutes = Math.floor((seconds % 3600) / 60);\n    const remainingSeconds = Math.floor(seconds % 60);\n\n    if (hours > 0) {\n      return `${hours}:${minutes.toString().padStart(2, \"0\")}:${remainingSeconds.toString().padStart(2, \"0\")}`;\n    }\n\n    return `${minutes}:${remainingSeconds.toString().padStart(2, \"0\")}`;\n  };\n\n  // Save preferences to localStorage\n  useEffect(() => {\n    if (typeof window !== \"undefined\") {\n      localStorage.setItem(\"albumsSortBy\", sortBy);\n      localStorage.setItem(\"albumsSortOrder\", sortOrder);\n      localStorage.setItem(\"albumsViewMode\", viewMode);\n    }\n  }, [sortBy, sortOrder, viewMode]);\n\n  // Toggle sort order\n  const toggleSortOrder = () => {\n    setSortOrder((prevOrder) => (prevOrder === \"asc\" ? \"desc\" : \"asc\"));\n  };\n\n  // Clear search term\n  const clearSearch = () => {\n    setSearchTerm(\"\");\n  };\n\n  // Show loading indicators\n  const isLoadingInitial = loading && albums.length === 0;\n  const isSearching = searchLoading && searchTerm;\n\n  return (\n    <TooltipProvider>\n      <div className=\"relative\">\n        {/* Sticky Header */}\n        <div className=\"sticky top-0 z-20 bg-white/95 backdrop-blur-sm dark:bg-black/95 pb-4\">\n          <div className=\"flex flex-col gap-8\">\n            {/* Header with title and description */}\n            <div className=\"flex flex-col\">\n              <div className=\"mt-4 text-lg leading-6 font-medium\">Albums</div>\n              <div className=\"opacity-50\">All of your albums in one place.</div>\n            </div>\n\n            {/* Search and filter controls */}\n            <div className=\"flex w-full flex-wrap items-center justify-between gap-4\">\n              <div className=\"relative w-full max-w-md\">\n                <Input\n                  placeholder=\"Search by album title or artist name...\"\n                  value={searchTerm}\n                  onChange={(e) => setSearchTerm(e.target.value)}\n                  className=\"pr-8 pl-8\"\n                />\n                {searchTerm && (\n                  <button\n                    onClick={clearSearch}\n                    className=\"absolute top-1/2 right-2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200\"\n                  >\n                    <IconX size={16} stroke={2} />\n                  </button>\n                )}\n                <div className=\"pointer-events-none absolute top-1/2 left-2 -translate-y-1/2 text-gray-400\">\n                  <IconSearch size={16} stroke={2} />\n                </div>\n              </div>\n\n              {/* Sort and View controls */}\n              <div className=\"flex items-center gap-3\">\n                <div className=\"flex items-center gap-2\">\n                  <Select value={sortBy} onValueChange={setSortBy}>\n                    <SelectTrigger className=\"w-[180px]\">\n                      <SelectValue placeholder=\"Sort by\" />\n                    </SelectTrigger>\n                    <SelectContent>\n                      <SelectItem value=\"name\">Album Title</SelectItem>\n                      <SelectItem value=\"artist\">Artist Name</SelectItem>\n                      <SelectItem value=\"year\">Release Year</SelectItem>\n                      <SelectItem value=\"duration\">Album Duration</SelectItem>\n                    </SelectContent>\n                  </Select>\n                  <Tooltip delayDuration={0}>\n                    <TooltipTrigger asChild>\n                      <Button\n                        variant=\"ghost\"\n                        onClick={toggleSortOrder}\n                        className=\"px-2\"\n                      >\n                        {sortOrder === \"asc\" ? (\n                          <IconSortAscending stroke={2} size={20} />\n                        ) : (\n                          <IconSortDescending stroke={2} size={20} />\n                        )}\n                      </Button>\n                    </TooltipTrigger>\n                    <TooltipContent>\n                      <p>{sortOrder === \"asc\" ? \"Ascending\" : \"Descending\"}</p>\n                    </TooltipContent>\n                  </Tooltip>\n                </div>\n                \n                {/* View Mode */}\n                <div className=\"flex items-center gap-1 rounded-lg border p-1 dark:border-gray-800\">\n                  <Tooltip delayDuration={0}>\n                    <TooltipTrigger asChild>\n                      <button\n                        onClick={() => setViewMode(\"grid-large\")}\n                        className={`rounded p-1 transition ${viewMode === \"grid-large\" ? \"bg-black text-white dark:bg-white dark:text-black\" : \"hover:bg-gray-100 dark:hover:bg-gray-800\"}`}\n                      >\n                        <IconLayoutGrid size={16} />\n                      </button>\n                    </TooltipTrigger>\n                    <TooltipContent>\n                      <p>Large Grid</p>\n                    </TooltipContent>\n                  </Tooltip>\n                  <Tooltip delayDuration={0}>\n                    <TooltipTrigger asChild>\n                      <button\n                        onClick={() => setViewMode(\"grid-small\")}\n                        className={`rounded p-1 transition ${viewMode === \"grid-small\" ? \"bg-black text-white dark:bg-white dark:text-black\" : \"hover:bg-gray-100 dark:hover:bg-gray-800\"}`}\n                      >\n                        <IconGridDots size={16} />\n                      </button>\n                    </TooltipTrigger>\n                    <TooltipContent>\n                      <p>Small Grid</p>\n                    </TooltipContent>\n                  </Tooltip>\n                  <Tooltip delayDuration={0}>\n                    <TooltipTrigger asChild>\n                      <button\n                        onClick={() => setViewMode(\"list\")}\n                        className={`rounded p-1 transition ${viewMode === \"list\" ? \"bg-black text-white dark:bg-white dark:text-black\" : \"hover:bg-gray-100 dark:hover:bg-gray-800\"}`}\n                      >\n                        <IconLayoutList size={16} />\n                      </button>\n                    </TooltipTrigger>\n                    <TooltipContent>\n                      <p>List View</p>\n                    </TooltipContent>\n                  </Tooltip>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n\n      {/* Content Area */}\n      <div className=\"mt-4\">\n        {/* Loading indicators */}\n        {isLoadingInitial || isSearching ? (\n          <div className=\"flex w-full items-center justify-center py-12\">\n            <Spinner className=\"h-6 w-6\" />\n          </div>\n        ) : (\n          <>\n            {filteredAlbums.length > 0 ? (\n              viewMode === \"list\" ? (\n                <div className=\"space-y-1\">\n                  {filteredAlbums.map((album) => (\n                    <Link\n                      key={album.id}\n                      href={`/album/${album.id}`}\n                      className=\"group flex cursor-pointer items-center gap-4 rounded-lg p-3 transition hover:bg-gray-100 dark:hover:bg-gray-800\"\n                    >\n                      <div className=\"relative h-12 w-12 flex-shrink-0 overflow-hidden rounded-lg bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-900\">\n                        {album.cover ? (\n                          <Image\n                            alt={album.name}\n                            src={`wora://${album.cover}`}\n                            fill\n                            loading=\"lazy\"\n                            className=\"object-cover\"\n                          />\n                        ) : (\n                          <div className=\"flex h-full w-full items-center justify-center\">\n                            <IconLayoutGrid size={20} className=\"opacity-50\" />\n                          </div>\n                        )}\n                      </div>\n                      <div className=\"flex-1 min-w-0\">\n                        <p className=\"truncate font-medium\">{album.name}</p>\n                        <p className=\"text-sm opacity-60\">\n                          {album.artist} · {album.year || \"Unknown year\"}\n                        </p>\n                      </div>\n                    </Link>\n                  ))}\n                </div>\n              ) : (\n                <div\n                  ref={gridRef}\n                  className={`grid h-full w-full gap-${viewMode === \"grid-small\" ? \"4\" : \"8\"} ${\n                    viewMode === \"grid-small\" \n                      ? \"grid-cols-4 sm:grid-cols-5 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10\" \n                      : \"grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6\"\n                  }`}\n                >\n                  {filteredAlbums.map((album) => (\n                    <AlbumCard\n                      key={album.id}\n                      album={{ ...album, id: album.id.toString() }}\n                    />\n                  ))}\n                </div>\n              )\n            ) : (\n              <div className=\"flex w-full items-center justify-center p-10 text-gray-500\">\n                {searchTerm\n                  ? \"No albums matching your search\"\n                  : \"No albums found in your library\"}\n              </div>\n            )}\n\n            {loading && filteredAlbums.length > 0 && (\n              <div className=\"flex w-full items-center justify-center py-4\">\n                <Spinner className=\"h-4 w-4\" />\n              </div>\n            )}\n          </>\n        )}\n      </div>\n    </div>\n    </TooltipProvider>\n  );\n}\n"
  },
  {
    "path": "renderer/pages/artists/[name].tsx",
    "content": "import React, { useEffect, useState, useRef } from \"react\";\nimport { useRouter } from \"next/router\";\nimport Image from \"next/image\";\nimport {\n  IconCircleFilled,\n  IconPlayerPlay,\n  IconArrowsShuffle2,\n  IconExternalLink,\n  IconUsers,\n  IconUser,\n  IconMusic,\n  IconInfoCircle,\n  IconCheck,\n} from \"@tabler/icons-react\";\nimport { usePlayer } from \"@/context/playerContext\";\nimport Songs from \"@/components/ui/songs\";\nimport { Button } from \"@/components/ui/button\";\nimport Spinner from \"@/components/ui/spinner\";\n\ntype Album = {\n  id: number;\n  name: string;\n  artist: string;\n  year: number;\n  cover: string;\n  songs: any[];\n};\n\ntype Artist = {\n  name: string;\n  albums: Album[];\n  albumsWithSongs: Album[];\n  songs: any[];\n  stats?: {\n    totalSongs: number;\n    totalAlbums: number;\n    totalDuration: number;\n    genres: string[];\n    formats: string[];\n    yearRange: { start: number; end: number } | null;\n    topSongs: Array<{\n      id: number;\n      name: string;\n      duration: number;\n      album: string;\n    }>;\n  };\n};\n\ntype ArtistInfo = {\n  name: string;\n  mbid?: string;\n  url?: string;\n  image?: Array<{ \"#text\": string; size: string }>;\n  bio?: {\n    summary: string;\n    content: string;\n  };\n  stats?: {\n    listeners: string;\n    playcount: string;\n  };\n  similar?: {\n    artist: Array<{\n      name: string;\n      url: string;\n      image?: Array<{ \"#text\": string; size: string }>;\n    }>;\n  };\n};\n\ntype TopTrack = {\n  name: string;\n  playcount: string;\n  listeners: string;\n  artist: {\n    name: string;\n  };\n};\n\nexport default function ArtistView() {\n  const router = useRouter();\n  const [artist, setArtist] = useState<Artist | null>(null);\n  const [artistInfo, setArtistInfo] = useState<ArtistInfo | null>(null);\n  const [topTracks, setTopTracks] = useState<TopTrack[]>([]);\n  const [similarArtists, setSimilarArtists] = useState<any[]>([]);\n  const [libraryArtists, setLibraryArtists] = useState<any[]>([]);\n  const [activeTab, setActiveTab] = useState(\"overview\");\n  const [loading, setLoading] = useState(true);\n  const [infoLoading, setInfoLoading] = useState(true);\n  const { setQueueAndPlay, song } = usePlayer();\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (typeof window !== \"undefined\") {\n      window.scrollTo(0, 0);\n      if (history.scrollRestoration) {\n        history.scrollRestoration = \"manual\";\n      }\n    }\n    return () => {\n      if (history.scrollRestoration) {\n        history.scrollRestoration = \"auto\";\n      }\n    };\n  }, [router.asPath]);\n\n  useEffect(() => {\n    if (similarArtists && similarArtists.length > 0) {\n      window.ipc.invoke(\"getAllArtists\").then((artists) => {\n        setLibraryArtists(artists || []);\n      });\n    }\n  }, [similarArtists]);\n\n  useEffect(() => {\n    if (!router.query.name) return;\n\n    const artistName = decodeURIComponent(router.query.name as string);\n    \n    setLoading(true);\n    window.ipc.invoke(\"getArtistWithAlbums\", artistName).then((response) => {\n      setArtist(response);\n      setLoading(false);\n    });\n\n    setInfoLoading(true);\n    Promise.all([\n      window.ipc.invoke(\"lastfm:getArtistInfo\", artistName),\n      window.ipc.invoke(\"lastfm:getArtistTopTracks\", artistName),\n      window.ipc.invoke(\"lastfm:getSimilarArtists\", artistName),\n    ])\n      .then(([infoRes, tracksRes, similarRes]) => {\n        if (infoRes.success && infoRes.artist) {\n          setArtistInfo(infoRes.artist);\n        }\n        if (tracksRes.success && tracksRes.toptracks?.track) {\n          setTopTracks(tracksRes.toptracks.track.slice(0, 10));\n        }\n        if (similarRes.success && similarRes.similarartists?.artist) {\n          setSimilarArtists(similarRes.similarartists.artist.slice(0, 6));\n        }\n      })\n      .finally(() => {\n        setInfoLoading(false);\n      });\n  }, [router.query.name]);\n\n  const playAllSongs = () => {\n    if (artist && artist.songs) {\n      setQueueAndPlay(artist.songs, 0);\n    }\n  };\n\n  const playAllSongsAndShuffle = () => {\n    if (artist && artist.songs) {\n      setQueueAndPlay(artist.songs, 0, true);\n    }\n  };\n\n  const playTopTrack = (trackName: string) => {\n    if (artist && artist.songs) {\n      const track = artist.songs.find(s => \n        s.name.toLowerCase().includes(trackName.toLowerCase())\n      );\n      if (track) {\n        const index = artist.songs.indexOf(track);\n        setQueueAndPlay(artist.songs, index);\n      }\n    }\n  };\n\n  const getArtistImage = () => {\n    if (artistInfo?.image && Array.isArray(artistInfo.image)) {\n      const sizes = [\"mega\", \"extralarge\", \"large\", \"medium\", \"small\"];\n      for (const size of sizes) {\n        const image = artistInfo.image.find(img => img.size === size);\n        if (image && image[\"#text\"] && image[\"#text\"].length > 0 && \n            !image[\"#text\"].includes(\"2a96cbd8b46e442fc41c2b86b821562f\") &&\n            !image[\"#text\"].includes(\"c951e87f7c3c76e91f7287e326f2edeb\")) {\n          return image[\"#text\"];\n        }\n      }\n    }\n    return getArtistCover();\n  };\n\n  const getArtistCover = () => {\n    if (artist?.albums && artist.albums.length > 0) {\n      const albumWithCover = artist.albums.find((album) => album.cover);\n      return albumWithCover\n        ? `wora://${albumWithCover.cover}`\n        : \"/coverArt.png\";\n    }\n    return \"/coverArt.png\";\n  };\n\n  const formatNumber = (num: string) => {\n    return parseInt(num).toLocaleString();\n  };\n\n  const cleanBio = (text: string) => {\n    return text.replace(/<a\\s+(?:[^>]*?\\s+)?href=\"([^\"]*)\"[^>]*>(.*?)<\\/a>/gi, '$2');\n  };\n\n  const formatDuration = (seconds: number) => {\n    const hours = Math.floor(seconds / 3600);\n    const minutes = Math.floor((seconds % 3600) / 60);\n    if (hours > 0) {\n      return `${hours}h ${minutes}m`;\n    }\n    return `${minutes} minutes`;\n  };\n\n  const convertTime = (seconds: number) => {\n    const mins = Math.floor(seconds / 60);\n    const secs = Math.floor(seconds % 60);\n    return `${mins}:${secs.toString().padStart(2, '0')}`;\n  };\n\n  if (loading) {\n    return (\n      <div className=\"flex h-96 w-full items-center justify-center\">\n        <Spinner className=\"h-8 w-8\" />\n      </div>\n    );\n  }\n\n  return (\n    <>\n      <div className=\"relative h-96 w-full overflow-hidden rounded-2xl\">\n        {getArtistImage().startsWith(\"http\") ? (\n          <img\n            alt={artist ? artist.name : \"Artist Cover\"}\n            src={getArtistImage()}\n            loading=\"lazy\"\n            className=\"absolute inset-0 h-full w-full object-cover object-center blur-xl opacity-50\"\n          />\n        ) : (\n          <Image\n            alt={artist ? artist.name : \"Artist Cover\"}\n            src={getArtistImage()}\n            fill\n            loading=\"lazy\"\n            className=\"object-cover object-center blur-xl opacity-50\"\n          />\n        )}\n        <div className=\"absolute bottom-6 left-6\">\n          <div className=\"flex items-end gap-6\">\n            <div className=\"relative h-52 w-52 overflow-hidden rounded-xl shadow-2xl transition duration-300\">\n              {getArtistImage().startsWith(\"http\") ? (\n                <img\n                  alt={artist ? artist.name : \"Artist Cover\"}\n                  src={getArtistImage()}\n                  loading=\"lazy\"\n                  className=\"h-full w-full object-cover\"\n                />\n              ) : (\n                <Image\n                  alt={artist ? artist.name : \"Artist Cover\"}\n                  src={getArtistImage()}\n                  fill\n                  loading=\"lazy\"\n                  className=\"object-cover\"\n                />\n              )}\n            </div>\n            <div className=\"flex flex-col gap-4\">\n              <div>\n                <h1 className=\"text-5xl font-bold drop-shadow-lg\">{artist?.name}</h1>\n                <div className=\"mt-2 flex items-center gap-3\">\n                  <p className=\"flex items-center gap-2 text-sm\">\n                    {artist?.albums?.length || 0} Albums\n                    <IconCircleFilled stroke={2} size={5} />\n                    {artist?.songs?.length || 0} Songs\n                  </p>\n                  {artistInfo?.stats && (\n                    <>\n                      <IconCircleFilled stroke={2} size={5} />\n                      <p className=\"text-sm\">\n                        {formatNumber(artistInfo.stats.listeners)} listeners\n                      </p>\n                    </>\n                  )}\n                </div>\n              </div>\n              <div className=\"flex gap-2\">\n                <Button\n                  onClick={playAllSongs}\n                  className=\"flex items-center gap-2 rounded-full bg-white px-6 py-2 text-sm font-medium text-black hover:bg-white/90 dark:bg-white dark:text-black\"\n                >\n                  <IconPlayerPlay\n                    className=\"fill-black dark:fill-black\"\n                    stroke={2}\n                    size={18}\n                  />\n                  Play\n                </Button>\n                <Button\n                  onClick={playAllSongsAndShuffle}\n                  variant=\"outline\"\n                  className=\"flex items-center gap-2 rounded-full px-6 py-2 text-sm font-medium\"\n                >\n                  <IconArrowsShuffle2 stroke={2} size={18} />\n                  Shuffle\n                </Button>\n                {artistInfo?.url && (\n                  <Button\n                    variant=\"ghost\"\n                    className=\"flex items-center gap-2 rounded-full px-4 py-2 text-sm font-medium\"\n                    onClick={() => window.open(artistInfo.url, \"_blank\")}\n                  >\n                    <IconExternalLink stroke={2} size={18} />\n                    Last.fm\n                  </Button>\n                )}\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <div className=\"mt-8\">\n        <div className=\"flex border-b border-gray-200 dark:border-gray-800\">\n          <button\n            className={`px-6 pb-4 text-sm font-medium transition-colors ${\n              activeTab === \"overview\"\n                ? \"border-b-2 border-black text-black dark:border-white dark:text-white\"\n                : \"text-gray-500 hover:text-gray-700 dark:hover:text-gray-300\"\n            }`}\n            onClick={() => setActiveTab(\"overview\")}\n          >\n            Overview\n          </button>\n          <button\n            className={`px-6 pb-4 text-sm font-medium transition-colors ${\n              activeTab === \"albums\"\n                ? \"border-b-2 border-black text-black dark:border-white dark:text-white\"\n                : \"text-gray-500 hover:text-gray-700 dark:hover:text-gray-300\"\n            }`}\n            onClick={() => setActiveTab(\"albums\")}\n          >\n            Albums\n          </button>\n          <button\n            className={`px-6 pb-4 text-sm font-medium transition-colors ${\n              activeTab === \"songs\"\n                ? \"border-b-2 border-black text-black dark:border-white dark:text-white\"\n                : \"text-gray-500 hover:text-gray-700 dark:hover:text-gray-300\"\n            }`}\n            onClick={() => setActiveTab(\"songs\")}\n          >\n            Songs\n          </button>\n        </div>\n\n        {activeTab === \"overview\" && (\n          <div className=\"py-6\">\n            {artist?.stats && (\n              <div className=\"mb-8 grid grid-cols-2 gap-4 md:grid-cols-4\">\n                <div className=\"wora-border rounded-xl p-4\">\n                  <p className=\"text-2xl font-bold\">{artist.stats.totalAlbums}</p>\n                  <p className=\"text-sm opacity-60\">Albums</p>\n                </div>\n                <div className=\"wora-border rounded-xl p-4\">\n                  <p className=\"text-2xl font-bold\">{artist.stats.totalSongs}</p>\n                  <p className=\"text-sm opacity-60\">Songs</p>\n                </div>\n                <div className=\"wora-border rounded-xl p-4\">\n                  <p className=\"text-2xl font-bold\">{formatDuration(artist.stats.totalDuration)}</p>\n                  <p className=\"text-sm opacity-60\">Total Duration</p>\n                </div>\n                {artist.stats.yearRange && (\n                  <div className=\"wora-border rounded-xl p-4\">\n                    <p className=\"text-2xl font-bold\">\n                      {artist.stats.yearRange.start === artist.stats.yearRange.end\n                        ? artist.stats.yearRange.start\n                        : `${artist.stats.yearRange.start}-${artist.stats.yearRange.end}`}\n                    </p>\n                    <p className=\"text-sm opacity-60\">Active Years</p>\n                  </div>\n                )}\n              </div>\n            )}\n\n            <div className=\"grid grid-cols-1 gap-8 lg:grid-cols-3\">\n              {/* Bio Section or Local Info */}\n              <div className=\"lg:col-span-2\">\n                {artistInfo?.bio ? (\n                  <>\n                    <h2 className=\"mb-4 flex items-center gap-2 text-xl font-bold\">\n                      <IconInfoCircle stroke={2} size={24} />\n                      About\n                    </h2>\n                    <div className=\"wora-border rounded-xl p-6\">\n                      <p className=\"text-sm leading-relaxed opacity-90\">\n                        {cleanBio(artistInfo.bio.summary)}\n                      </p>\n                    </div>\n                  </>\n                ) : artist?.stats ? (\n                  <>\n                    <h2 className=\"mb-4 flex items-center gap-2 text-xl font-bold\">\n                      <IconInfoCircle stroke={2} size={24} />\n                      Library Info\n                    </h2>\n                    <div className=\"wora-border rounded-xl p-6\">\n                      <div className=\"space-y-4\">\n                        <div>\n                          <p className=\"mb-2 text-sm font-medium\">Audio Formats</p>\n                          <div className=\"flex flex-wrap gap-2\">\n                            {artist.stats.formats.map((format) => (\n                              <span\n                                key={format}\n                                className=\"rounded-full bg-black/10 px-3 py-1 text-xs dark:bg-white/10\"\n                              >\n                                {format}\n                              </span>\n                            ))}\n                          </div>\n                        </div>\n                        {artist.stats.topSongs.length > 0 && (\n                          <div>\n                            <p className=\"mb-2 text-sm font-medium\">Songs in Library</p>\n                            {artist.stats.topSongs.map((song, idx) => (\n                              <div key={song.id} className=\"flex items-center justify-between py-1\">\n                                <div>\n                                  <p className=\"text-sm\">{song.name}</p>\n                                  <p className=\"text-xs opacity-50\">{song.album}</p>\n                                </div>\n                                <span className=\"text-xs opacity-50\">{convertTime(song.duration)}</span>\n                              </div>\n                            ))}\n                          </div>\n                        )}\n                      </div>\n                    </div>\n                  </>\n                ) : null}\n              </div>\n\n              {/* Top Tracks Section */}\n              {topTracks.length > 0 && (\n                <div>\n                  <h2 className=\"mb-4 flex items-center gap-2 text-xl font-bold\">\n                    <IconMusic stroke={2} size={24} />\n                    Top Tracks\n                  </h2>\n                  <div className=\"wora-border rounded-xl p-2\">\n                    {topTracks.slice(0, 5).map((track, index) => {\n                      // Try to match Last.fm track with local song (fuzzy matching)\n                      const localTrack = artist?.songs?.find(s => {\n                        const localName = s.name.toLowerCase().replace(/[^a-z0-9]/g, '');\n                        const lastFmName = track.name.toLowerCase().replace(/[^a-z0-9]/g, '');\n                        return localName.includes(lastFmName) || lastFmName.includes(localName);\n                      });\n                      const isPlaying = localTrack && song?.id === localTrack.id;\n                      const isClickable = !!localTrack;\n                      \n                      return (\n                        <div\n                          key={index}\n                          className={`group flex items-center justify-between rounded-lg p-3 transition-colors ${\n                            isClickable ? \"cursor-pointer hover:bg-black/5 dark:hover:bg-white/5\" : \"\"\n                          } ${isPlaying ? \"bg-black/5 dark:bg-white/5\" : \"\"}`}\n                          onClick={() => isClickable && playTopTrack(track.name)}\n                        >\n                          <div className=\"flex items-center gap-3\">\n                            <span className=\"text-sm font-bold opacity-50\">\n                              {index + 1}\n                            </span>\n                            <div className=\"flex-1\">\n                              <p className={`text-sm font-medium ${isPlaying ? \"text-blue-500\" : \"\"} ${isClickable ? \"group-hover:text-blue-500\" : \"\"}`}>\n                                {track.name}\n                                {localTrack && (\n                                  <span className=\"ml-2 text-xs opacity-50\">• In Library</span>\n                                )}\n                              </p>\n                              <p className=\"text-xs opacity-50\">\n                                {formatNumber(track.playcount)} plays\n                              </p>\n                            </div>\n                          </div>\n                          {localTrack && (\n                            <IconPlayerPlay \n                              size={16} \n                              className=\"opacity-0 transition-opacity group-hover:opacity-100\"\n                            />\n                          )}\n                        </div>\n                      );\n                    })}\n                  </div>\n                </div>\n              )}\n            </div>\n\n            {/* Empty State for Overview */}\n            {!artistInfo?.bio && !artist?.stats && !topTracks.length && (\n              <div className=\"flex flex-col items-center justify-center py-16\">\n                <IconMusic size={64} className=\"mb-4 opacity-20\" />\n                <h3 className=\"mb-2 text-xl font-medium opacity-60\">No Information Available</h3>\n                <p className=\"text-sm opacity-50\">Artist data will appear here as you play their music</p>\n              </div>\n            )}\n\n            {/* Similar Artists Section */}\n            {similarArtists.length > 0 && (\n              <div className=\"mt-8\">\n                <h2 className=\"mb-4 flex items-center gap-2 text-xl font-bold\">\n                  <IconUsers stroke={2} size={24} />\n                  Similar Artists\n                </h2>\n                <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6\">\n                  {similarArtists.map((simArtist, index) => {\n                    // Check if artist exists in library\n                    const libraryArtist = libraryArtists.find(a => \n                      a.name.toLowerCase() === simArtist.name.toLowerCase()\n                    );\n                    const isInLibrary = !!libraryArtist;\n                    \n                    // Get Last.fm image\n                    let imageUrl = simArtist.image?.find((img: any) => img.size === \"large\")?.[\"#text\"];\n                    \n                    // If it's the placeholder and we have the artist in library, use their album cover\n                    if (imageUrl?.includes(\"2a96cbd8b46e442fc41c2b86b821562f\") && libraryArtist?.cover) {\n                      imageUrl = `wora://${libraryArtist.cover}`;\n                    }\n                    \n                    const isLocalImage = imageUrl?.startsWith(\"wora://\");\n                    const hasRealImage = imageUrl && !imageUrl.includes(\"2a96cbd8b46e442fc41c2b86b821562f\");\n                    \n                    return (\n                      <div\n                        key={index}\n                        className=\"group cursor-pointer\"\n                        onClick={() => router.push(`/artists/${encodeURIComponent(simArtist.name)}`)}\n                      >\n                        <div className=\"relative aspect-square overflow-hidden rounded-xl shadow-lg transition duration-300 group-hover:scale-[1.02] group-hover:shadow-xl\">\n                          {imageUrl && !isLocalImage && hasRealImage ? (\n                            <img\n                              alt={simArtist.name}\n                              src={imageUrl}\n                              loading=\"lazy\"\n                              className=\"h-full w-full object-cover\"\n                            />\n                          ) : isLocalImage ? (\n                            <Image\n                              alt={simArtist.name}\n                              src={imageUrl}\n                              fill\n                              loading=\"lazy\"\n                              className=\"object-cover\"\n                            />\n                          ) : (\n                            <div className=\"flex h-full w-full items-center justify-center bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-900\">\n                              <IconUser stroke={1.5} size={48} className=\"opacity-50\" />\n                            </div>\n                          )}\n                          {isInLibrary && (\n                            <div className=\"absolute bottom-2 right-2 rounded-full bg-black/70 p-1.5 backdrop-blur-sm dark:bg-white/70\">\n                              <IconCheck size={14} className=\"text-white dark:text-black\" />\n                            </div>\n                          )}\n                        </div>\n                        <p className=\"mt-2 line-clamp-2 text-sm font-medium\">\n                          {simArtist.name}\n                          {isInLibrary && (\n                            <span className=\"ml-1 text-xs opacity-50\">• In Library</span>\n                          )}\n                        </p>\n                      </div>\n                    );\n                  })}\n                </div>\n              </div>\n            )}\n          </div>\n        )}\n\n        {/* Albums View */}\n        {activeTab === \"albums\" && (\n          <div className=\"grid grid-cols-2 gap-6 py-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6\">\n            {artist?.albums?.map((album) => (\n              <div\n                key={album.id}\n                className=\"group cursor-pointer\"\n                onClick={() => router.push(`/albums/${album.id}`)}\n              >\n                <div className=\"relative aspect-square overflow-hidden rounded-xl shadow-lg transition duration-300 group-hover:scale-[1.02] group-hover:shadow-xl\">\n                  <Image\n                    alt={album.name}\n                    src={\n                      album.cover ? `wora://${album.cover}` : \"/coverArt.png\"\n                    }\n                    fill\n                    loading=\"lazy\"\n                    className=\"object-cover\"\n                  />\n                </div>\n                <div className=\"mt-2\">\n                  <p className=\"line-clamp-1 text-sm font-medium\">\n                    {album.name}\n                  </p>\n                  <p className=\"text-xs opacity-50\">\n                    {album.year || \"Unknown\"}\n                  </p>\n                </div>\n              </div>\n            ))}\n          </div>\n        )}\n\n        {/* Songs View */}\n        {activeTab === \"songs\" && (\n          <div className=\"py-4\">\n            <Songs library={artist?.songs || []} disableScroll={true} />\n          </div>\n        )}\n      </div>\n    </>\n  );\n}"
  },
  {
    "path": "renderer/pages/artists/index.tsx",
    "content": "import React, { useEffect, useState, useMemo, useCallback, memo } from \"react\";\nimport { useRouter } from \"next/router\";\nimport { useScrollAreaRestoration } from \"@/hooks/useScrollAreaRestoration\";\nimport { useDebounce } from \"@/hooks/useDebounce\";\nimport Image from \"next/image\";\nimport { \n  IconUser, \n  IconSearch, \n  IconLayoutGrid, \n  IconLayoutList,\n  IconGridDots,\n  IconSortAscending,\n  IconSortDescending\n} from \"@tabler/icons-react\";\nimport { Input } from \"@/components/ui/input\";\nimport Spinner from \"@/components/ui/spinner\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { ArtistGridSkeleton } from \"@/components/LoadingSkeletons\";\nimport ErrorBoundary from \"@/components/ErrorBoundary\";\n\ntype ArtistItem = {\n  name: string;\n  albumCount: number;\n  songCount: number;\n  cover?: string;\n};\n\ntype ViewMode = \"grid-large\" | \"grid-small\" | \"list\";\ntype SortBy = \"name\" | \"albums\" | \"songs\";\ntype SortOrder = \"asc\" | \"desc\";\n\n\nexport default function ArtistsPage() {\n  const router = useRouter();\n  const [artists, setArtists] = useState<ArtistItem[]>([]);\n  const [loading, setLoading] = useState(true);\n  const [isSearching, setIsSearching] = useState(false);\n  const [error, setError] = useState<string | null>(null);\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const debouncedSearchQuery = useDebounce(searchQuery, 300);\n\n  useScrollAreaRestoration('artists');\n  \n  \n  const [viewMode, setViewMode] = useState<ViewMode>(() => {\n    if (typeof window !== \"undefined\") {\n      return (localStorage.getItem(\"artistsViewMode\") as ViewMode) || \"grid-large\";\n    }\n    return \"grid-large\";\n  });\n  \n  const [sortBy, setSortBy] = useState<SortBy>(() => {\n    if (typeof window !== \"undefined\") {\n      return (localStorage.getItem(\"artistsSortBy\") as SortBy) || \"name\";\n    }\n    return \"name\";\n  });\n  \n  const [sortOrder, setSortOrder] = useState<SortOrder>(() => {\n    if (typeof window !== \"undefined\") {\n      return (localStorage.getItem(\"artistsSortOrder\") as SortOrder) || \"asc\";\n    }\n    return \"asc\";\n  });\n  \n  useEffect(() => {\n    if (typeof window !== \"undefined\") {\n      localStorage.setItem(\"artistsViewMode\", viewMode);\n    }\n  }, [viewMode]);\n  \n  useEffect(() => {\n    if (typeof window !== \"undefined\") {\n      localStorage.setItem(\"artistsSortBy\", sortBy);\n    }\n  }, [sortBy]);\n  \n  useEffect(() => {\n    if (typeof window !== \"undefined\") {\n      localStorage.setItem(\"artistsSortOrder\", sortOrder);\n    }\n  }, [sortOrder]);\n\n  useEffect(() => {\n    const loadArtists = async () => {\n      setLoading(true);\n      setError(null);\n      try {\n        const allArtists = await window.ipc.invoke(\"getAllArtists\");\n        if (Array.isArray(allArtists)) {\n          setArtists(allArtists);\n        } else {\n          throw new Error(\"Invalid data format received\");\n        }\n      } catch (err) {\n        console.error(\"Error loading artists:\", err);\n        setError(\"Failed to load artists. Please try again.\");\n      } finally {\n        setLoading(false);\n      }\n    };\n\n    loadArtists();\n  }, []);\n\n  useEffect(() => {\n    if (searchQuery !== debouncedSearchQuery) {\n      setIsSearching(true);\n    } else {\n      setIsSearching(false);\n    }\n  }, [searchQuery, debouncedSearchQuery]);\n\n  const filteredArtists = useMemo(() => {\n    let filtered = artists;\n    \n    if (debouncedSearchQuery) {\n      const query = debouncedSearchQuery.toLowerCase();\n      filtered = artists.filter((artist) =>\n        artist.name.toLowerCase().includes(query)\n      );\n    }\n    \n    const sorted = [...filtered].sort((a, b) => {\n      let comparison = 0;\n      \n      switch (sortBy) {\n        case \"albums\":\n          comparison = a.albumCount - b.albumCount;\n          break;\n        case \"songs\":\n          comparison = a.songCount - b.songCount;\n          break;\n        case \"name\":\n        default:\n          comparison = a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });\n          break;\n      }\n      \n      return sortOrder === \"asc\" ? comparison : -comparison;\n    });\n    \n    return sorted;\n  }, [debouncedSearchQuery, artists, sortBy, sortOrder]);\n\n  const handleArtistClick = useCallback((artistName: string) => {\n    router.push(`/artists/${encodeURIComponent(artistName)}`);\n  }, [router]);\n\n  return (\n    <TooltipProvider>\n      <div className=\"relative\">\n        <div className=\"sticky top-0 z-20 bg-white/95 backdrop-blur-sm dark:bg-black/95 pb-4\">\n          <div className=\"flex flex-col gap-8\">\n            <div className=\"flex flex-col\">\n              <div className=\"mt-4 text-lg leading-6 font-medium\">Artists</div>\n              <div className=\"opacity-50\">\n                Browse and discover artists in your music library.\n              </div>\n            </div>\n            \n            <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n            <div className=\"relative max-w-md flex-1\">\n              <IconSearch className=\"absolute left-3 top-1/2 -translate-y-1/2 opacity-50\" size={20} />\n              <Input\n                type=\"text\"\n                placeholder=\"Search artists...\"\n                value={searchQuery}\n                onChange={(e) => setSearchQuery(e.target.value)}\n                className=\"pl-10 pr-10\"\n              />\n              {isSearching && (\n                <div className=\"absolute right-3 top-1/2 -translate-y-1/2\">\n                  <Spinner className=\"h-4 w-4\" />\n                </div>\n              )}\n            </div>\n            \n            <div className=\"flex items-center gap-2\">\n              <div className=\"flex items-center gap-1 rounded-lg border p-1 dark:border-gray-800\">\n                <Tooltip delayDuration={0}>\n                  <TooltipTrigger asChild>\n                    <button\n                      onClick={() => setSortBy(\"name\")}\n                      className={`rounded px-2 py-1 text-xs transition ${sortBy === \"name\" ? \"bg-black text-white dark:bg-white dark:text-black\" : \"hover:bg-gray-100 dark:hover:bg-gray-800\"}`}\n                    >\n                      Name\n                    </button>\n                  </TooltipTrigger>\n                  <TooltipContent>\n                    <p>Sort by name</p>\n                  </TooltipContent>\n                </Tooltip>\n                <Tooltip delayDuration={0}>\n                  <TooltipTrigger asChild>\n                    <button\n                      onClick={() => setSortBy(\"albums\")}\n                      className={`rounded px-2 py-1 text-xs transition ${sortBy === \"albums\" ? \"bg-black text-white dark:bg-white dark:text-black\" : \"hover:bg-gray-100 dark:hover:bg-gray-800\"}`}\n                    >\n                      Albums\n                    </button>\n                  </TooltipTrigger>\n                  <TooltipContent>\n                    <p>Sort by album count</p>\n                  </TooltipContent>\n                </Tooltip>\n                <Tooltip delayDuration={0}>\n                  <TooltipTrigger asChild>\n                    <button\n                      onClick={() => setSortBy(\"songs\")}\n                      className={`rounded px-2 py-1 text-xs transition ${sortBy === \"songs\" ? \"bg-black text-white dark:bg-white dark:text-black\" : \"hover:bg-gray-100 dark:hover:bg-gray-800\"}`}\n                    >\n                      Songs\n                    </button>\n                  </TooltipTrigger>\n                  <TooltipContent>\n                    <p>Sort by song count</p>\n                  </TooltipContent>\n                </Tooltip>\n              </div>\n              \n              <Tooltip delayDuration={0}>\n                <TooltipTrigger asChild>\n                  <button\n                    onClick={() => setSortOrder(sortOrder === \"asc\" ? \"desc\" : \"asc\")}\n                    className=\"rounded-lg border p-2 transition hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-gray-800\"\n                  >\n                    {sortOrder === \"asc\" ? <IconSortAscending size={16} /> : <IconSortDescending size={16} />}\n                  </button>\n                </TooltipTrigger>\n                <TooltipContent>\n                  <p>{sortOrder === \"asc\" ? \"Ascending\" : \"Descending\"}</p>\n                </TooltipContent>\n              </Tooltip>\n              \n              <div className=\"flex items-center gap-1 rounded-lg border p-1 dark:border-gray-800\">\n                <Tooltip delayDuration={0}>\n                  <TooltipTrigger asChild>\n                    <button\n                      onClick={() => setViewMode(\"grid-large\")}\n                      className={`rounded p-1 transition ${viewMode === \"grid-large\" ? \"bg-black text-white dark:bg-white dark:text-black\" : \"hover:bg-gray-100 dark:hover:bg-gray-800\"}`}\n                    >\n                      <IconLayoutGrid size={16} />\n                    </button>\n                  </TooltipTrigger>\n                  <TooltipContent>\n                    <p>Large Grid</p>\n                  </TooltipContent>\n                </Tooltip>\n                <Tooltip delayDuration={0}>\n                  <TooltipTrigger asChild>\n                    <button\n                      onClick={() => setViewMode(\"grid-small\")}\n                      className={`rounded p-1 transition ${viewMode === \"grid-small\" ? \"bg-black text-white dark:bg-white dark:text-black\" : \"hover:bg-gray-100 dark:hover:bg-gray-800\"}`}\n                    >\n                      <IconGridDots size={16} />\n                    </button>\n                  </TooltipTrigger>\n                  <TooltipContent>\n                    <p>Small Grid</p>\n                  </TooltipContent>\n                </Tooltip>\n                <Tooltip delayDuration={0}>\n                  <TooltipTrigger asChild>\n                    <button\n                      onClick={() => setViewMode(\"list\")}\n                      className={`rounded p-1 transition ${viewMode === \"list\" ? \"bg-black text-white dark:bg-white dark:text-black\" : \"hover:bg-gray-100 dark:hover:bg-gray-800\"}`}\n                    >\n                      <IconLayoutList size={16} />\n                    </button>\n                  </TooltipTrigger>\n                  <TooltipContent>\n                    <p>List View</p>\n                  </TooltipContent>\n                </Tooltip>\n              </div>\n            </div>\n            </div>\n            \n            <div className=\"text-sm opacity-60\">\n              {filteredArtists.length} {filteredArtists.length === 1 ? \"artist\" : \"artists\"}\n              {searchQuery && ` matching \"${searchQuery}\"`}\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mt-4\">\n          {error ? (\n            <div className=\"flex h-64 w-full flex-col items-center justify-center\">\n              <p className=\"text-lg text-red-500\">{error}</p>\n              <button\n                onClick={() => window.location.reload()}\n                className=\"mt-4 px-4 py-2 bg-black text-white rounded-lg hover:bg-gray-800 dark:bg-white dark:text-black dark:hover:bg-gray-200 transition\"\n              >\n                Retry\n              </button>\n            </div>\n          ) : loading ? (\n            <ArtistGridSkeleton viewMode={viewMode} />\n          ) : filteredArtists.length === 0 ? (\n            <div className=\"flex h-64 w-full flex-col items-center justify-center\">\n              <IconSearch size={48} className=\"mb-4 opacity-50\" />\n              <p className=\"text-lg opacity-50\">No artists found</p>\n            </div>\n          ) : (\n            <ErrorBoundary>\n              {viewMode === \"grid-large\" && (\n                <div className=\"grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-8\">\n                  {filteredArtists.map((artist) => (\n                    <div\n                      key={artist.name}\n                      className=\"group cursor-pointer\"\n                      onClick={() => handleArtistClick(artist.name)}\n                    >\n                      <div className=\"relative aspect-square overflow-hidden rounded-xl bg-gradient-to-br from-gray-100 to-gray-200 shadow-lg transition duration-300 group-hover:scale-[1.02] group-hover:shadow-xl dark:from-gray-800 dark:to-gray-900\">\n                        {artist.cover ? (\n                          <Image\n                            alt={artist.name}\n                            src={`wora://${artist.cover}`}\n                            fill\n                            loading=\"lazy\"\n                            className=\"object-cover\"\n                            sizes=\"(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw\"\n                          />\n                        ) : (\n                          <div className=\"flex h-full w-full items-center justify-center\">\n                            <IconUser stroke={1.5} size={48} className=\"opacity-50\" />\n                          </div>\n                        )}\n                      </div>\n                      <div className=\"mt-3\">\n                        <p className=\"truncate font-medium\">{artist.name}</p>\n                        <p className=\"text-xs opacity-60\">\n                          {artist.albumCount} {artist.albumCount === 1 ? \"album\" : \"albums\"} · {artist.songCount} {artist.songCount === 1 ? \"song\" : \"songs\"}\n                        </p>\n                      </div>\n                    </div>\n                  ))}\n                </div>\n              )}\n\n              {viewMode === \"grid-small\" && (\n                <div className=\"grid grid-cols-4 gap-3 sm:grid-cols-5 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10 2xl:grid-cols-12\">\n                  {filteredArtists.map((artist) => (\n                    <div\n                      key={artist.name}\n                      className=\"group cursor-pointer\"\n                      onClick={() => handleArtistClick(artist.name)}\n                    >\n                      <div className=\"relative aspect-square overflow-hidden rounded-lg bg-gradient-to-br from-gray-100 to-gray-200 shadow transition duration-300 group-hover:scale-[1.05] group-hover:shadow-lg dark:from-gray-800 dark:to-gray-900\">\n                        {artist.cover ? (\n                          <Image\n                            alt={artist.name}\n                            src={`wora://${artist.cover}`}\n                            fill\n                            loading=\"lazy\"\n                            className=\"object-cover\"\n                            sizes=\"(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw\"\n                          />\n                        ) : (\n                          <div className=\"flex h-full w-full items-center justify-center\">\n                            <IconUser stroke={1.5} size={32} className=\"opacity-50\" />\n                          </div>\n                        )}\n                      </div>\n                      <div className=\"mt-2\">\n                        <p className=\"truncate text-xs font-medium\">{artist.name}</p>\n                        <p className=\"truncate text-xs opacity-50\">\n                          {artist.songCount} songs\n                        </p>\n                      </div>\n                    </div>\n                  ))}\n                </div>\n              )}\n\n              {viewMode === \"list\" && (\n                <div className=\"space-y-1\">\n                  {filteredArtists.map((artist) => (\n                    <div\n                      key={artist.name}\n                      className=\"group flex cursor-pointer items-center gap-4 rounded-lg p-3 transition hover:bg-gray-100 dark:hover:bg-gray-800\"\n                      onClick={() => handleArtistClick(artist.name)}\n                    >\n                      <div className=\"relative h-12 w-12 flex-shrink-0 overflow-hidden rounded-lg bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-900\">\n                        {artist.cover ? (\n                          <Image\n                            alt={artist.name}\n                            src={`wora://${artist.cover}`}\n                            fill\n                            loading=\"lazy\"\n                            className=\"object-cover\"\n                            sizes=\"(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw\"\n                          />\n                        ) : (\n                          <div className=\"flex h-full w-full items-center justify-center\">\n                            <IconUser stroke={1.5} size={20} className=\"opacity-50\" />\n                          </div>\n                        )}\n                      </div>\n                      <div className=\"flex-1 min-w-0\">\n                        <p className=\"truncate font-medium\">{artist.name}</p>\n                        <p className=\"text-sm opacity-60\">\n                          {artist.albumCount} {artist.albumCount === 1 ? \"album\" : \"albums\"} · {artist.songCount} {artist.songCount === 1 ? \"song\" : \"songs\"}\n                        </p>\n                      </div>\n                    </div>\n                  ))}\n                </div>\n              )}\n            </ErrorBoundary>\n          )}\n        </div>\n      </div>\n    </TooltipProvider>\n  );\n}"
  },
  {
    "path": "renderer/pages/home.tsx",
    "content": "import React, { useEffect, useState, useRef } from \"react\";\nimport {\n  Carousel,\n  CarouselContent,\n  CarouselItem,\n  CarouselNext,\n  CarouselPrevious,\n} from \"@/components/ui/carousel\";\nimport AlbumCard from \"@/components/ui/album\";\nimport Songs from \"@/components/ui/songs\";\nimport { Button } from \"@/components/ui/button\";\nimport { IconArrowsShuffle2 } from \"@tabler/icons-react\";\nimport { usePlayer } from \"@/context/playerContext\";\nimport Link from \"next/link\";\n\nexport default function Home() {\n  const [library, setLibrary] = useState<any | null>([]);\n  const [allSongs, setAllSongs] = useState<any[]>([]);\n  const [isLoading, setIsLoading] = useState(false);\n  const { setQueueAndPlay } = usePlayer();\n  const songsRef = useRef(null);\n\n  useEffect(() => {\n    window.ipc.invoke(\"getRandomLibraryItems\").then((response) => {\n      setLibrary(response);\n    });\n\n    // Listen for reset event from main process\n    const resetListener = window.ipc.on(\"resetHomeState\", () => {\n      // Refresh random library items\n      window.ipc.invoke(\"getRandomLibraryItems\").then((response) => {\n        setLibrary(response);\n      });\n    });\n\n    return () => {\n      // Clean up event listener\n      resetListener();\n    };\n  }, []);\n\n  const handleShuffleAllSongs = async () => {\n    setIsLoading(true);\n    try {\n      // Always fetch fresh songs to ensure proper album data\n      const songs = await window.ipc.invoke(\"getAllSongs\");\n\n      // Process songs to ensure album data is complete\n      const processedSongs = songs.map((song) => {\n        // Ensure the song has a proper album structure\n        if (!song.album) {\n          song.album = {\n            id: null,\n            name: \"Unknown Album\",\n            artist: \"Unknown Artist\",\n            cover: null,\n          };\n        }\n\n        // Make sure the album object is complete\n        return {\n          ...song,\n          album: {\n            id: song.album.id || null,\n            name: song.album.name || \"Unknown Album\",\n            artist: song.album.artist || \"Unknown Artist\",\n            cover: song.album.cover || null,\n            year: song.album.year || null,\n          },\n        };\n      });\n\n      setAllSongs(processedSongs);\n\n      // Play all songs in shuffle mode\n      setQueueAndPlay(processedSongs, 0, true);\n    } catch (error) {\n      console.error(\"Error shuffling all songs:\", error);\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  return (\n    <div className=\"flex flex-col gap-8\">\n      <div className=\"flex flex-row items-center justify-between\">\n        <div className=\"flex flex-col\">\n          <div className=\"mt-4 text-lg leading-6 font-medium\">Home</div>\n          <div className=\"opacity-50\">\n            The coolest music library in the world.\n          </div>\n        </div>\n        <Button\n          onClick={handleShuffleAllSongs}\n          className=\"flex items-center gap-2\"\n          disabled={isLoading}\n        >\n          <IconArrowsShuffle2 stroke={2} size={16} />\n          Shuffle All Songs\n        </Button>\n      </div>\n      {library?.albums && library.albums.length > 5 && (\n        <Carousel\n          className=\"relative w-[88vw]\"\n          opts={{\n            loop: true,\n          }}\n        >\n          <CarouselPrevious className=\"absolute left-0 z-50 my-0\" />\n          <div className=\"w-full mask-x-from-70%\">\n            <CarouselContent className=\"-ml-8\">\n              {library.albums.map((album: any, index: number) => (\n                <CarouselItem key={index} className=\"basis-1/5 pl-8\">\n                  <AlbumCard album={album} />\n                </CarouselItem>\n              ))}\n            </CarouselContent>\n          </div>\n          <CarouselNext className=\"absolute right-0 z-50 my-0\" />\n        </Carousel>\n      )}\n      <div>\n        <div className=\"mb-2 flex items-center justify-between\">\n          <h3 className=\"text-sm font-medium\">Recently Added</h3>\n          <Button variant=\"ghost\" className=\"text-xs\" asChild>\n            <Link href=\"/songs\">View All</Link>\n          </Button>\n        </div>\n        <Songs\n          library={library?.songs}\n          ref={songsRef}\n          limit={5}\n          disableScroll={true}\n        />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "renderer/pages/playlists/[slug].tsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport Image from \"next/image\";\nimport { useRouter } from \"next/router\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  IconPlayerPlay,\n  IconArrowsShuffle2,\n  IconX,\n  IconCheck,\n  IconStar,\n  IconTrash,\n  IconArrowRight,\n} from \"@tabler/icons-react\";\nimport { usePlayer } from \"@/context/playerContext\";\nimport { toast } from \"sonner\";\nimport Spinner from \"@/components/ui/spinner\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\nimport Songs from \"@/components/ui/songs\";\nimport { ContextMenuItem } from \"@/components/ui/context-menu\";\n\n// Form validation schema\nconst formSchema = z.object({\n  name: z.string().min(2, {\n    message: \"Playlist name must be at least 2 characters.\",\n  }),\n  description: z.string().optional(),\n});\n\n// Playlist type definition\ntype Playlist = {\n  id: number;\n  name: string;\n  description: string;\n  cover: string;\n  songs: any[];\n};\n\nexport default function Playlist() {\n  const router = useRouter();\n  const [playlist, setPlaylist] = useState<Playlist | null>(null);\n  const [dialogOpen, setDialogOpen] = useState(false);\n  const [loading, setLoading] = useState(false);\n  const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);\n  const { setQueueAndPlay } = usePlayer();\n\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n  });\n\n  // Reset scroll position when the component mounts or route changes\n  useEffect(() => {\n    if (typeof window !== \"undefined\") {\n      window.scrollTo(0, 0);\n    }\n  }, [router.asPath]);\n\n  // Load playlist data when the slug changes\n  useEffect(() => {\n    if (!router.query.slug) return;\n    fetchPlaylistData();\n  }, [router.query.slug]);\n\n  // Update form values when playlist data changes\n  useEffect(() => {\n    if (playlist) {\n      form.reset({\n        name: playlist.name,\n        description: playlist.description,\n      });\n    }\n  }, [playlist, form]);\n\n  const fetchPlaylistData = async () => {\n    try {\n      const response = await window.ipc.invoke(\n        \"getPlaylistWithSongs\",\n        router.query.slug,\n      );\n      setPlaylist(response);\n    } catch (error) {\n      console.error(\"Error fetching playlist:\", error);\n      toast(\n        <div className=\"flex w-fit items-center gap-2 text-xs\">\n          <IconX className=\"text-red-500\" stroke={2} size={16} />\n          Failed to load playlist\n        </div>,\n      );\n    }\n  };\n\n  const playPlaylist = (shuffle = false) => {\n    if (!playlist?.songs?.length) return;\n    setQueueAndPlay(playlist.songs, 0, shuffle);\n  };\n\n  const removeSongFromPlaylist = async (songId: number) => {\n    if (!playlist) return;\n    try {\n      const response = await window.ipc.invoke(\"removeSongFromPlaylist\", {\n        playlistId: playlist.id,\n        songId,\n      });\n      if (response) {\n        toast(\n          <div className=\"flex w-fit items-center gap-2 text-xs\">\n            <IconCheck className=\"text-green-400\" stroke={2} size={16} />\n            Song removed from playlist\n          </div>,\n        );\n        fetchPlaylistData();\n      }\n    } catch (error) {\n      console.error(\"Error removing song:\", error);\n    }\n  };\n\n  const updatePlaylist = async (data: z.infer<typeof formSchema>) => {\n    if (!playlist) return;\n    setLoading(true);\n    try {\n      const response = await window.ipc.invoke(\"updatePlaylist\", {\n        id: playlist.id,\n        data,\n      });\n      if (response) {\n        await fetchPlaylistData();\n        setDialogOpen(false);\n        toast(\n          <div className=\"flex w-fit items-center gap-2 text-xs\">\n            <IconCheck className=\"text-green-400\" stroke={2} size={16} />\n            Playlist updated successfully\n          </div>,\n        );\n      }\n    } catch (error) {\n      console.error(\"Error updating playlist:\", error);\n      toast(\n        <div className=\"flex w-fit items-center gap-2 text-xs\">\n          <IconX className=\"text-red-500\" stroke={2} size={16} />\n          Failed to update playlist\n        </div>,\n      );\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const deletePlaylist = async () => {\n    if (!playlist) return;\n    setLoading(true);\n    try {\n      await window.ipc.invoke(\"deletePlaylist\", { id: playlist.id });\n      toast(\n        <div className=\"flex w-fit items-center gap-2 text-xs\">\n          <IconCheck className=\"text-green-400\" stroke={2} size={16} />\n          Playlist deleted successfully.\n        </div>,\n      );\n      await router.push(\"/playlists\");\n    } catch (err) {\n      toast.error(`Failed to delete playlist: ${err.message}`);\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const renderContextMenuItems = (song: any) => (\n    <ContextMenuItem\n      className=\"flex items-center gap-2\"\n      onClick={() => removeSongFromPlaylist(song.id)}\n    >\n      <IconX stroke={2} size={14} />\n      Remove from Playlist\n    </ContextMenuItem>\n  );\n\n  if (!playlist) {\n    return (\n      <div className=\"flex h-[50vh] w-full items-center justify-center\">\n        <Spinner className=\"h-6 w-6\" />\n      </div>\n    );\n  }\n\n  return (\n    <>\n      <div className=\"relative h-96 w-full overflow-hidden rounded-2xl\">\n        {playlist.id === 1 ? (\n          <div className=\"h-full w-full bg-red-500 mask-b-from-30%\"></div>\n        ) : (\n          <Image\n            alt={playlist.name}\n            src={playlist.cover ? \"wora://\" + playlist.cover : \"/coverArt.png\"}\n            fill\n            loading=\"lazy\"\n            className=\"mask-b-from-30% object-cover object-center blur-xl\"\n          />\n        )}\n        <div className=\"absolute bottom-6 left-6\">\n          <div className=\"flex items-end gap-4\">\n            <div className=\"relative h-52 w-52 overflow-hidden rounded-xl shadow-lg\">\n              <Image\n                alt={playlist.name}\n                src={\n                  playlist.id === 1\n                    ? \"/favouritesCoverArt.png\"\n                    : playlist.cover\n                      ? \"wora://\" + playlist.cover\n                      : \"/coverArt.png\"\n                }\n                fill\n                loading=\"lazy\"\n                className=\"scale-[1.01] object-cover\"\n              />\n            </div>\n            <div className=\"flex flex-col gap-4\">\n              <div>\n                <h1 className=\"text-2xl font-medium\">{playlist.name}</h1>\n                <p className=\"flex items-center gap-2 text-sm\">\n                  {playlist.description}\n                </p>\n              </div>\n              <div className=\"flex gap-2\">\n                <Button onClick={() => playPlaylist(false)} className=\"w-fit\">\n                  <IconPlayerPlay\n                    className=\"fill-black dark:fill-white\"\n                    stroke={2}\n                    size={16}\n                  />{\" \"}\n                  Play\n                </Button>\n                <Button className=\"w-fit\" onClick={() => playPlaylist(true)}>\n                  <IconArrowsShuffle2 stroke={2} size={16} /> Shuffle\n                </Button>\n                {playlist.id !== 1 && (\n                  <Button className=\"w-fit\" onClick={() => setDialogOpen(true)}>\n                    <IconStar stroke={2} size={16} /> Edit\n                  </Button>\n                )}\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n      <div className=\"pt-2\">\n        <Songs\n          library={playlist.songs}\n          renderAdditionalMenuItems={renderContextMenuItems}\n          disableScroll={true}\n        />\n      </div>\n      <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>Update Playlist</DialogTitle>\n            <DialogDescription>Update your existing playlist</DialogDescription>\n          </DialogHeader>\n          <Form {...form}>\n            <form\n              onSubmit={form.handleSubmit(updatePlaylist)}\n              className=\"flex gap-4 text-xs\"\n            >\n              <div>\n                <div className=\"relative h-36 w-36 overflow-hidden rounded-xl\">\n                  <Image\n                    alt=\"album\"\n                    src={\n                      playlist.cover\n                        ? \"wora://\" + playlist.cover\n                        : \"/coverArt.png\"\n                    }\n                    fill\n                    className=\"object-cover\"\n                  />\n                </div>\n              </div>\n              <div className=\"flex h-full w-full flex-col items-end gap-4\">\n                <FormField\n                  control={form.control}\n                  name=\"name\"\n                  render={({ field }) => (\n                    <FormItem className=\"w-full\">\n                      <FormControl>\n                        <Input placeholder=\"Name\" {...field} />\n                      </FormControl>\n                      <FormMessage className=\"text-xs\" />\n                    </FormItem>\n                  )}\n                />\n                <FormField\n                  control={form.control}\n                  name=\"description\"\n                  render={({ field }) => (\n                    <FormItem className=\"w-full\">\n                      <FormControl>\n                        <Input placeholder=\"Description\" {...field} />\n                      </FormControl>\n                      <FormMessage className=\"text-xs\" />\n                    </FormItem>\n                  )}\n                />\n                <div className=\"flex gap-2\">\n                  <Button\n                    className=\"w-fit justify-between text-xs\"\n                    type=\"button\"\n                    variant=\"destructive\"\n                    onClick={() => setConfirmDeleteOpen(true)}\n                    disabled={loading}\n                  >\n                    Delete Playlist\n                    {loading ? (\n                      <Spinner className=\"h-3.5 w-3.5\" />\n                    ) : (\n                      <IconTrash stroke={2} className=\"h-3.5 w-3.5\" />\n                    )}\n                  </Button>\n                  <Button\n                    className=\"w-fit justify-between text-xs\"\n                    type=\"submit\"\n                    disabled={loading}\n                  >\n                    Update Playlist\n                    {loading ? (\n                      <Spinner className=\"h-3.5 w-3.5\" />\n                    ) : (\n                      <IconArrowRight stroke={2} className=\"h-3.5 w-3.5\" />\n                    )}\n                  </Button>\n                </div>\n              </div>\n            </form>\n          </Form>\n        </DialogContent>\n      </Dialog>\n      <Dialog open={confirmDeleteOpen} onOpenChange={setConfirmDeleteOpen}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>Confirm Delete</DialogTitle>\n            <DialogDescription>\n              Are you sure you want to delete this playlist? This action cannot\n              be undone.\n            </DialogDescription>\n          </DialogHeader>\n          <div className=\"flex justify-end gap-2 pt-2\">\n            <Button\n              className=\"w-fit justify-between text-xs\"\n              variant=\"outline\"\n              onClick={() => setConfirmDeleteOpen(false)}\n              disabled={loading}\n            >\n              Cancel\n            </Button>\n            <Button\n              className=\"w-fit justify-between text-xs\"\n              variant=\"destructive\"\n              onClick={async () => {\n                await deletePlaylist();\n                setConfirmDeleteOpen(false);\n                setDialogOpen(false);\n              }}\n              disabled={loading}\n            >\n              {loading ? <Spinner className=\"h-3.5 w-3.5\" /> : \"Delete\"}\n            </Button>\n          </div>\n        </DialogContent>\n      </Dialog>\n    </>\n  );\n}\n"
  },
  {
    "path": "renderer/pages/playlists.tsx",
    "content": "\"use client\";\nimport React, { useEffect, useState } from \"react\";\nimport Image from \"next/image\";\nimport Link from \"next/link\";\nimport { Label } from \"@/components/ui/label\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { z } from \"zod\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useForm } from \"react-hook-form\";\nimport { toast } from \"sonner\";\nimport { IconArrowRight, IconPlus, IconX } from \"@tabler/icons-react\";\nimport { useRouter } from \"next/router\";\nimport { Button } from \"@/components/ui/button\";\nimport Spinner from \"@/components/ui/spinner\";\n\nconst formSchema = z.object({\n  name: z.string().min(2, {\n    message: \"Playlist name must be at least 2 characters.\",\n  }),\n  description: z.string().optional(),\n  playlistCover: z.any().optional(),\n});\n\nexport default function Playlists() {\n  const router = useRouter();\n  const [playlists, setPlaylists] = useState<any[]>([]);\n  const [previewUrl, setPreviewUrl] = useState(\"\");\n  const [dialogOpen, setDialogOpen] = useState(false);\n  const [loading, setLoading] = useState(false);\n\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n    defaultValues: { name: \"\", description: \"\", playlistCover: undefined },\n  });\n\n  useEffect(() => {\n    const load = () =>\n      window.ipc.invoke(\"getAllPlaylists\").then((resp) => setPlaylists(resp));\n    load();\n    const resetListener = window.ipc.on(\"resetPlaylistsState\", load);\n    return () => {\n      resetListener();\n    };\n  }, []);\n\n  useEffect(() => {\n    return () => {\n      if (previewUrl.startsWith(\"blob:\")) URL.revokeObjectURL(previewUrl);\n    };\n  }, [previewUrl]);\n\n  const createPlaylist = async (data: z.infer<typeof formSchema>) => {\n    setLoading(true);\n    let playlistCoverPath = null;\n    try {\n      const files = data.playlistCover as FileList | undefined;\n      if (files && files.length > 0) {\n        const file = files[0];\n        const buffer = await file.arrayBuffer();\n        playlistCoverPath = await window.ipc.invoke(\"uploadPlaylistCover\", {\n          name: file.name,\n          data: Array.from(new Uint8Array(buffer)),\n        });\n      }\n      const resp = await window.ipc.invoke(\"createPlaylist\", {\n        name: data.name,\n        description: data.description,\n        cover: playlistCoverPath,\n      });\n      setDialogOpen(false);\n      setPreviewUrl(\"\");\n      form.reset();\n      router.push(`/playlists/${resp.lastInsertRowid}`);\n    } catch {\n      toast(\n        <div className=\"flex w-fit items-center gap-2 text-xs\">\n          <IconX className=\"text-red-500\" stroke={2} size={16} />\n          Failed to create playlist. Please try again.\n        </div>\n      );\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  return (\n    <div className=\"flex flex-col gap-8\">\n      <div className=\"flex w-full items-center justify-between\">\n        <div className=\"flex flex-col\">\n          <div className=\"mt-4 text-lg font-medium leading-6\">Playlists</div>\n          <div className=\"opacity-50\">\n            Most awesome, epic playlists created by you.\n          </div>\n        </div>\n        <Button variant=\"default\" onClick={() => setDialogOpen(true)}>\n          Create Playlist <IconPlus size={14} />\n        </Button>\n      </div>\n      <div className=\"grid w-full grid-cols-5 gap-8\">\n        {playlists.map((pl) => (\n          <Link key={pl.id} href={`/playlists/${pl.id}`} passHref>\n            <div className=\"group/album wora-border wora-transition rounded-2xl p-5 hover:bg-black/5 dark:hover:bg-white/10\">\n              <div className=\"relative flex flex-col justify-between\">\n                <div className=\"relative w-full overflow-hidden rounded-xl pb-[100%] shadow-lg\">\n                  <Image\n                    alt={pl.name || \"Playlist Cover\"}\n                    src={\n                      pl.id === 1\n                        ? \"/favouritesCoverArt.png\"\n                        : pl.cover\n                          ? \"wora://\" + pl.cover\n                          : \"/coverArt.png\"\n                    }\n                    fill\n                    loading=\"lazy\"\n                    className=\"z-10 object-cover\"\n                  />\n                </div>\n                <div className=\"mt-8 flex w-full flex-col overflow-hidden\">\n                  <p className=\"truncate text-sm font-medium\">{pl.name}</p>\n                  <p className=\"truncate opacity-50\">{pl.description}</p>\n                </div>\n              </div>\n            </div>\n          </Link>\n        ))}\n      </div>\n      <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>\n        <DialogContent>\n          <DialogHeader>\n            <DialogTitle>Create Playlist</DialogTitle>\n            <DialogDescription>\n              Add a new playlist to your library.\n            </DialogDescription>\n          </DialogHeader>\n          <Form {...form}>\n            <form\n              onSubmit={form.handleSubmit(createPlaylist)}\n              className=\"flex gap-4 text-xs\"\n            >\n              <FormField\n                control={form.control}\n                name=\"playlistCover\"\n                render={({ field: { onChange, value, ...rest } }) => (\n                  <FormItem>\n                    <Label\n                      htmlFor=\"playlistCover\"\n                      className=\"wora-transition block cursor-pointer hover:opacity-50\"\n                    >\n                      <div className=\"relative h-36 w-36 overflow-hidden rounded-lg shadow-lg\">\n                        <Image\n                          alt=\"Cover Preview\"\n                          src={previewUrl || \"/coverArt.png\"}\n                          fill\n                          className=\"object-cover\"\n                        />\n                      </div>\n                    </Label>\n                    <FormControl>\n                      <Input\n                        id=\"playlistCover\"\n                        type=\"file\"\n                        accept=\"image/*\"\n                        className=\"hidden\"\n                        onChange={(e) => {\n                          const files = e.target.files;\n                          if (files?.length) {\n                            onChange(files);\n                            const url = URL.createObjectURL(files[0]);\n                            setPreviewUrl(url);\n                          }\n                        }}\n                        {...rest}\n                      />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n              <div className=\"flex h-full w-full flex-col items-end justify-between gap-4\">\n                <div className=\"flex w-full flex-col gap-2\">\n                  <FormField\n                    control={form.control}\n                    name=\"name\"\n                    render={({ field }) => (\n                      <FormItem className=\"w-full\">\n                        <FormControl>\n                          <Input placeholder=\"Name\" {...field} />\n                        </FormControl>\n                        <FormMessage className=\"text-xs\" />\n                      </FormItem>\n                    )}\n                  />\n                  <FormField\n                    control={form.control}\n                    name=\"description\"\n                    render={({ field }) => (\n                      <FormItem className=\"w-full\">\n                        <FormControl>\n                          <Input placeholder=\"Description\" {...field} />\n                        </FormControl>\n                        <FormMessage className=\"text-xs\" />\n                      </FormItem>\n                    )}\n                  />\n                </div>\n                <Button\n                  className=\"w-fit justify-between text-xs\"\n                  type=\"submit\"\n                  disabled={loading}\n                >\n                  Create Playlist\n                  {loading ? (\n                    <Spinner className=\"h-3.5 w-3.5\" />\n                  ) : (\n                    <IconArrowRight stroke={2} className=\"h-3.5 w-3.5\" />\n                  )}\n                </Button>\n              </div>\n            </form>\n          </Form>\n        </DialogContent>\n      </Dialog>\n    </div>\n  );\n}\n"
  },
  {
    "path": "renderer/pages/settings.tsx",
    "content": "import React, { useEffect, useState, useRef } from \"react\";\nimport {\n  IconArrowRight,\n  IconBrandLastfm,\n  IconCheck,\n  IconRefresh,\n  IconLogout,\n  IconX,\n} from \"@tabler/icons-react\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormMessage,\n  FormLabel,\n  FormDescription,\n} from \"@/components/ui/form\";\nimport { Button } from \"@/components/ui/button\";\nimport Spinner from \"@/components/ui/spinner\";\nimport { Avatar, AvatarImage } from \"@/components/ui/avatar\";\nimport { toast } from \"sonner\";\nimport { Label } from \"@/components/ui/label\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { Slider } from \"@/components/ui/slider\";\nimport {\n  initializeLastFM,\n  getSessionKey,\n  logout as lastFmLogout,\n  getUserInfo,\n  initializeLastFMWithSession,\n} from \"@/lib/lastfm\";\n\nconst formSchema = z.object({\n  name: z.string().min(2, {\n    message: \"Username must be at least 2 characters long.\",\n  }),\n  profilePicture: z.any().optional(),\n});\n\nconst lastFmFormSchema = z.object({\n  lastFmUsername: z.string().min(1, {\n    message: \"Username is required.\",\n  }),\n  lastFmPassword: z.string().min(1, {\n    message: \"Password is required.\",\n  }),\n});\n\nconst lastFmSettingsSchema = z.object({\n  enableLastFm: z.boolean().default(false),\n  scrobbleThreshold: z.number().min(50).max(100).default(50),\n});\n\ntype Settings = {\n  name: string;\n  profilePicture: string;\n  musicFolder: string;\n  lastFmUsername?: string;\n  lastFmSessionKey?: string;\n  enableLastFm?: boolean;\n  scrobbleThreshold?: number;\n};\n\ntype LastFmSettings = {\n  lastFmUsername: string | null;\n  lastFmSessionKey: string | null;\n  enableLastFm: boolean;\n  scrobbleThreshold: number;\n};\n\nexport default function Settings() {\n  const [settings, setSettings] = useState<Settings | null>(null);\n  const [loading, setLoading] = useState(false);\n  const [lastFmLoading, setLastFmLoading] = useState(false);\n  const [lastFmSettings, setLastFmSettings] = useState<LastFmSettings>({\n    lastFmUsername: null,\n    lastFmSessionKey: null,\n    enableLastFm: false,\n    scrobbleThreshold: 50,\n  });\n  const [musicLoading, setMusicLoading] = useState(false);\n  const [previewUrl, setPreviewUrl] = useState(\"\");\n  const [stats, setStats] = useState<{\n    songs: number;\n    albums: number;\n    playlists: number;\n  } | null>(null);\n  const [lastFmUserInfo, setLastFmUserInfo] = useState(null);\n  const [logoutLoading, setLogoutLoading] = useState(false);\n\n  useEffect(() => {\n    window.ipc.invoke(\"getSettings\").then((response) => {\n      setSettings(response);\n      setPreviewUrl(\n        response?.profilePicture\n          ? `wora://${response.profilePicture}`\n          : \"/userPicture.png\",\n      );\n    });\n\n    window.ipc.invoke(\"getLastFmSettings\").then((response) => {\n      setLastFmSettings(response);\n      lastFmSettingsForm.reset({\n        enableLastFm: response.enableLastFm,\n        scrobbleThreshold: response.scrobbleThreshold,\n      });\n\n      // Fetch Last.fm user info if we have a session\n      if (\n        response.lastFmUsername &&\n        response.lastFmSessionKey &&\n        response.enableLastFm\n      ) {\n        initializeLastFMWithSession(\n          response.lastFmSessionKey,\n          response.lastFmUsername,\n        );\n        fetchUserInfo();\n      }\n    });\n\n    window.ipc.invoke(\"getLibraryStats\").then((response) => {\n      setStats(response);\n    });\n  }, []);\n\n  // Fetch Last.fm user info\n  const fetchUserInfo = async () => {\n    try {\n      const userInfo = await getUserInfo();\n      if (userInfo) {\n        setLastFmUserInfo(userInfo);\n        console.log(\"Last.fm user info:\", userInfo);\n      }\n    } catch (error) {\n      console.error(\"Failed to fetch Last.fm user info:\", error);\n    }\n  };\n\n  const updateSettings = async (data: z.infer<typeof formSchema>) => {\n    setLoading(true);\n\n    let profilePicturePath = settings?.profilePicture;\n\n    if (\n      data.profilePicture &&\n      data.profilePicture instanceof FileList &&\n      data.profilePicture.length > 0\n    ) {\n      const file = data.profilePicture[0];\n      const fileData = await file.arrayBuffer();\n      try {\n        profilePicturePath = await window.ipc.invoke(\"uploadProfilePicture\", {\n          name: file.name,\n          data: Array.from(new Uint8Array(fileData)),\n        });\n      } catch (error) {\n        console.error(\"Error uploading profile picture:\", error);\n        toast(\n          <div className=\"flex w-fit items-center gap-2 text-xs\">\n            <IconX className=\"text-red-500\" stroke={2} size={16} />\n            Failed to upload profile picture. Using existing picture.\n          </div>,\n        );\n        // Fallback to the original profile picture\n        profilePicturePath = settings?.profilePicture;\n      }\n    } else {\n      // No new file selected, use the existing profile picture\n      profilePicturePath = settings?.profilePicture;\n    }\n\n    const updatedData = {\n      name: data.name,\n      profilePicture: profilePicturePath,\n    };\n\n    await window.ipc.invoke(\"updateSettings\", updatedData).then((response) => {\n      if (response) {\n        setLoading(false);\n        setSettings((prevSettings) => ({ ...prevSettings, ...updatedData }));\n        toast.success(\"Your settings are updated.\");\n      }\n    });\n  };\n\n  const connectToLastFm = async (data: z.infer<typeof lastFmFormSchema>) => {\n    setLastFmLoading(true);\n    try {\n      // Initialize LastFM and get a session key\n      const success = await initializeLastFM(\n        data.lastFmUsername,\n        data.lastFmPassword,\n      );\n      if (success) {\n        const sessionKey = getSessionKey();\n\n        if (sessionKey) {\n          // Save the Last.fm settings\n          await window.ipc.invoke(\"updateLastFmSettings\", {\n            lastFmUsername: data.lastFmUsername,\n            lastFmSessionKey: sessionKey,\n            enableLastFm: true,\n            scrobbleThreshold: lastFmSettings.scrobbleThreshold || 50,\n          });\n\n          // Update local state\n          setLastFmSettings({\n            lastFmUsername: data.lastFmUsername,\n            lastFmSessionKey: sessionKey,\n            enableLastFm: true,\n            scrobbleThreshold: lastFmSettings.scrobbleThreshold || 50,\n          });\n\n          // Fetch user info\n          await fetchUserInfo();\n\n          // Reset form\n          lastFmForm.reset();\n\n          // Update settings form\n          lastFmSettingsForm.reset({\n            enableLastFm: true,\n            scrobbleThreshold: lastFmSettings.scrobbleThreshold || 50,\n          });\n\n          toast(\n            <div className=\"flex w-fit items-center gap-2 text-xs\">\n              <IconCheck className=\"text-green-400\" stroke={2} size={16} />\n              Successfully connected to Last.fm!\n            </div>,\n          );\n        }\n      } else {\n        toast(\n          <div className=\"flex w-fit items-center gap-2 text-xs\">\n            <IconX className=\"text-red-500\" stroke={2} size={16} />\n            Failed to connect to Last.fm. Check your credentials.\n          </div>,\n        );\n      }\n    } catch (error) {\n      console.error(\"Error connecting to Last.fm:\", error);\n      toast(\n        <div className=\"flex w-fit items-center gap-2 text-xs\">\n          <IconX className=\"text-red-500\" stroke={2} size={16} />\n          An error occurred while connecting to Last.fm.\n        </div>,\n      );\n    } finally {\n      setLastFmLoading(false);\n    }\n  };\n\n  const disconnectFromLastFm = async () => {\n    setLogoutLoading(true);\n    try {\n      // Clear Last.fm session\n      lastFmLogout();\n\n      // Update database\n      await window.ipc.invoke(\"updateLastFmSettings\", {\n        lastFmUsername: null,\n        lastFmSessionKey: null,\n        enableLastFm: false,\n        scrobbleThreshold: lastFmSettings.scrobbleThreshold || 50,\n      });\n\n      // Update local state\n      setLastFmSettings({\n        lastFmUsername: null,\n        lastFmSessionKey: null,\n        enableLastFm: false,\n        scrobbleThreshold: lastFmSettings.scrobbleThreshold || 50,\n      });\n\n      // Clear user info\n      setLastFmUserInfo(null);\n\n      toast(\n        <div className=\"flex w-fit items-center gap-2 text-xs\">\n          <IconCheck className=\"text-green-400\" stroke={2} size={16} />\n          Successfully disconnected from Last.fm\n        </div>,\n      );\n    } catch (error) {\n      console.error(\"Error disconnecting from Last.fm:\", error);\n      toast(\n        <div className=\"flex w-fit items-center gap-2 text-xs\">\n          <IconX className=\"text-red-500\" stroke={2} size={16} />\n          Failed to disconnect from Last.fm\n        </div>,\n      );\n    } finally {\n      setLogoutLoading(false);\n    }\n  };\n\n  const updateLastFmSettings = async (\n    data: z.infer<typeof lastFmSettingsSchema>,\n  ) => {\n    try {\n      await window.ipc.invoke(\"updateLastFmSettings\", {\n        lastFmUsername: lastFmSettings.lastFmUsername,\n        lastFmSessionKey: lastFmSettings.lastFmSessionKey,\n        enableLastFm: data.enableLastFm,\n        scrobbleThreshold: data.scrobbleThreshold,\n      });\n\n      setLastFmSettings({\n        ...lastFmSettings,\n        enableLastFm: data.enableLastFm,\n        scrobbleThreshold: data.scrobbleThreshold,\n      });\n\n      toast(\n        <div className=\"flex w-fit items-center gap-2 text-xs\">\n          <IconCheck className=\"text-green-400\" stroke={2} size={16} />\n          Last.fm settings updated successfully.\n        </div>,\n      );\n    } catch (error) {\n      console.error(\"Error updating Last.fm settings:\", error);\n      toast(\n        <div className=\"flex w-fit items-center gap-2 text-xs\">\n          <IconX className=\"text-red-500\" stroke={2} size={16} />\n          Failed to update Last.fm settings.\n        </div>,\n      );\n    }\n  };\n\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n  });\n\n  const lastFmForm = useForm<z.infer<typeof lastFmFormSchema>>({\n    resolver: zodResolver(lastFmFormSchema),\n    defaultValues: {\n      lastFmUsername: \"\",\n      lastFmPassword: \"\",\n    },\n  });\n\n  const lastFmSettingsForm = useForm<z.infer<typeof lastFmSettingsSchema>>({\n    resolver: zodResolver(lastFmSettingsSchema),\n    defaultValues: {\n      enableLastFm: lastFmSettings.enableLastFm,\n      scrobbleThreshold: lastFmSettings.scrobbleThreshold,\n    },\n  });\n\n  useEffect(() => {\n    if (settings) {\n      form.reset({\n        name: settings.name,\n        profilePicture: settings.profilePicture,\n      });\n    }\n  }, [settings]);\n\n  const rescanLibrary = () => {\n    setMusicLoading(true);\n    window.ipc\n      .invoke(\"rescanLibrary\")\n      .then(() => {\n        setMusicLoading(false);\n        toast(\n          <div className=\"flex w-fit items-center gap-2 text-xs\">\n            <IconCheck className=\"text-green-400\" stroke={2} size={16} />\n            Your library is rescanned.\n          </div>,\n        );\n        window.ipc.invoke(\"getLibraryStats\").then((response) => {\n          setStats(response);\n        });\n      })\n      .catch(() => setMusicLoading(false));\n  };\n\n  const scanLibrary = () => {\n    setMusicLoading(true);\n    window.ipc\n      .invoke(\"scanLibrary\", true)\n      .then((response) => {\n        setMusicLoading(false);\n        if (response) return;\n        toast(\n          <div className=\"flex w-fit items-center gap-2 text-xs\">\n            <IconCheck className=\"text-green-400\" stroke={2} size={16} />\n            Your music folder is updated.\n          </div>,\n        );\n        window.ipc.invoke(\"getSettings\").then((response) => {\n          setSettings(response);\n          setPreviewUrl(\n            response?.profilePicture\n              ? `wora://${response.profilePicture}`\n              : \"/userPicture.png\",\n          );\n        });\n\n        window.ipc.invoke(\"getLibraryStats\").then((response) => {\n          setStats(response);\n        });\n      })\n      .catch(() => setMusicLoading(false));\n  };\n\n  useEffect(() => {\n    return () => {\n      if (previewUrl.startsWith(\"blob:\")) {\n        URL.revokeObjectURL(previewUrl);\n      }\n    };\n  }, [previewUrl]);\n\n  return (\n    <div className=\"flex flex-col gap-8\">\n      <div className=\"flex flex-col gap-8\">\n        <div className=\"flex flex-col\">\n          <div className=\"mt-4 text-lg leading-6 font-medium\">Settings</div>\n          <div className=\"opacity-50\">You&apos;re on your own here.</div>\n        </div>\n        <div className=\"relative flex w-full flex-col gap-8\">\n          <div className=\"flex w-full items-center gap-8\">\n            <div className=\"wora-border h-48 w-2/5 rounded-2xl p-6\">\n              <Form {...form}>\n                <form\n                  onSubmit={form.handleSubmit(updateSettings)}\n                  className=\"flex h-full flex-col justify-between text-xs\"\n                >\n                  <div className=\"flex w-full items-center gap-4\">\n                    <Label\n                      className=\"wora-transition w-fit cursor-pointer hover:opacity-50\"\n                      htmlFor=\"profilePicture\"\n                    >\n                      <Avatar className=\"h-20 w-20\">\n                        <AvatarImage src={previewUrl} />\n                      </Avatar>\n                    </Label>\n                    <FormField\n                      control={form.control}\n                      name=\"profilePicture\"\n                      render={({ field: { onChange, value, ...rest } }) => {\n                        const fileInputRef = useRef<HTMLInputElement>(null);\n                        return (\n                          <FormItem hidden className=\"w-full\">\n                            <FormControl>\n                              <Input\n                                id=\"profilePicture\"\n                                placeholder=\"Picture\"\n                                type=\"file\"\n                                accept=\"image/*\"\n                                onChange={(e) => {\n                                  const files = e.target.files;\n                                  if (files && files.length > 0) {\n                                    const file = files[0];\n                                    onChange(files);\n                                    const objectUrl = URL.createObjectURL(file);\n                                    setPreviewUrl(objectUrl);\n                                  }\n                                }}\n                                ref={fileInputRef}\n                                {...rest}\n                              />\n                            </FormControl>\n                            <FormMessage className=\"text-xs\" />\n                          </FormItem>\n                        );\n                      }}\n                    />\n                    <div className=\"flex flex-col\">\n                      <p className=\"text-sm font-medium\">\n                        {settings && settings.name\n                          ? settings.name\n                          : \"Wora User\"}\n                      </p>\n                      <p className=\"opacity-50\">A great listener of music.</p>\n                    </div>\n                  </div>\n                  <div className=\"flex w-full items-center gap-2\">\n                    <FormField\n                      control={form.control}\n                      name=\"name\"\n                      render={({ field }) => (\n                        <FormItem className=\"w-full\">\n                          <FormControl>\n                            <Input\n                              placeholder=\"A username would be great.\"\n                              {...field}\n                            />\n                          </FormControl>\n                          <FormMessage className=\"text-xs\" />\n                        </FormItem>\n                      )}\n                    />\n                    <Button\n                      className=\"w-fit justify-between text-xs\"\n                      type=\"submit\"\n                    >\n                      Save\n                      {loading ? (\n                        <Spinner className=\"h-3.5 w-3.5\" />\n                      ) : (\n                        <IconArrowRight stroke={2} className=\"h-3.5 w-3.5\" />\n                      )}\n                    </Button>\n                  </div>\n                </form>\n              </Form>\n            </div>\n            <div className=\"wora-border h-48 w-3/5 rounded-2xl p-6\">\n              <div className=\"flex h-full flex-col justify-between text-xs\">\n                <div className=\"flex w-full items-center gap-4\">\n                  <div className=\"mt-4 flex w-full justify-around\">\n                    <div className=\"flex flex-col items-center gap-2\">\n                      Songs\n                      <p className=\"text-xl font-medium\">\n                        {stats && stats.songs}\n                      </p>\n                    </div>\n                    <div className=\"flex flex-col items-center gap-2\">\n                      Albums\n                      <p className=\"text-xl font-medium\">\n                        {stats && stats.albums}\n                      </p>\n                    </div>\n                    <div className=\"flex flex-col items-center gap-2\">\n                      Playlists\n                      <p className=\"text-xl font-medium\">\n                        {stats && stats.playlists}\n                      </p>\n                    </div>\n                  </div>\n                </div>\n                <div className=\"flex w-full items-center gap-2\">\n                  <Input\n                    value={settings && settings.musicFolder}\n                    className=\"w-full\"\n                    disabled\n                  />\n                  <Button\n                    className=\"w-fit justify-between text-xs text-nowrap\"\n                    onClick={rescanLibrary}\n                  >\n                    <IconRefresh stroke={2} className=\"h-3.5 w-3.5\" />\n                  </Button>\n                  <Button\n                    className=\"w-fit justify-between text-xs text-nowrap\"\n                    onClick={scanLibrary}\n                  >\n                    Update Music Folder\n                    {musicLoading ? (\n                      <Spinner className=\"h-3.5 w-3.5\" />\n                    ) : (\n                      <IconArrowRight stroke={2} className=\"h-3.5 w-3.5\" />\n                    )}\n                  </Button>\n                </div>\n              </div>\n            </div>\n          </div>\n\n          {/* Last.fm Integration Section */}\n          <div className=\"flex w-full flex-col gap-4\">\n            <div className=\"flex items-center gap-2\">\n              <IconBrandLastfm stroke={2} size={20} className=\"text-red-500\" />\n              <h2 className=\"text-lg font-medium\">Last.fm Integration</h2>\n            </div>\n\n            {lastFmSettings.lastFmSessionKey ? (\n              // Show Last.fm settings if connected\n              <div className=\"wora-border rounded-2xl p-6\">\n                <Form {...lastFmSettingsForm}>\n                  <form\n                    onSubmit={lastFmSettingsForm.handleSubmit(\n                      updateLastFmSettings,\n                    )}\n                    className=\"flex flex-col gap-4\"\n                  >\n                    <div className=\"flex flex-col gap-4\">\n                      <div className=\"flex items-center justify-between\">\n                        <div className=\"flex flex-col\">\n                          <div className=\"flex items-center gap-2\">\n                            <p className=\"text-sm font-medium\">\n                              Connected as: {lastFmSettings.lastFmUsername}\n                            </p>\n                            {lastFmUserInfo &&\n                              lastFmUserInfo.image &&\n                              lastFmUserInfo.image.length > 0 && (\n                                <Avatar className=\"h-6 w-6\">\n                                  <AvatarImage\n                                    src={lastFmUserInfo.image[1][\"#text\"]}\n                                  />\n                                </Avatar>\n                              )}\n                          </div>\n                          <p className=\"text-xs opacity-50\">\n                            Scrobble your played tracks to Last.fm\n                          </p>\n                          {lastFmUserInfo && (\n                            <a\n                              href={`https://www.last.fm/user/${lastFmSettings.lastFmUsername}`}\n                              target=\"_blank\"\n                              rel=\"noopener noreferrer\"\n                              className=\"mt-1 text-xs text-red-500 hover:underline\"\n                            >\n                              View profile ({lastFmUserInfo.playcount || 0}{\" \"}\n                              scrobbles)\n                            </a>\n                          )}\n                        </div>\n                        <div className=\"flex items-center gap-3\">\n                          <Button\n                            type=\"button\"\n                            variant=\"outline\"\n                            onClick={disconnectFromLastFm}\n                            disabled={logoutLoading}\n                            className=\"flex items-center gap-1\"\n                          >\n                            {logoutLoading ? (\n                              <Spinner className=\"h-3.5 w-3.5\" />\n                            ) : (\n                              <>\n                                <IconLogout stroke={2} size={14} />\n                                Logout\n                              </>\n                            )}\n                          </Button>\n                          <FormField\n                            control={lastFmSettingsForm.control}\n                            name=\"enableLastFm\"\n                            render={({ field }) => (\n                              <FormItem className=\"flex items-center space-x-2\">\n                                <FormControl>\n                                  <Switch\n                                    checked={field.value}\n                                    onCheckedChange={field.onChange}\n                                  />\n                                </FormControl>\n                              </FormItem>\n                            )}\n                          />\n                        </div>\n                      </div>\n\n                      <FormField\n                        control={lastFmSettingsForm.control}\n                        name=\"scrobbleThreshold\"\n                        render={({ field }) => (\n                          <FormItem className=\"space-y-1\">\n                            <div className=\"flex justify-between\">\n                              <FormLabel className=\"text-xs\">\n                                Scrobble Threshold: {field.value}%\n                              </FormLabel>\n                            </div>\n                            <FormControl>\n                              <Slider\n                                value={[field.value]}\n                                min={50}\n                                max={100}\n                                step={1}\n                                onValueChange={(vals) =>\n                                  field.onChange(vals[0])\n                                }\n                              />\n                            </FormControl>\n                            <FormDescription className=\"text-xs opacity-50\">\n                              Track will scrobble after playing this percentage\n                              of its length\n                            </FormDescription>\n                          </FormItem>\n                        )}\n                      />\n                    </div>\n\n                    <div className=\"flex justify-end\">\n                      <Button type=\"submit\" className=\"w-fit text-xs\">\n                        Save Last.fm Settings\n                      </Button>\n                    </div>\n                  </form>\n                </Form>\n              </div>\n            ) : (\n              // Show connection form if not connected\n              <div className=\"wora-border rounded-2xl p-6\">\n                <Form {...lastFmForm}>\n                  <form\n                    onSubmit={lastFmForm.handleSubmit(connectToLastFm)}\n                    className=\"flex flex-col gap-4\"\n                  >\n                    <div className=\"flex flex-col gap-1\">\n                      <p className=\"text-sm font-medium\">Connect to Last.fm</p>\n                      <p className=\"mb-2 text-xs opacity-50\">\n                        Connect your Last.fm account to scrobble tracks\n                      </p>\n                    </div>\n\n                    <div className=\"grid grid-cols-2 gap-4\">\n                      <FormField\n                        control={lastFmForm.control}\n                        name=\"lastFmUsername\"\n                        render={({ field }) => (\n                          <FormItem className=\"flex flex-col\">\n                            <FormLabel className=\"text-xs\">\n                              Last.fm Username\n                            </FormLabel>\n                            <FormControl>\n                              <Input placeholder=\"Username\" {...field} />\n                            </FormControl>\n                            <FormMessage className=\"text-xs\" />\n                          </FormItem>\n                        )}\n                      />\n\n                      <FormField\n                        control={lastFmForm.control}\n                        name=\"lastFmPassword\"\n                        render={({ field }) => (\n                          <FormItem className=\"flex flex-col\">\n                            <FormLabel className=\"text-xs\">\n                              Last.fm Password\n                            </FormLabel>\n                            <FormControl>\n                              <Input\n                                type=\"password\"\n                                placeholder=\"Password\"\n                                {...field}\n                              />\n                            </FormControl>\n                            <FormMessage className=\"text-xs\" />\n                          </FormItem>\n                        )}\n                      />\n                    </div>\n\n                    <div className=\"flex justify-end\">\n                      <Button\n                        type=\"submit\"\n                        className=\"w-fit text-xs\"\n                        disabled={lastFmLoading}\n                      >\n                        {lastFmLoading ? (\n                          <>\n                            Connecting <Spinner className=\"ml-2 h-3.5 w-3.5\" />\n                          </>\n                        ) : (\n                          <>Connect to Last.fm</>\n                        )}\n                      </Button>\n                    </div>\n                  </form>\n                </Form>\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "renderer/pages/setup.tsx",
    "content": "import Actions from \"@/components/ui/actions\";\nimport { Button } from \"@/components/ui/button\";\nimport Image from \"next/image\";\nimport { IconArrowRight } from \"@tabler/icons-react\";\nimport { useRouter } from \"next/router\";\nimport Spinner from \"@/components/ui/spinner\";\nimport { useState } from \"react\";\nimport { toast } from \"sonner\";\n\nexport default function Setup() {\n  const [loading, setLoading] = useState(false);\n  const router = useRouter();\n\n  const handleSelectMusicFolder = () => {\n    setLoading(true);\n\n    window.ipc\n      .invoke(\"scanLibrary\", true)\n      .then((response) => {\n        if (response?.canceled) {\n          setLoading(false);\n          return;\n        }\n\n        router.push(\"/home\");\n      })\n      .catch((error) => {\n        console.error(\"Error setting up music folder:\", error);\n        toast(\"Failed to set up music folder. Please try again.\");\n        setLoading(false);\n      });\n  };\n\n  return (\n    <div className=\"wora-transition h-screen w-screen\">\n      <Actions />\n      <div className=\"relative flex h-full w-full items-center overflow-hidden p-8 select-none\">\n        <div className=\"absolute -bottom-36 -left-32 h-96 w-96 rounded-full bg-black blur-[1700px] dark:bg-white\" />\n        <div className=\"z-10 flex flex-col gap-8\">\n          <div className=\"flex flex-col gap-3\">\n            <Image\n              src=\"/assets/Full [Dark].png\"\n              width={124}\n              height={0}\n              alt=\"logo\"\n              className=\"hidden dark:block\"\n            />\n            <Image\n              src=\"/assets/Full.png\"\n              width={124}\n              height={0}\n              alt=\"logo\"\n              className=\"block dark:hidden\"\n            />\n            <div className=\"flex items-center text-sm opacity-50\">\n              A beautiful player for audiophiles 🎧\n            </div>\n          </div>\n          <Button\n            className=\"w-fit\"\n            onClick={handleSelectMusicFolder}\n            disabled={loading}\n          >\n            Select Music Folder\n            {loading ? (\n              <Spinner className=\"h-3.5 w-3.5\" />\n            ) : (\n              <IconArrowRight stroke={2} className=\"h-3.5 w-3.5\" />\n            )}\n          </Button>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "renderer/pages/songs.tsx",
    "content": "import React, { useEffect, useState, useCallback, useRef } from \"react\";\nimport { useScrollAreaRestoration } from \"@/hooks/useScrollAreaRestoration\";\nimport { Button } from \"@/components/ui/button\";\nimport Songs from \"@/components/ui/songs\";\nimport { usePlayer } from \"@/context/playerContext\";\nimport Spinner from \"@/components/ui/spinner\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport {\n  IconArrowsShuffle2,\n  IconSortAscending,\n  IconSortDescending,\n  IconSearch,\n  IconX,\n} from \"@tabler/icons-react\";\nimport { Input } from \"@/components/ui/input\";\nimport songCache from \"@/lib/songCache\";\nimport { useRouter } from \"next/router\";\n\nexport default function AllSongs() {\n  const [songs, setSongs] = useState(songCache.getAllSongs());\n  const [filteredSongs, setFilteredSongs] = useState(\n    songCache.getFilteredSongs(),\n  );\n  const [loading, setLoading] = useState(false);\n  const [searchLoading, setSearchLoading] = useState(false);\n  const [page, setPage] = useState(songCache.getPage());\n  const [hasMore, setHasMore] = useState(songCache.hasMore());\n  \n  // Use scroll restoration hook for ScrollArea\n  useScrollAreaRestoration('songs');\n\n  // Get sort settings from cache\n  const cachedSortSettings = songCache.getSortSettings();\n  const [sortBy, setSortBy] = useState(cachedSortSettings.sortBy);\n  const [sortOrder, setSortOrder] = useState(cachedSortSettings.sortOrder);\n\n  // Get last search query from cache\n  const [searchTerm, setSearchTerm] = useState(songCache.getLastSearchQuery());\n\n  const { setQueueAndPlay } = usePlayer();\n  const searchTimeout = useRef<NodeJS.Timeout | null>(null);\n  const router = useRouter();\n\n  // Add ref to Songs component\n  const songsListRef = useRef<{ scrollToTop: () => void }>(null);\n  const contentRef = useRef<HTMLDivElement>(null);\n\n  \n  useEffect(() => {\n    // Listen for reset event from main process\n    const resetListener = window.ipc.on(\"resetSongsState\", () => {\n      // First, clear the search and cache state\n      setSearchTerm(\"\");\n      songCache.setSearchResults([], \"\");\n\n      // Reset sort options to defaults\n      setSortBy(\"name\");\n      setSortOrder(\"asc\");\n\n      // Clear existing song data to prevent duplicates\n      setSongs([]);\n      setFilteredSongs([]);\n\n      // Force reset pagination state\n      setPage(1);\n      setHasMore(true);\n\n      // Reset scroll position if songs list ref is available\n      if (songsListRef.current && songsListRef.current.scrollToTop) {\n        songsListRef.current.scrollToTop();\n      }\n\n      // Force immediate reload of fresh data\n      setTimeout(() => {\n        loadSongs(true);\n      }, 0);\n    });\n\n    return () => {\n      // Clean up event listener\n      resetListener();\n    };\n  }, []);\n\n  // Load songs on initial render\n  useEffect(() => {\n    if (!songCache.isInitialized() || songCache.isStale()) {\n      loadSongs();\n      songCache.setInitialized();\n    } else {\n      // Use the cached data\n      setSongs(songCache.getAllSongs());\n      setFilteredSongs(songCache.getFilteredSongs());\n\n      // If we have a search query, use the cached search results\n      if (searchTerm) {\n        setFilteredSongs(songCache.getSearchResults());\n      }\n    }\n  }, []);\n\n  // Load songs from the database with pagination\n  const loadSongs = useCallback(\n    async (isReset = false) => {\n      // When resetting, ignore hasMore/loading restrictions\n      if (!isReset && (loading || !hasMore) && !searchTerm) return;\n\n      console.log(\"Loading songs: page\", isReset ? 1 : page);\n      setLoading(true);\n\n      // If this is a reset load, reset the song state\n      const pageToLoad = isReset ? 1 : page;\n\n      try {\n        // Get paginated songs\n        const newSongs = await window.ipc.invoke(\"getSongs\", pageToLoad);\n\n        if (newSongs.length === 0) {\n          console.log(\"No more songs to load\");\n          setHasMore(false);\n          songCache.updatePagination(pageToLoad, false);\n        } else {\n          console.log(`Loaded ${newSongs.length} songs`);\n          // Process songs to ensure complete data\n          const processedSongs = newSongs.map((song) => ({\n            ...song,\n            album: {\n              id: song.album?.id || null,\n              name: song.album?.name || \"Unknown Album\",\n              artist: song.album?.artist || \"Unknown Artist\",\n              cover: song.album?.cover || null,\n              year: song.album?.year || null,\n            },\n          }));\n\n          // For reset operations, replace the entire song list\n          if (isReset) {\n            setSongs(processedSongs);\n\n            // Sort with default settings\n            const sortedSongs = songCache.sortSongs(\n              processedSongs,\n              sortBy,\n              sortOrder,\n            );\n            setFilteredSongs(sortedSongs);\n            songCache.setFilteredSongs(sortedSongs);\n            songCache.setAllSongs(processedSongs);\n\n            // Update pagination\n            setPage(2); // Set to 2 because we just loaded page 1\n            songCache.updatePagination(2, true);\n          } else {\n            // Regular pagination behavior follows\n            const updatedSongs = [...songs, ...processedSongs];\n            setSongs(updatedSongs);\n            setPage(page + 1);\n\n            // Only update filtered songs if not searching\n            if (!searchTerm) {\n              const sortedSongs = songCache.sortSongs(\n                updatedSongs,\n                sortBy,\n                sortOrder,\n              );\n              setFilteredSongs(sortedSongs);\n              songCache.setFilteredSongs(sortedSongs);\n            }\n\n            // Update global cache\n            songCache.addSongs(processedSongs);\n            songCache.updatePagination(page + 1, true);\n          }\n        }\n      } catch (error) {\n        console.error(\"Error loading songs:\", error);\n      } finally {\n        setLoading(false);\n      }\n    },\n    [page, loading, hasMore, songs, searchTerm, sortBy, sortOrder],\n  );\n\n  // Handle search with debounce\n  const handleSearch = useCallback(\n    async (term) => {\n      // Clear any pending search\n      if (searchTimeout.current) {\n        clearTimeout(searchTimeout.current);\n      }\n\n      if (!term) {\n        // If search is cleared, show all sorted songs\n        const sortedSongs = songCache.sortSongs(songs, sortBy, sortOrder);\n        setFilteredSongs(sortedSongs);\n        songCache.setFilteredSongs(sortedSongs);\n        songCache.setSearchResults([], \"\");\n        return;\n      }\n\n      setSearchLoading(true);\n\n      // Debounce search to avoid too many requests\n      searchTimeout.current = setTimeout(async () => {\n        try {\n          // Always use the dedicated searchSongs endpoint to search the entire database\n          // This ensures we search all songs regardless of how many have been loaded\n          const results = await window.ipc.invoke(\"searchSongs\", term);\n\n          if (results && results.length >= 0) {\n            // Process results to ensure complete data\n            const processedResults = results.map((song) => ({\n              ...song,\n              album: {\n                id: song.album?.id || null,\n                name: song.album?.name || \"Unknown Album\",\n                artist: song.album?.artist || \"Unknown Artist\",\n                cover: song.album?.cover || null,\n                year: song.album?.year || null,\n              },\n            }));\n\n            const sortedResults = songCache.sortSongs(\n              processedResults,\n              sortBy,\n              sortOrder,\n            );\n            setFilteredSongs(sortedResults);\n\n            // Update cache with search results\n            songCache.setSearchResults(sortedResults, term);\n\n            // Also update the entire songs collection if appropriate\n            if (processedResults.length > songs.length) {\n              songCache.setAllSongs(processedResults);\n              setSongs(processedResults);\n            }\n          } else {\n            // If no results, show empty list\n            setFilteredSongs([]);\n            songCache.setSearchResults([], term);\n          }\n        } catch (error) {\n          console.error(\"Error searching songs:\", error);\n          // Fall back to empty results\n          setFilteredSongs([]);\n          songCache.setSearchResults([], term);\n        } finally {\n          setSearchLoading(false);\n        }\n      }, 300);\n    },\n    [songs, sortBy, sortOrder],\n  );\n\n  // Handle shuffle all songs\n  const handleShuffleAllSongs = async () => {\n    // If we're searching or filtering, shuffle only the filtered songs\n    if (searchTerm || filteredSongs.length !== songs.length) {\n      if (filteredSongs.length > 0) {\n        setQueueAndPlay(filteredSongs, 0, true);\n      }\n      return;\n    }\n\n    // Handle shuffling all songs\n    setSearchLoading(true);\n    try {\n      // Use cached all songs if available and not stale\n      let allSongs = songCache.getAllSongs();\n\n      // If the cache doesn't have all songs or is stale, fetch them\n      if (allSongs.length === 0 || songCache.isStale()) {\n        allSongs = await window.ipc.invoke(\"getAllSongs\");\n\n        if (allSongs && allSongs.length > 0) {\n          // Process and cache all songs\n          const processedSongs = allSongs.map((song) => ({\n            ...song,\n            album: {\n              id: song.album?.id || null,\n              name: song.album?.name || \"Unknown Album\",\n              artist: song.album?.artist || \"Unknown Artist\",\n              cover: song.album?.cover || null,\n            },\n          }));\n\n          songCache.setAllSongs(processedSongs);\n          allSongs = processedSongs;\n        }\n      }\n\n      if (allSongs.length > 0) {\n        // Shuffle and play all songs\n        setQueueAndPlay(allSongs, 0, true);\n      }\n    } catch (error) {\n      console.error(\"Error fetching all songs:\", error);\n      if (filteredSongs.length > 0) {\n        setQueueAndPlay(filteredSongs, 0, true);\n      }\n    } finally {\n      setSearchLoading(false);\n    }\n  };\n\n  // Handle play all songs in current order\n  const handlePlayAllSongs = async () => {\n    // If we're searching or filtering, play only the filtered songs\n    if (searchTerm || filteredSongs.length !== songs.length) {\n      if (filteredSongs.length > 0) {\n        setQueueAndPlay(filteredSongs, 0, false);\n      }\n      return;\n    }\n\n    // Handle playing all songs\n    setSearchLoading(true);\n    try {\n      // Use cached all songs if available and not stale\n      let allSongs = songCache.getAllSongs();\n\n      // If the cache doesn't have all songs or is stale, fetch them\n      if (allSongs.length === 0 || songCache.isStale()) {\n        allSongs = await window.ipc.invoke(\"getAllSongs\");\n\n        if (allSongs && allSongs.length > 0) {\n          // Process and cache all songs\n          const processedSongs = allSongs.map((song) => ({\n            ...song,\n            album: {\n              id: song.album?.id || null,\n              name: song.album?.name || \"Unknown Album\",\n              artist: song.album?.artist || \"Unknown Artist\",\n              cover: song.album?.cover || null,\n            },\n          }));\n\n          songCache.setAllSongs(processedSongs);\n          allSongs = processedSongs;\n        }\n      }\n\n      if (allSongs.length > 0) {\n        // Sort and play all songs\n        const sortedSongs = songCache.sortSongs(allSongs, sortBy, sortOrder);\n        setQueueAndPlay(sortedSongs, 0, false);\n      }\n    } catch (error) {\n      console.error(\"Error fetching all songs:\", error);\n      if (filteredSongs.length > 0) {\n        setQueueAndPlay(filteredSongs, 0, false);\n      }\n    } finally {\n      setSearchLoading(false);\n    }\n  };\n\n  // Update search when term changes\n  useEffect(() => {\n    handleSearch(searchTerm);\n  }, [searchTerm, handleSearch]);\n\n  // Update sorting when sort parameters change\n  useEffect(() => {\n    // Update the global cache with new sort settings\n    songCache.updateSortSettings(sortBy, sortOrder);\n\n    if (searchTerm) {\n      // If we're searching, sort the existing search results\n      const searchResults = songCache.getSearchResults();\n      if (searchResults.length > 0) {\n        const sortedResults = songCache.sortSongs(\n          searchResults,\n          sortBy,\n          sortOrder,\n        );\n        setFilteredSongs(sortedResults);\n        songCache.setSearchResults(sortedResults, searchTerm);\n      }\n    } else {\n      // If not searching, fetch and sort all songs\n      const fetchAndSortAllSongs = async () => {\n        setSearchLoading(true);\n        try {\n          // Always query all songs from the database to ensure comprehensive sorting\n          const allSongs = await window.ipc.invoke(\"getAllSongs\");\n\n          if (allSongs && allSongs.length > 0) {\n            // Process and cache all songs\n            const processedSongs = allSongs.map((song) => ({\n              ...song,\n              album: {\n                id: song.album?.id || null,\n                name: song.album?.name || \"Unknown Album\",\n                artist: song.album?.artist || \"Unknown Artist\",\n                cover: song.album?.cover || null,\n              },\n            }));\n\n            // Update the complete song collection in cache\n            songCache.setAllSongs(processedSongs);\n\n            // Sort and display all songs with the new sort parameters\n            const sortedSongs = songCache.sortSongs(\n              processedSongs,\n              sortBy,\n              sortOrder,\n            );\n            setFilteredSongs(sortedSongs);\n            songCache.setFilteredSongs(sortedSongs);\n\n            // Update local state\n            setSongs(processedSongs);\n          } else {\n            // Fall back to sorting just the loaded songs if the query failed\n            const sortedSongs = songCache.sortSongs(songs, sortBy, sortOrder);\n            setFilteredSongs(sortedSongs);\n            songCache.setFilteredSongs(sortedSongs);\n          }\n        } catch (error) {\n          console.error(\"Error fetching all songs for sorting:\", error);\n          // Fall back to sorting just the loaded songs\n          const sortedSongs = songCache.sortSongs(songs, sortBy, sortOrder);\n          setFilteredSongs(sortedSongs);\n          songCache.setFilteredSongs(sortedSongs);\n        } finally {\n          setSearchLoading(false);\n        }\n      };\n\n      fetchAndSortAllSongs();\n    }\n  }, [sortBy, sortOrder]);\n\n  // Toggle sort order\n  const toggleSortOrder = () => {\n    setSortOrder((prevOrder) => (prevOrder === \"asc\" ? \"desc\" : \"asc\"));\n  };\n\n  // Clear search term\n  const clearSearch = () => {\n    setSearchTerm(\"\");\n  };\n\n  // Handle loading more songs when user scrolls near the bottom\n  const handleLoadMore = useCallback(() => {\n    if (!searchTerm && !loading && hasMore) {\n      console.log(\"Loading more songs from scroll trigger\");\n      loadSongs();\n    }\n  }, [loadSongs, hasMore, loading, searchTerm]);\n\n  // Show loading indicators\n  const isLoadingInitial = loading && songs.length === 0;\n  const isSearching = searchLoading && searchTerm;\n\n  return (\n    <div className=\"flex flex-col gap-8\" ref={contentRef}>\n      <div className=\"flex flex-col gap-8\">\n        <div className=\"flex w-full items-center justify-between\">\n          <div className=\"flex flex-col\">\n            <div className=\"mt-4 text-lg font-medium leading-6\">Songs</div>\n            <div className=\"opacity-50\">\n              All your songs in one place, ready to be sorted and filtered.\n            </div>\n          </div>\n          <div className=\"flex items-center gap-4\">\n            <Button\n              onClick={handlePlayAllSongs}\n              className=\"flex items-center gap-2\"\n              disabled={isLoadingInitial || filteredSongs.length === 0}\n            >\n              Play All\n            </Button>\n            <Button\n              onClick={handleShuffleAllSongs}\n              className=\"flex items-center gap-2\"\n              disabled={isLoadingInitial || filteredSongs.length === 0}\n            >\n              <IconArrowsShuffle2 stroke={2} size={16} />\n              Shuffle\n            </Button>\n          </div>\n        </div>\n\n        {/* Search and filter section */}\n        <div className=\"flex w-full items-center gap-4\">\n          <div className=\"relative w-full max-w-md\">\n            <Input\n              placeholder=\"Search by song, artist or album...\"\n              value={searchTerm}\n              onChange={(e) => setSearchTerm(e.target.value)}\n              className=\"pl-8 pr-8\"\n            />\n            {searchTerm && (\n              <button\n                onClick={clearSearch}\n                className=\"absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200\"\n              >\n                <IconX size={16} stroke={2} />\n              </button>\n            )}\n            <div className=\"pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 text-gray-400\">\n              <IconSearch size={16} stroke={2} />\n            </div>\n          </div>\n          <div className=\"flex items-center gap-2\">\n            <Select value={sortBy} onValueChange={setSortBy}>\n              <SelectTrigger className=\"w-[180px]\">\n                <SelectValue placeholder=\"Sort by\" />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectItem value=\"name\">Song Title</SelectItem>\n                <SelectItem value=\"artist\">Artist</SelectItem>\n                <SelectItem value=\"album\">Album</SelectItem>\n                <SelectItem value=\"duration\">Duration</SelectItem>\n              </SelectContent>\n            </Select>\n            <Button variant=\"ghost\" onClick={toggleSortOrder} className=\"px-2\">\n              {sortOrder === \"asc\" ? (\n                <IconSortAscending stroke={2} size={20} />\n              ) : (\n                <IconSortDescending stroke={2} size={20} />\n              )}\n            </Button>\n          </div>\n        </div>\n\n        {isLoadingInitial || isSearching ? (\n          <div className=\"flex w-full items-center justify-center py-12\">\n            <Spinner className=\"h-6 w-6\" />\n          </div>\n        ) : (\n          <>\n            {filteredSongs.length > 0 ? (\n              <Songs\n                library={filteredSongs}\n                ref={songsListRef}\n                onLoadMore={handleLoadMore}\n                hasMore={hasMore && !searchTerm}\n                loadingMore={loading}\n              />\n            ) : (\n              <div className=\"flex w-full items-center justify-center p-10 text-gray-500\">\n                {searchTerm\n                  ? \"No songs matching your search\"\n                  : \"No songs found in your library\"}\n              </div>\n            )}\n          </>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "renderer/postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n"
  },
  {
    "path": "renderer/preload.d.ts",
    "content": "import { IpcHandler } from \"../main/preload\";\n\ndeclare global {\n  interface Window {\n    ipc: IpcHandler;\n  }\n}\n"
  },
  {
    "path": "renderer/styles/globals.css",
    "content": "@import url(\"https://fonts.googleapis.com/css2?family=Maven+Pro:wght@400..900&display=swap\")\nlayer(base);\n\n@import \"tailwindcss\";\n@plugin 'tailwindcss-animate';\n\n@custom-variant dark (&:is(.dark *));\n\n@theme {\n  --font-sans: Maven Pro, ui-sans-serif, system-ui, sans-serif;\n  --animate-accordion-down: accordion-down 0.2s ease-out;\n  --animate-accordion-up: accordion-up 0.2s ease-out;\n\n  @keyframes accordion-down {\n    from {\n      height: 0;\n    }\n    to {\n      height: var(--radix-accordion-content-height);\n    }\n  }\n  @keyframes accordion-up {\n    from {\n      height: var(--radix-accordion-content-height);\n    }\n    to {\n      height: 0;\n    }\n  }\n}\n\n/*\n  The default border color has changed to `currentcolor` in Tailwind CSS v4,\n  so we've added these compatibility styles to make sure everything still\n  looks the same as it did with Tailwind CSS v3.\n\n  If we ever want to remove these styles, we need to add an explicit border\n  color utility to any element that depends on these defaults.\n*/\n@layer base {\n  *,\n  ::after,\n  ::before,\n  ::backdrop,\n  ::file-selector-button {\n    border-color: var(--color-gray-200, currentcolor);\n  }\n}\n\n.wora-border {\n  @apply border border-black/5 dark:border-white/10;\n}\n\n.wora-transition {\n  @apply transition-all duration-300;\n}\n\n.h-utility {\n  height: calc(100vh - 14.25rem);\n}\n\n/* Hide scrollbar for Chrome, Safari and Opera */\n.no-scrollbar::-webkit-scrollbar {\n  display: none;\n}\n\n/* Hide scrollbar for IE, Edge and Firefox */\n.no-scrollbar {\n  -ms-overflow-style: none; /* IE and Edge */\n  scrollbar-width: none; /* Firefox */\n}\n\n.draggable-region {\n  -webkit-app-region: drag;\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: calc(100% - 90px); /* 90px is the aprox width of the buttons */\n  height: 30px;\n  background: #00000000;\n  z-index: 1000;\n}\n\n.non-draggable {\n  -webkit-app-region: no-drag;\n}\n\n.drag {\n  -webkit-app-region: drag;\n}\n\n.no-drag {\n  -webkit-app-region: no-drag;\n}\n\nhtml {\n  overscroll-behavior: none;\n}\n\n@keyframes music-bar {\n  0%, 100% { \n    transform: scaleY(0.3);\n  }\n  50% { \n    transform: scaleY(1);\n  }\n}\n"
  },
  {
    "path": "renderer/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.json\",\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"],\n  \"compilerOptions\": {\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": false,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"incremental\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"baseUrl\": \"./renderer\",\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"exclude\": [\"node_modules\", \"renderer/next.config.js\", \"app\", \"dist\"]\n}\n"
  },
  {
    "path": "vercel/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/basic-features/typescript for more information.\n"
  },
  {
    "path": "vercel/next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: true,\n  swcMinify: true,\n  images: {\n    domains: [\"lastfm.freetls.fastly.net\"],\n  },\n};\n\nmodule.exports = nextConfig;\n"
  },
  {
    "path": "vercel/package.json",
    "content": "{\n  \"name\": \"wora-api\",\n  \"version\": \"0.4.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\"\n  },\n  \"dependencies\": {\n    \"next\": \"^14.2.21\",\n    \"react\": \"^18.3.1\",\n    \"react-dom\": \"^18.3.1\",\n    \"node-fetch\": \"2\",\n    \"crypto-js\": \"^4.2.0\",\n    \"typescript\": \"^5.7.2\",\n    \"@types/react\": \"^18.3.18\",\n    \"@types/node\": \"^22.10.2\"\n  }\n}\n"
  },
  {
    "path": "vercel/pages/_app.tsx",
    "content": "import type { AppProps } from \"next/app\";\n\nexport default function App({ Component, pageProps }: AppProps) {\n  return <Component {...pageProps} />;\n}\n"
  },
  {
    "path": "vercel/pages/api/config.ts",
    "content": "// Configuration for Last.fm API\n// These credentials are stored on the server side and not exposed to clients\nexport const LASTFM_CONFIG = {\n  API_KEY: process.env.LASTFM_API_KEY || \"\",\n  API_SECRET: process.env.LASTFM_API_SECRET || \"\",\n  API_URL: \"https://ws.audioscrobbler.com/2.0/\",\n};\n"
  },
  {
    "path": "vercel/pages/api/index.ts",
    "content": "import { NextApiRequest, NextApiResponse } from \"next\";\n\nexport default function handler(req: NextApiRequest, res: NextApiResponse) {\n  res.status(200).json({\n    status: \"OK\",\n    message: \"API is running properly!\",\n    endpoints: [\n      \"/api/lastfm/auth\",\n      \"/api/lastfm/now-playing\",\n      \"/api/lastfm/scrobble\",\n      \"/api/lastfm/track-info\",\n      \"/api/lastfm/user-info\",\n    ],\n  });\n}\n"
  },
  {
    "path": "vercel/pages/api/lastfm/auth.ts",
    "content": "import { NextApiRequest, NextApiResponse } from \"next\";\nimport { LASTFM_CONFIG } from \"../config\";\nimport { createFormData, generateSignature } from \"../utils/lastfm\";\nimport * as crypto from \"crypto\";\n\nexport default async function handler(\n  req: NextApiRequest,\n  res: NextApiResponse,\n) {\n  // This endpoint requires a POST request\n  if (req.method !== \"POST\") {\n    return res\n      .status(405)\n      .json({ success: false, error: \"Method not allowed\" });\n  }\n\n  try {\n    const { username, password } = req.body;\n\n    if (!username || !password) {\n      return res.status(400).json({\n        success: false,\n        error: \"Username and password are required\",\n      });\n    }\n\n    // For Last.fm's auth.getMobileSession:\n    // According to the API docs: http://www.last.fm/api/mobileauth\n\n    // Set up the parameters for the API request\n    const params: Record<string, string> = {\n      method: \"auth.getMobileSession\",\n      username: username,\n      password: password, // Last.fm expects the plaintext password\n      api_key: LASTFM_CONFIG.API_KEY,\n    };\n\n    // Generate signature\n    params.api_sig = generateSignature(params);\n    params.format = \"json\";\n\n    // Create form data\n    const formData = createFormData(params);\n\n    // Make the request\n    console.log(\"Making Last.fm auth request with params:\", {\n      ...params,\n      password: \"[HIDDEN]\", // Don't log the password\n      api_sig: \"[SIGNATURE]\", // Don't log the full signature\n    });\n\n    const response = await fetch(LASTFM_CONFIG.API_URL, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/x-www-form-urlencoded\",\n      },\n      body: formData.toString(),\n    });\n\n    const data = await response.json();\n\n    if (data.error) {\n      console.error(\"Last.fm auth error:\", data);\n      return res.status(400).json({\n        success: false,\n        error: `Last.fm error ${data.error}: ${data.message}`,\n      });\n    }\n\n    // Return the session key\n    return res.status(200).json({\n      success: true,\n      session: data.session,\n    });\n  } catch (error) {\n    console.error(\"Last.fm authentication error:\", error);\n    return res.status(500).json({\n      success: false,\n      error: \"An error occurred during authentication\",\n    });\n  }\n}\n"
  },
  {
    "path": "vercel/pages/api/lastfm/now-playing.ts",
    "content": "import { NextApiRequest, NextApiResponse } from \"next\";\nimport { LASTFM_CONFIG } from \"../config\";\nimport { createFormData, generateSignature } from \"../utils/lastfm\";\n\nexport default async function handler(\n  req: NextApiRequest,\n  res: NextApiResponse,\n) {\n  // This endpoint requires a POST request\n  if (req.method !== \"POST\") {\n    return res\n      .status(405)\n      .json({ success: false, error: \"Method not allowed\" });\n  }\n\n  try {\n    const { sessionKey, artist, track, album, duration } = req.body;\n\n    if (!sessionKey || !artist || !track) {\n      return res.status(400).json({\n        success: false,\n        error: \"Session key, artist, and track are required\",\n      });\n    }\n\n    // Build parameters for the API request\n    const params: Record<string, string> = {\n      method: \"track.updateNowPlaying\",\n      artist,\n      track,\n      api_key: LASTFM_CONFIG.API_KEY,\n      sk: sessionKey,\n    };\n\n    // Add optional parameters if available\n    if (album) params.album = album;\n    if (duration) params.duration = duration.toString();\n\n    // Add signature for authenticated requests\n    params[\"api_sig\"] = generateSignature(params);\n    params[\"format\"] = \"json\";\n\n    // Create form data\n    const formData = createFormData(params);\n\n    // Make the request\n    const response = await fetch(LASTFM_CONFIG.API_URL, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/x-www-form-urlencoded\",\n      },\n      body: formData.toString(),\n    });\n\n    const data = await response.json();\n\n    if (data.error) {\n      return res.status(400).json({\n        success: false,\n        error: `Last.fm error ${data.error}: ${data.message}`,\n      });\n    }\n\n    // Return success response\n    return res.status(200).json({\n      success: true,\n    });\n  } catch (error) {\n    console.error(\"Last.fm now playing error:\", error);\n    return res.status(500).json({\n      success: false,\n      error: \"An error occurred updating now playing status\",\n    });\n  }\n}\n"
  },
  {
    "path": "vercel/pages/api/lastfm/scrobble.ts",
    "content": "import { NextApiRequest, NextApiResponse } from \"next\";\nimport { LASTFM_CONFIG } from \"../config\";\nimport { createFormData, generateSignature } from \"../utils/lastfm\";\n\nexport default async function handler(\n  req: NextApiRequest,\n  res: NextApiResponse,\n) {\n  // This endpoint requires a POST request\n  if (req.method !== \"POST\") {\n    return res\n      .status(405)\n      .json({ success: false, error: \"Method not allowed\" });\n  }\n\n  try {\n    const { sessionKey, artist, track, album, timestamp, duration } = req.body;\n\n    if (!sessionKey || !artist || !track) {\n      return res.status(400).json({\n        success: false,\n        error: \"Session key, artist, and track are required\",\n      });\n    }\n\n    // Build parameters for the API request\n    const params: Record<string, string> = {\n      method: \"track.scrobble\",\n      artist,\n      track,\n      timestamp: timestamp || Math.floor(Date.now() / 1000).toString(),\n      api_key: LASTFM_CONFIG.API_KEY,\n      sk: sessionKey,\n    };\n\n    // Add optional parameters if available\n    if (album) params.album = album;\n    if (duration) params.duration = duration.toString();\n\n    // Add signature for authenticated requests\n    params[\"api_sig\"] = generateSignature(params);\n    params[\"format\"] = \"json\";\n\n    // Create form data\n    const formData = createFormData(params);\n\n    // Make the request\n    const response = await fetch(LASTFM_CONFIG.API_URL, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/x-www-form-urlencoded\",\n      },\n      body: formData.toString(),\n    });\n\n    const data = await response.json();\n\n    if (data.error) {\n      return res.status(400).json({\n        success: false,\n        error: `Last.fm error ${data.error}: ${data.message}`,\n      });\n    }\n\n    // Return success response\n    return res.status(200).json({\n      success: true,\n    });\n  } catch (error) {\n    console.error(\"Last.fm scrobble error:\", error);\n    return res.status(500).json({\n      success: false,\n      error: \"An error occurred scrobbling the track\",\n    });\n  }\n}\n"
  },
  {
    "path": "vercel/pages/api/lastfm/track-info.ts",
    "content": "import { NextApiRequest, NextApiResponse } from \"next\";\nimport { LASTFM_CONFIG } from \"../config\";\n\nexport default async function handler(\n  req: NextApiRequest,\n  res: NextApiResponse,\n) {\n  // This endpoint requires a GET request\n  if (req.method !== \"GET\") {\n    return res\n      .status(405)\n      .json({ success: false, error: \"Method not allowed\" });\n  }\n\n  try {\n    const { artist, track, username } = req.query;\n\n    if (!artist || !track) {\n      return res.status(400).json({\n        success: false,\n        error: \"Artist and track parameters are required\",\n      });\n    }\n\n    // Build URL parameters\n    const params = new URLSearchParams({\n      method: \"track.getInfo\",\n      artist: Array.isArray(artist) ? artist[0] : artist,\n      track: Array.isArray(track) ? track[0] : track,\n      api_key: LASTFM_CONFIG.API_KEY,\n      format: \"json\",\n    });\n\n    // Add username if available (for loved status)\n    if (username) {\n      params.append(\n        \"username\",\n        Array.isArray(username) ? username[0] : username,\n      );\n    }\n\n    // Make the request\n    const url = `${LASTFM_CONFIG.API_URL}?${params.toString()}`;\n    const response = await fetch(url);\n    const data = await response.json();\n\n    if (data.error) {\n      return res.status(400).json({\n        success: false,\n        error: `Last.fm error ${data.error}: ${data.message}`,\n      });\n    }\n\n    // Return track info\n    return res.status(200).json({\n      success: true,\n      track: data.track,\n    });\n  } catch (error) {\n    console.error(\"Last.fm track info error:\", error);\n    return res.status(500).json({\n      success: false,\n      error: \"An error occurred retrieving track information\",\n    });\n  }\n}\n"
  },
  {
    "path": "vercel/pages/api/lastfm/user-info.ts",
    "content": "import { NextApiRequest, NextApiResponse } from \"next\";\nimport { LASTFM_CONFIG } from \"../config\";\nimport { createFormData, generateSignature } from \"../utils/lastfm\";\n\nexport default async function handler(\n  req: NextApiRequest,\n  res: NextApiResponse,\n) {\n  // This endpoint requires a GET request\n  if (req.method !== \"GET\") {\n    return res\n      .status(405)\n      .json({ success: false, error: \"Method not allowed\" });\n  }\n\n  try {\n    const { username, sessionKey } = req.query;\n\n    if (!username) {\n      return res.status(400).json({\n        success: false,\n        error: \"Username parameter is required\",\n      });\n    }\n\n    // Build parameters for the API request\n    const params: Record<string, string> = {\n      method: \"user.getInfo\",\n      user: Array.isArray(username) ? username[0] : username,\n      api_key: LASTFM_CONFIG.API_KEY,\n    };\n\n    // If there's a session key, this is authenticated request (for private user data)\n    let isAuthenticatedRequest = false;\n    if (sessionKey && sessionKey !== \"undefined\" && sessionKey !== \"null\") {\n      isAuthenticatedRequest = true;\n      params.sk = Array.isArray(sessionKey) ? sessionKey[0] : sessionKey;\n    }\n\n    // Add signature for authenticated requests\n    if (isAuthenticatedRequest) {\n      params[\"api_sig\"] = generateSignature(params);\n    }\n    params[\"format\"] = \"json\";\n\n    // Create request parameters\n    let url: string;\n    let options: RequestInit = {};\n\n    if (isAuthenticatedRequest) {\n      // For authenticated requests, use POST\n      const formData = createFormData(params);\n      url = LASTFM_CONFIG.API_URL;\n      options = {\n        method: \"POST\",\n        headers: {\n          \"Content-Type\": \"application/x-www-form-urlencoded\",\n        },\n        body: formData.toString(),\n      };\n    } else {\n      // For unauthenticated requests, use GET\n      const queryParams = new URLSearchParams(params);\n      url = `${LASTFM_CONFIG.API_URL}?${queryParams.toString()}`;\n    }\n\n    // Make the request\n    const response = await fetch(url, options);\n    const data = await response.json();\n\n    if (data.error) {\n      return res.status(400).json({\n        success: false,\n        error: `Last.fm error ${data.error}: ${data.message}`,\n      });\n    }\n\n    // Return user info\n    return res.status(200).json({\n      success: true,\n      user: data.user,\n    });\n  } catch (error) {\n    console.error(\"Last.fm user info error:\", error);\n    return res.status(500).json({\n      success: false,\n      error: \"An error occurred retrieving user information\",\n    });\n  }\n}\n"
  },
  {
    "path": "vercel/pages/api/utils/lastfm.ts",
    "content": "import * as crypto from \"crypto\";\nimport { LASTFM_CONFIG } from \"../config\";\n\n/**\n * Generate MD5 hash for password authentication\n */\nexport const getMD5Auth = (username: string, password: string): string => {\n  // Last.fm expects: md5(username + md5(password))\n  const passwordHash = crypto.createHash(\"md5\").update(password).digest(\"hex\");\n  return crypto\n    .createHash(\"md5\")\n    .update(username.toLowerCase() + passwordHash)\n    .digest(\"hex\");\n};\n\n/**\n * Generate API signature for authenticated requests\n */\nexport const generateSignature = (params: Record<string, string>): string => {\n  // Remove format parameter\n  const signatureParams = { ...params };\n  delete signatureParams.format;\n\n  // Create signature string: alphabetically sorted param names + values + secret\n  const signatureStr =\n    Object.keys(signatureParams)\n      .sort()\n      .map((key) => key + signatureParams[key])\n      .join(\"\") + LASTFM_CONFIG.API_SECRET;\n\n  // Return MD5 hash\n  return crypto.createHash(\"md5\").update(signatureStr).digest(\"hex\");\n};\n\n/**\n * Create form data for POST requests\n */\nexport const createFormData = (\n  params: Record<string, string>,\n): URLSearchParams => {\n  const formData = new URLSearchParams();\n  Object.entries(params).forEach(([key, value]) => {\n    formData.append(key, value);\n  });\n  return formData;\n};\n"
  },
  {
    "path": "vercel/pages/index.tsx",
    "content": "import React from \"react\";\n\nexport default function Home() {\n  return (\n    <div\n      style={{\n        padding: \"20px\",\n        maxWidth: \"800px\",\n        margin: \"0 auto\",\n        fontFamily: \"system-ui, sans-serif\",\n      }}\n    >\n      <h1>WORA Last.fm API</h1>\n      <p>\n        This is a serverless API for Last.fm integration. The following\n        endpoints are available:\n      </p>\n\n      <ul style={{ lineHeight: 1.6 }}>\n        <li>\n          <strong>POST /api/lastfm/auth</strong> - Authenticate with Last.fm\n        </li>\n        <li>\n          <strong>POST /api/lastfm/now-playing</strong> - Update now playing\n          status\n        </li>\n        <li>\n          <strong>POST /api/lastfm/scrobble</strong> - Scrobble a track\n        </li>\n        <li>\n          <strong>GET /api/lastfm/track-info</strong> - Get track information\n        </li>\n        <li>\n          <strong>GET /api/lastfm/user-info</strong> - Get user information\n        </li>\n      </ul>\n\n      <p>\n        <a href=\"/api\" style={{ color: \"#0070f3\", textDecoration: \"none\" }}>\n          Test API Status\n        </a>\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "vercel/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2015\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "vercel/vercel.json",
    "content": "{\n  \"framework\": \"nextjs\",\n  \"outputDirectory\": \".next\",\n  \"buildCommand\": \"npm run build\",\n  \"devCommand\": \"npm run dev\",\n  \"installCommand\": \"npm install\",\n  \"regions\": [\"iad1\"],\n  \"rewrites\": [{ \"source\": \"/api/(.*)\", \"destination\": \"/api/$1\" }],\n  \"env\": {\n    \"VERCEL\": \"true\"\n  }\n}\n"
  }
]