Full Code of playwora/wora for AI

main 4b8621ac3b44 cached
97 files
452.8 KB
109.0k tokens
176 symbols
1 requests
Download .txt
Showing preview only (481K chars total). Download the full file or copy to clipboard to get everything.
Repository: playwora/wora
Branch: main
Commit: 4b8621ac3b44
Files: 97
Total size: 452.8 KB

Directory structure:
gitextract_a5817xvy/

├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── report_issue.yml
│   │   └── request_feature.yml
│   └── workflows/
│       └── release.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── bun.lockb
├── components.json
├── electron-builder.yml
├── main/
│   ├── background.ts
│   ├── helpers/
│   │   ├── create-window.ts
│   │   ├── db/
│   │   │   ├── connectDB.ts
│   │   │   ├── createDB.ts
│   │   │   └── schema.ts
│   │   ├── index.ts
│   │   └── lastfm-service.ts
│   └── preload.ts
├── package.json
├── renderer/
│   ├── components/
│   │   ├── ErrorBoundary.tsx
│   │   ├── LoadingSkeletons.tsx
│   │   ├── PageTransition.tsx
│   │   ├── PageTransitionMinimal.tsx
│   │   ├── main/
│   │   │   ├── lyrics.tsx
│   │   │   ├── navbar.tsx
│   │   │   └── player.tsx
│   │   ├── themeProvider.tsx
│   │   └── ui/
│   │       ├── actions.tsx
│   │       ├── album.tsx
│   │       ├── avatar.tsx
│   │       ├── badge.tsx
│   │       ├── button.tsx
│   │       ├── carousel.tsx
│   │       ├── command.tsx
│   │       ├── context-menu.tsx
│   │       ├── dialog.tsx
│   │       ├── form.tsx
│   │       ├── input.tsx
│   │       ├── label.tsx
│   │       ├── scroll-area.tsx
│   │       ├── select.tsx
│   │       ├── skeleton.tsx
│   │       ├── slider.tsx
│   │       ├── songs.tsx
│   │       ├── sonner.tsx
│   │       ├── spinner.tsx
│   │       ├── switch.tsx
│   │       ├── tabs.tsx
│   │       └── tooltip.tsx
│   ├── context/
│   │   └── playerContext.tsx
│   ├── hooks/
│   │   ├── useDebounce.ts
│   │   └── useScrollAreaRestoration.ts
│   ├── lib/
│   │   ├── albumCache.ts
│   │   ├── apiConfig.ts
│   │   ├── helpers.ts
│   │   ├── lastfm-client.ts
│   │   ├── lastfm.ts
│   │   ├── songCache.ts
│   │   └── utils.ts
│   ├── next-env.d.ts
│   ├── next.config.js
│   ├── pages/
│   │   ├── _app.tsx
│   │   ├── albums/
│   │   │   └── [slug].tsx
│   │   ├── albums.tsx
│   │   ├── artists/
│   │   │   ├── [name].tsx
│   │   │   └── index.tsx
│   │   ├── home.tsx
│   │   ├── playlists/
│   │   │   └── [slug].tsx
│   │   ├── playlists.tsx
│   │   ├── settings.tsx
│   │   ├── setup.tsx
│   │   └── songs.tsx
│   ├── postcss.config.js
│   ├── preload.d.ts
│   ├── styles/
│   │   └── globals.css
│   └── tsconfig.json
├── resources/
│   └── icon.icns
├── tsconfig.json
└── vercel/
    ├── next-env.d.ts
    ├── next.config.js
    ├── package.json
    ├── pages/
    │   ├── _app.tsx
    │   ├── api/
    │   │   ├── config.ts
    │   │   ├── index.ts
    │   │   ├── lastfm/
    │   │   │   ├── auth.ts
    │   │   │   ├── now-playing.ts
    │   │   │   ├── scrobble.ts
    │   │   │   ├── track-info.ts
    │   │   │   └── user-info.ts
    │   │   └── utils/
    │   │       └── lastfm.ts
    │   └── index.tsx
    ├── tsconfig.json
    └── vercel.json

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

================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: hiaaryan
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']


================================================
FILE: .github/ISSUE_TEMPLATE/report_issue.yml
================================================
name: Bug Report 👾
description: File a bug report.
title: "[Bug]: "
labels: ["bug"]
body:
  - type: markdown
    attributes:
      value: |
        Thanks for taking the time to fill out this bug report!
  - type: textarea
    id: what-happened
    attributes:
      label: What Happened?
      description: Also tell us, what did you expect to happen?
      placeholder: Ex. I expected the page to load, but instead I got a 404 error.
    validations:
      required: true
  - type: input
    id: version
    attributes:
      label: Wora Version
      description: Which version of Wora did this bug happen on?
      placeholder: Ex. 0.3.2
    validations:
      required: true
  - type: input
    id: os
    attributes:
      label: Operating System
      description: What operating system are you using?
      placeholder: Ex. Windows 10, macOS 11.2
    validations:
      required: true
  - type: textarea
    id: steps-to-reproduce
    attributes:
      label: Steps to Reproduce
      description: Please provide step-by-step instructions to reproduce the issue.
      placeholder: Ex. 1. Go to '...' 2. Click on '...' 3. Scroll down to '...'
    validations:
      required: true
  - type: textarea
    id: environment-details
    attributes:
      label: Environment Details
      description: Provide any additional details about your environment that might be relevant (e.g., hardware, network conditions).
      placeholder: Ex. Running on a high-latency network, using an external sound card.
    validations:
      required: false
  - type: dropdown
    id: severity
    attributes:
      label: Severity
      description: How severe is this issue? (e.g., Minor, Major, Critical)
      options:
        - Minor
        - Major
        - Critical
      default: 0
    validations:
      required: true
  - type: textarea
    id: logs
    attributes:
      label: Screenshots/Logs
      description: Attach any screenshots or logs that might help in diagnosing the problem.
      placeholder: Ex. Drag and drop your screenshots or logs here.
    validations:
      required: false
  - type: input
    id: contact
    attributes:
      label: Discord Username
      description: How can we get in touch with you if we need more info?
      placeholder: Ex. charlie3x, bluespin2e
    validations:
      required: false
  - type: checkboxes
    id: terms
    attributes:
      label: Code of Conduct
      description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/playwora/wora/blob/main/CODE_OF_CONDUCT.md).
      options:
        - label: I agree to follow this project's Code of Conduct
          required: true


================================================
FILE: .github/ISSUE_TEMPLATE/request_feature.yml
================================================
name: Feature Request 🌟
description: Suggest a new feature or enhancement.
title: "[Feature]: "
labels: ["enhancement"]
body:
  - type: markdown
    attributes:
      value: |
        Thanks for taking the time to suggest a feature!
  - type: textarea
    id: feature-description
    attributes:
      label: Feature Description
      description: Describe the feature you would like to see.
      placeholder: Ex. I would like to have a dark mode option in the settings.
    validations:
      required: true
  - type: textarea
    id: problem-solution
    attributes:
      label: Problem and Solution
      description: Describe the problem this feature will solve and how you envision the solution.
      placeholder: Ex. The app is too bright at night, a dark mode would make it easier on the eyes.
    validations:
      required: true
  - type: textarea
    id: additional-context
    attributes:
      label: Additional Context
      description: Provide any other context or screenshots about the feature request.
      placeholder: Ex. Similar to how dark mode works in other apps.
    validations:
      required: false
  - type: textarea
    id: potential-issues
    attributes:
      label: Potential Issues
      description: Are there any potential issues or challenges with this feature?
      placeholder: Ex. It might be challenging to ensure all UI elements are visible in dark mode.
    validations:
      required: false
  - type: input
    id: version
    attributes:
      label: Wora Version
      description: Which version of Wora are you using?
      placeholder: Ex. 0.3.2
    validations:
      required: true
  - type: input
    id: contact
    attributes:
      label: Discord Username
      description: How can we get in touch with you if we need more info?
      placeholder: Ex. charlie3x, bluespin2e
    validations:
      required: false
  - type: checkboxes
    id: terms
    attributes:
      label: Code of Conduct
      description: By submitting this request, you agree to follow our [Code of Conduct](https://github.com/playwora/wora/blob/main/CODE_OF_CONDUCT.md).
      options:
        - label: I agree to follow this project's Code of Conduct
          required: true


================================================
FILE: .github/workflows/release.yml
================================================
name: Release and Build

on:
  push:
    branches:
      - main

jobs:
  check-version-change:
    runs-on: ubuntu-latest
    outputs:
      version_changed: ${{ steps.check.outputs.version_changed }}
      new_version: ${{ steps.check.outputs.new_version }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2
      - name: Check Version Change
        id: check
        run: |
          git diff HEAD^ HEAD --name-only | grep -q '^package.json$' || exit 0
          old_version=$(git show HEAD^:package.json | jq -r '.version')
          new_version=$(jq -r '.version' package.json)
          if [ "$old_version" != "$new_version" ] && [ $(git diff HEAD^ HEAD --name-only | wc -l) -eq 1 ]; then
            echo "version_changed=true" >> $GITHUB_OUTPUT
            echo "new_version=$new_version" >> $GITHUB_OUTPUT
          else
            echo "version_changed=false" >> $GITHUB_OUTPUT
          fi

  build:
    needs: check-version-change
    if: needs.check-version-change.outputs.version_changed == 'true'
    strategy:
      matrix:
        os: [macos-latest, ubuntu-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest
      - name: Install Dependencies
        run: bun install
      - name: Build for ${{ matrix.os }}
        run: |
          if [ "${{ matrix.os }}" == "macos-latest" ]; then
            bun run build:mac
          elif [ "${{ matrix.os }}" == "ubuntu-latest" ]; then
            bun run build:linux
          elif [ "${{ matrix.os }}" == "windows-latest" ]; then
            bun run build:win64
          fi
        shell: bash
      - name: Get Asset Details
        id: get_asset
        run: |
          if [ "${{ matrix.os }}" == "macos-latest" ]; then
            echo "asset_path=./dist/*.dmg" >> $GITHUB_OUTPUT
          elif [ "${{ matrix.os }}" == "ubuntu-latest" ]; then
            echo "asset_path=./dist/*.AppImage" >> $GITHUB_OUTPUT
          elif [ "${{ matrix.os }}" == "windows-latest" ]; then
            echo "asset_path=./dist/*.exe" >> $GITHUB_OUTPUT
          fi
        shell: bash
      - name: Release
        uses: softprops/action-gh-release@v2
        if: success()
        with:
          tag_name: v${{ needs.check-version-change.outputs.new_version }}
          name: v${{ needs.check-version-change.outputs.new_version }}
          draft: false
          prerelease: false
          files: ${{ steps.get_asset.outputs.asset_path }}
        env:
          GITHUB_TOKEN: ${{ secrets.TOKEN }}


================================================
FILE: .gitignore
================================================
node_modules
*.log
.next
app
dist
.DS_Store
.db

# Environment variables
.env
.env.local
.env.development
.env.production
.vercel


================================================
FILE: .prettierignore
================================================
build
coverage
app
dist
.yarn


================================================
FILE: .prettierrc
================================================
{
  "plugins": ["prettier-plugin-tailwindcss"],
  "tailwindConfig": "./renderer/tailwind.config.js"
}


================================================
FILE: CODE_OF_CONDUCT.md
================================================
<p align="center">
  <img src="https://github.com/playwora/wora/blob/main/renderer/public/github/Header.png?raw=true" alt="Wora Logo" />
</p>

<p align="center">
  <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>
  <a href="https://github.com/playwora/wora"><img src="https://img.shields.io/github/last-commit/playwora/wora/main?commit" alt="Last Commit" /></a>
  <a href="LICENSE"><img src="https://img.shields.io/github/license/playwora/wora?license" alt="License" /></a>
  <a href="https://discord.gg/CrAbAYMGCe"><img src="https://dcbadge.limes.pink/api/server/https://discord.gg/CrAbAYMGCe?style=flat" alt="Discord" /></a>
  <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>
  <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>
  <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>
</p>

## 🤝 Contributor Covenant Code of Conduct

We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation 🌟

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community 🌈

## 📄 Our Standards

Examples of behavior that contributes to a positive environment for our
community include:

- Demonstrating empathy and kindness toward other people 🤗
- Being respectful of differing opinions, viewpoints, and experiences 🤝
- Giving and gracefully accepting constructive feedback 🎯
- Accepting responsibility and apologizing to those affected by our mistakes,
  and learning from the experience 🙏
- Focusing on what is best not just for us as individuals, but for the
  overall community 🌍

Examples of unacceptable behavior include:

- The use of sexualized language or imagery, and sexual attention or
  advances of any kind 🚫
- Trolling, insulting or derogatory comments, and personal or political attacks 🗣️
- Public or private harassment 🔇
- Publishing others' private information, such as a physical or email
  address, without their explicit permission 🕵️
- Other conduct which could reasonably be considered inappropriate in a
  professional setting ❌

## ⭐️ Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful. ⚖️

Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate. ✏️

## 🙌 Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event. 🌐

## 👍 Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[https://discord.gg/CrAbAYMGCe](https://discord.gg/CrAbAYMGCe). 🔗
All complaints will be reviewed and investigated promptly and fairly. ⏱️

All community leaders are obligated to respect the privacy and security of the
reporter of any incident. 🔒

## 🙏 Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:

### 1. Correction

**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.

**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested. 📝

### 2. Warning

**Community Impact**: A violation through a single incident or series
of actions.

**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban. ⚠️

### 3. Temporary Ban

**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.

**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban. ⛔

### 4. Permanent Ban

**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.

**Consequence**: A permanent ban from any sort of public interaction within
the community. 🚷


================================================
FILE: CONTRIBUTING.md
================================================
<p align="center">
  <img src="https://github.com/playwora/wora/blob/main/renderer/public/github/Header.png?raw=true" alt="Wora Logo" />
</p>

<p align="center">
  <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>
  <a href="https://github.com/playwora/wora"><img src="https://img.shields.io/github/last-commit/playwora/wora/main?commit" alt="Last Commit" /></a>
  <a href="LICENSE"><img src="https://img.shields.io/github/license/playwora/wora?license" alt="License" /></a>
  <a href="https://discord.gg/CrAbAYMGCe"><img src="https://dcbadge.limes.pink/api/server/https://discord.gg/CrAbAYMGCe?style=flat" alt="Discord" /></a>
  <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>
  <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>
  <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>
</p>

## 🤝 Contributing to Wora

Thank you for considering contributing to **Wora**! 🎉 We welcome contributions from everyone. We have prepared some guidelines for you to get started ✅

## 🛠️ Project Setup

Wora 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:

```mermaid
erDiagram
    settings {
        int id
        string name
        string profilePicture
        string musicFolder
    }

    songs {
        int id
        string filePath
        string name
        string artist
        int duration
        int albumID
    }

    albums {
        int id
        string name
        string artist
        int year
        string coverArt
    }

    playlists {
        int id
        string name
        string description
        string coverArt
    }

    playlistSongs {
        int playlistId
        int songId
    }

    albums ||--|{ songs : ""
    playlists ||--o{ playlistSongs : ""
    songs ||--o{ playlistSongs : ""
```

## 🎯 **How to Contribute**

Once 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 🛠️

<a href="https://discord.gg/CrAbAYMGCe"><img src="https://dcbadge.limes.pink/api/server/https://discord.gg/CrAbAYMGCe?style=flat" alt="Discord" /></a>

1. **Fork the Repository**

Fork the [repository](https://github.com/playwora/wora) and clone it locally:

```sh
git clone https://github.com/your-username/wora.git
cd wora
```

2. **Create a New Branch**

Create a new branch for your feature or bugfix:

```sh
git checkout -b feature-branch
```

3. **Install Dependencies**

Install the required dependencies:

```sh
yarn install
```

4. **Start Development Server**

Run the development server to see your changes:

```sh
yarn dev
```

5. **Commit Your Changes**

Commit your changes with a meaningful message:

```sh
git commit -am 'Add new feature ✅'
```

6. **Push to Your Branch**

Push the changes to your branch on GitHub:

```sh
git push origin feature-branch
```

7. **Create a Pull Request**

Go 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 🙏

## 💬 Join the Community

Join our [Discord server](https://discord.gg/CrAbAYMGCe) to connect with other users and developers 🤝

<a href="https://discord.gg/CrAbAYMGCe"><img src="https://dcbadge.limes.pink/api/server/https://discord.gg/CrAbAYMGCe?style=flat" alt="Discord"></a>

---

MIT License. Made with ❤️ by [hiaaryan](https://github.com/hiaaryan) and contributors.


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2024 Wora

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
> [!IMPORTANT]  
> 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.

<p align="center">
  <img src="https://github.com/playwora/wora/blob/main/renderer/public/github/Header.png?raw=true" alt="Wora Logo" />
</p>

<p align="center">
  <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>
  <a href="https://github.com/playwora/wora"><img src="https://img.shields.io/github/last-commit/playwora/wora/main?commit" alt="Last Commit" /></a>
  <a href="LICENSE"><img src="https://img.shields.io/github/license/playwora/wora?license" alt="License" /></a>
  <a href="https://discord.gg/CrAbAYMGCe"><img src="https://dcbadge.limes.pink/api/server/https://discord.gg/CrAbAYMGCe?style=flat" alt="Discord" /></a>
  <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>
  <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>
  <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>
</p>

## ⭐️ Description

**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:

- Create and manage playlists 🎉
- Stream FLACs, WAVs apart from regular music extensions 🎧
- Quick play using command menu ⌨️
- View synced and unsynced lyrics 💬
- Admire the beautiful UI ✨

<p align="center">
  <img src="https://github.com/playwora/wora/blob/main/renderer/public/github/Home%20Page.png?raw=true" alt="Screenshot 1" />
  <img src="https://github.com/playwora/wora/blob/main/renderer/public/github/Search%20Console.png?raw=true" alt="Screenshot 2" />
  <img src="https://github.com/playwora/wora/blob/main/renderer/public/github/Album%20Page.png?raw=true" alt="Screenshot 3" />
  <img src="https://github.com/playwora/wora/blob/main/renderer/public/github/Synced%20Lyrics.png?raw=true" alt="Screenshot 4" />
</p>

## 🚀 Getting Started

A 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 🛠️

<a href="https://discord.gg/CrAbAYMGCe"><img src="https://dcbadge.limes.pink/api/server/https://discord.gg/CrAbAYMGCe?style=flat" alt="Discord" /></a>

### 〽️ Prerequisites

- [Node.js](https://nodejs.org/) v14 or higher
- [Git](https://git-scm.com/) for obvious reasons
- [Bun](https://bun.sh/) for dependencies

### 👾 Installation

1. **Clone the repository:**

   ```sh
   git clone https://github.com/playwora/wora.git
   cd wora
   ```

2. **Install the dependencies:**

   ```sh
   bun install
   ```

3. **Start the application:**

   ```sh
   bun run dev
   ```

4. **Build the application**

   ```sh
   bun run build
   ```

## 🤝 Contributing

Contributions are always welcome! Please read the [Contributing Guide](CONTRIBUTING.md) to learn about the process and how to submit your contributions.

1. Fork the repository
2. Create a new branch (`git checkout -b feature-branch`)
3. Commit your changes (`git commit -am 'Add new feature'`)
4. Push to the branch (`git push origin feature-branch`)
5. Create a new Pull Request

## 💬 Join the Community

Join our [Discord server](https://discord.gg/CrAbAYMGCe) to connect with other users and developers.

<a href="https://discord.gg/CrAbAYMGCe"><img src="https://dcbadge.limes.pink/api/server/https://discord.gg/CrAbAYMGCe?style=flat" alt="Discord"></a>

---

<br />
<a href="https://vercel.com/oss">
  <img alt="Vercel OSS Program" src="https://vercel.com/oss/program-badge.svg" />
</a>
<br />
<br />

MIT License. Made with ❤️ by [hiaaryan](https://github.com/hiaaryan) and contributors.


================================================
FILE: components.json
================================================
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "app/globals.css",
    "baseColor": "neutral",
    "cssVariables": false,
    "prefix": ""
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils"
  }
}


================================================
FILE: electron-builder.yml
================================================
appId: com.wora.player
productName: Wora
copyright: Copyright © 2024 Aaryan Kapoor
directories:
  output: dist
  buildResources: resources
files:
  - from: .
    filter:
      - package.json
      - app
publish: null
artifactName: Wora [v${version}].${ext}
linux:
  target:
    - AppImage
  category: Audio
mac:
  target:
    - dmg
  category: public.app-category.music
win:
  target:
    - nsis
fileAssociations:
  - ext: mp3
    name: MP3 Audio File
  - ext: mpeg
    name: MPEG Audio File
  - ext: opus
    name: Opus Audio File
  - ext: ogg
    name: OGG Audio File
  - ext: oga
    name: OGA Audio File
  - ext: wav
    name: WAV Audio File
  - ext: aac
    name: AAC Audio File
  - ext: caf
    name: CAF Audio File
  - ext: m4a
    name: M4A Audio File
  - ext: m4b
    name: M4B Audio File
  - ext: mp4
    name: MP4 Audio File
  - ext: weba
    name: WEBA Audio File
  - ext: webm
    name: WEBM Audio File
  - ext: flac
    name: FLAC Audio File


================================================
FILE: main/background.ts
================================================
import path from "path";
import { Menu, Tray, app, dialog, ipcMain, shell } from "electron";
import serve from "electron-serve";
import { createWindow } from "./helpers";
import { protocol } from "electron";
import {
  addSongToPlaylist,
  addToFavourites,
  createPlaylist,
  db,
  getAlbumWithSongs,
  getAlbums,
  getAllArtists,
  getArtistWithAlbums,
  getLastFmSettings,
  getLibraryStats,
  getPlaylistWithSongs,
  getPlaylists,
  getRandomLibraryItems,
  getSettings,
  initializeData,
  isSongFavorite,
  migrateDatabase,
  removeSongFromPlaylist,
  searchDB,
  searchSongs,
  updateLastFmSettings,
  deletePlaylist,
  updatePlaylist,
  updateSettings,
  getSongs,
  getAlbumsWithDuration,
} from "./helpers/db/connectDB";
import { initDatabase } from "./helpers/db/createDB";
import { parseFile } from "music-metadata";
import fs from "fs";
import { Client } from "@xhayper/discord-rpc";
import { eq, sql } from "drizzle-orm";
import { initializeLastFmHandlers } from "./helpers/lastfm-service";
import * as electronLog from "electron-log";

// Configure application logging for production
electronLog.transports.file.level = "info";
const logger = electronLog.default;

// Log application startup
logger.info(`Wora starting up - ${new Date().toISOString()}`);
logger.info(`Node environment: ${process.env.NODE_ENV}`);
logger.info(`Electron version: ${process.versions.electron}`);
logger.info(`Chrome version: ${process.versions.chrome}`);
logger.info(`OS: ${process.platform} ${process.arch}`);

const isProd = process.env.NODE_ENV === "production";

// Set the app user model id for Windows
if (process.platform === "win32") {
  app.setAppUserModelId("com.hiaaryan.wora");
}

if (isProd) {
  logger.info("Running in production mode");
  serve({ directory: "app" });
} else {
  logger.info("Running in development mode");
  app.setPath("userData", `${app.getPath("userData")}`);
}

let mainWindow: any;
let settings: any;

// Global cache for frequently accessed data
const dataCache = {
  libraryStats: null,
  randomItems: null,
  lastUpdated: 0,
};

// @hiaaryan: Initialize Database on Startup with optimized loading
const initializeLibrary = async () => {
  try {
    // Initialize SQLite database
    await initDatabase();

    // Run database migrations for schema updates
    await migrateDatabase();

    // Only load essential data at startup (settings)
    settings = await getSettings();

    if (settings) {
      // Start a non-blocking initialization of the music library
      // This allows the app UI to load while data is being processed
      setTimeout(() => {
        initializeData(settings.musicFolder, true)
          .then(() => {
            // Pre-cache some common data for faster access
            Promise.all([getLibraryStats(), getRandomLibraryItems()]).then(
              ([stats, randomItems]) => {
                dataCache.libraryStats = stats;
                dataCache.randomItems = randomItems;
                dataCache.lastUpdated = Date.now();

                // Notify renderer that library is fully loaded
                if (mainWindow) {
                  mainWindow.webContents.send("library-initialized");
                }
              },
            );
          })
          .catch((err) => {
            console.error("Error initializing music library:", err);
          });
      }, 1000); // Delay initialization to prioritize app UI loading
    }
  } catch (error) {
    console.error("Error initializing library:", error);
  }
};

(async () => {
  await app.whenReady();
  await initializeLibrary();

  // Initialize Last.fm IPC handlers
  initializeLastFmHandlers();

  // @hiaaryan: Using Depreciated API [Seeking Not Supported with Net]
  protocol.registerFileProtocol("wora", (request, callback) => {
    callback({ path: decodeURIComponent(request.url.replace("wora://", "")) });
  });

  mainWindow = createWindow("main", {
    width: 1500,
    height: 900,
    titleBarStyle: "hidden",
    trafficLightPosition: { x: 20, y: 20 },
    transparent: true,
    frame: false,
    icon: path.join(__dirname, "resources/icon.icns"),
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
      backgroundThrottling: false,
    },
  });

  ipcMain.on("quitApp", async () => {
    return app.quit();
  });

  ipcMain.on("minimizeWindow", async () => {
    return mainWindow.minimize();
  });

  ipcMain.on("maximizeWindow", async (_, isMaximized: boolean) => {
    if (isMaximized) {
      return mainWindow.maximize(isMaximized);
    } else {
      return mainWindow.unmaximize();
    }
  });

  if (settings) {
    if (isProd) {
      await mainWindow.loadURL("app://./home");
    } else {
      const port = process.argv[2];
      await mainWindow.loadURL(`http://localhost:${port}/home`);
    }
  } else {
    if (isProd) {
      await mainWindow.loadURL("app://./setup");
    } else {
      const port = process.argv[2];
      await mainWindow.loadURL(`http://localhost:${port}/setup`);
    }
  }
})();

// @hiaaryan: Initialize Discord RPC
const client = new Client({
  clientId: "1243707416588320800",
});

ipcMain.on(
  "set-rpc-state",
  async (_, { details, state, seek, duration, cover }) => {
    let startTimestamp, endTimestamp;

    if (duration && seek) {
      const now = Math.ceil(Date.now());
      startTimestamp = now - seek * 1000;
      endTimestamp = now + (duration - seek) * 1000;
    }

    const setActivity = {
      details,
      state,
      largeImageKey: cover,
      instance: false,
      type: 2,
      startTimestamp: startTimestamp,
      endTimestamp: endTimestamp,
      buttons: [
        { label: "Support Project", url: "https://github.com/playwora/wora" },
      ],
    };

    if (!client.isConnected) {
      try {
        await client.login();
      } catch (error) {
        console.error("Error logging into Discord:", error);
      }
    }

    if (client.isConnected) {
      client.user.setActivity(setActivity);
    }
  },
);

// @hiaaryan: Called to Rescan Library
ipcMain.handle("rescanLibrary", async () => {
  await initializeLibrary();
});

// @hiaaryan: Called to Set Music Folder
ipcMain.handle("scanLibrary", async () => {
  const diag = await dialog
    .showOpenDialog({
      properties: ["openDirectory", "createDirectory"],
    })
    .then(async (result) => {
      if (result.canceled) {
        return result;
      }

      await initializeData(result.filePaths[0]);
    })
    .catch((err) => {
      console.log(err);
    });

  return diag;
});

// @hiaaryan: Set Tray for Wora
let tray = null;
app.whenReady().then(() => {
  const trayIconPath = !isProd
    ? path.join(__dirname, `../renderer/public/assets/TrayTemplate.png`)
    : path.join(__dirname, `../app/assets/TrayTemplate.png`);
  tray = new Tray(trayIconPath);
  const contextMenu = Menu.buildFromTemplate([
    { label: "About", type: "normal", role: "about" },
    { type: "separator" },
    {
      label: "GitHub",
      type: "normal",
      click: () => {
        shell.openExternal("https://github.com/playwora/wora");
      },
    },
    {
      label: "Discord",
      type: "normal",
      click: () => {
        shell.openExternal("https://discord.gg/CrAbAYMGCe");
      },
    },
    { type: "separator" },
    {
      label: "Quit",
      type: "normal",
      role: "quit",
      accelerator: "Cmd+Q",
    },
  ]);
  tray.setToolTip("Wora");
  tray.setContextMenu(contextMenu);
});

// Use cached data when available for frequently accessed endpoints
ipcMain.handle("getLibraryStats", async () => {
  // Check if we have fresh cached data (less than 5 minutes old)
  if (dataCache.libraryStats && Date.now() - dataCache.lastUpdated < 300000) {
    return dataCache.libraryStats;
  }

  // Otherwise get fresh data and update cache
  const stats = await getLibraryStats();
  dataCache.libraryStats = stats;
  dataCache.lastUpdated = Date.now();
  return stats;
});

ipcMain.handle("getRandomLibraryItems", async () => {
  // Check if we have fresh cached data (less than 5 minutes old)
  if (dataCache.randomItems && Date.now() - dataCache.lastUpdated < 300000) {
    return dataCache.randomItems;
  }

  // Otherwise get fresh data and update cache
  const libraryItems = await getRandomLibraryItems();
  dataCache.randomItems = libraryItems;
  dataCache.lastUpdated = Date.now();
  return libraryItems;
});

// @hiaaryan: IPC Handlers from Renderer
ipcMain.handle("getAlbums", async (_, page) => {
  return await getAlbums(page);
});

// Page state reset handlers
ipcMain.on("resetAlbumsPageState", () => {
  // Notify renderer to reset albums page state
  mainWindow.webContents.send("resetAlbumsState");
});

ipcMain.on("resetSongsPageState", () => {
  // Notify renderer to reset songs page state
  mainWindow.webContents.send("resetSongsState");
});

ipcMain.on("resetPlaylistsPageState", () => {
  // Notify renderer to reset playlists page state
  mainWindow.webContents.send("resetPlaylistsState");
});

ipcMain.on("resetHomePageState", () => {
  // Notify renderer to reset home page state
  mainWindow.webContents.send("resetHomeState");
});

ipcMain.handle("getAllPlaylists", async () => {
  const playlists = await getPlaylists();
  return playlists;
});

ipcMain.handle("getAlbumWithSongs", async (_, id: number) => {
  const albumWithSongs = await getAlbumWithSongs(id);
  return albumWithSongs;
});

ipcMain.handle("getPlaylistWithSongs", async (_, id: number) => {
  const playlistWithSongs = await getPlaylistWithSongs(id);
  return playlistWithSongs;
});

ipcMain.handle("getSongMetadata", async (_, file: string) => {
  const metadata = await parseFile(file, {
    skipPostHeaders: true,
    skipCovers: true,
  });

  const favourite = await isSongFavorite(file);

  return { metadata, favourite };
});

ipcMain.on("addToFavourites", async (_, id: number) => {
  return addToFavourites(id);
});

ipcMain.handle("search", async (_, query: string) => {
  const results = await searchDB(query);
  return results;
});

ipcMain.handle("createPlaylist", async (_, data: any) => {
  const playlist = await createPlaylist(data);
  // Invalidate cache when data changes
  dataCache.lastUpdated = 0;
  return playlist;
});

ipcMain.handle("deletePlaylist", async (_, data: { id: number; coverPath?: string }) => {
  return deletePlaylist(data);
});

ipcMain.handle("updatePlaylist", async (_, data: any) => {
  const playlist = await updatePlaylist(data);
  // Invalidate cache when data changes
  dataCache.lastUpdated = 0;
  return playlist;
});

ipcMain.handle("addSongToPlaylist", async (_, data: any) => {
  const add = await addSongToPlaylist(data.playlistId, data.songId);
  // Invalidate cache when data changes
  dataCache.lastUpdated = 0;
  return add;
});

ipcMain.handle("removeSongFromPlaylist", async (_, data: any) => {
  const remove = await removeSongFromPlaylist(data.playlistId, data.songId);
  // Invalidate cache when data changes
  dataCache.lastUpdated = 0;
  return remove;
});

ipcMain.handle("getSettings", async () => {
  const settings = await getSettings();
  return settings;
});

ipcMain.handle("updateSettings", async (_, data: any) => {
  const settings = await updateSettings(data);
  mainWindow.webContents.send("confirmSettingsUpdate", settings);
  return settings;
});

ipcMain.handle("uploadProfilePicture", async (_, file) => {
  const uploadsDir = path.join(
    app.getPath("userData"),
    "utilities/uploads/profile",
  );
  if (!fs.existsSync(uploadsDir)) {
    fs.mkdirSync(uploadsDir, { recursive: true });
  }

  const fileName = `profile_${Date.now()}${path.extname(file.name)}`;
  const filePath = path.join(uploadsDir, fileName);

  fs.writeFileSync(filePath, Buffer.from(file.data));

  return filePath;
});

ipcMain.handle("uploadPlaylistCover", async (_, file) => {
  const uploadsDir = path.join(
    app.getPath("userData"),
    "utilities/uploads/playlists",
  );
  if (!fs.existsSync(uploadsDir)) {
    fs.mkdirSync(uploadsDir, { recursive: true });
  }

  const fileName = `playlists_${Date.now()}${path.extname(file.name)}`;
  const filePath = path.join(uploadsDir, fileName);

  fs.writeFileSync(filePath, Buffer.from(file.data));

  return filePath;
});

ipcMain.handle("getActionsData", async () => {
  const isNotMac = process.platform !== "darwin";
  const appVersion = app.getVersion();

  return { isNotMac, appVersion };
});

ipcMain.handle("getArtistWithAlbums", async (_, artist: string) => {
  const artistData = await getArtistWithAlbums(artist);
  return artistData;
});

// Handler to get all artists
ipcMain.handle("getAllArtists", async () => {
  try {
    const allArtists = await getAllArtists();
    return allArtists;
  } catch (error) {
    console.error("Error getting all artists:", error);
    return [];
  }
});

// New handler to get all songs for shuffle feature
ipcMain.handle("getAllSongs", async () => {
  try {
    console.log("Getting all songs for shuffle...");

    // Get all songs with their album information in a single query for better performance
    const songsWithAlbums = await db.query.songs.findMany({
      with: {
        album: true, // This fetches the full album data for each song
      },
      orderBy: sql`RANDOM()`, // Randomize the songs to make shuffling more natural
    });

    // Transform the data to match the expected format in the frontend
    const formattedSongs = songsWithAlbums.map((song) => {
      return {
        id: song.id,
        name: song.name || "Unknown Title",
        artist: song.artist || "Unknown Artist",
        duration: song.duration || 0,
        filePath: song.filePath,
        album: song.album
          ? {
              id: song.album.id,
              name: song.album.name || "Unknown Album",
              artist: song.album.artist || "Unknown Artist",
              cover: song.album.cover || null,
              year: song.album.year,
            }
          : {
              id: null,
              name: "Unknown Album",
              artist: "Unknown Artist",
              cover: null,
              year: null,
            },
      };
    });

    console.log(
      `Returning ${formattedSongs.length} songs with complete album data`,
    );
    return formattedSongs;
  } catch (error) {
    console.error("Error in getAllSongs:", error);
    return [];
  }
});

// New handler to get songs with pagination
ipcMain.handle("getSongs", async (_, page: number = 1) => {
  try {
    console.log(`Getting songs for page ${page}...`);
    const songsWithAlbums = await getSongs(page);
    return songsWithAlbums;
  } catch (error) {
    console.error("Error in getSongs:", error);
    return [];
  }
});

// Handler for searching songs with the new searchSongs function
ipcMain.handle("searchSongs", async (_, query: string) => {
  try {
    console.log(`Searching songs with query: "${query}"`);
    const results = await searchSongs(query);
    console.log(`Found ${results.length} song matches`);
    return results;
  } catch (error) {
    console.error("Error in searchSongs:", error);
    return [];
  }
});

// Handler for getting albums with calculated durations
ipcMain.handle("getAlbumsWithDuration", async (_, page: number = 1) => {
  try {
    console.log(`Getting albums with durations for page ${page}...`);
    const albumsWithDurations = await getAlbumsWithDuration(page);
    console.log(`Found ${albumsWithDurations.length} albums with durations`);
    return albumsWithDurations;
  } catch (error) {
    console.error("Error in getAlbumsWithDuration:", error);
    return [];
  }
});

// Add LastFM handlers after existing handlers

// Get LastFM settings
ipcMain.handle("getLastFmSettings", async () => {
  try {
    const lastFmSettings = await getLastFmSettings();
    return lastFmSettings;
  } catch (error) {
    console.error("Error in getLastFmSettings:", error);
    return {
      lastFmUsername: null,
      lastFmSessionKey: null,
      enableLastFm: false,
      scrobbleThreshold: 50,
    };
  }
});

// Update LastFM settings
ipcMain.handle("updateLastFmSettings", async (_, data) => {
  try {
    const result = await updateLastFmSettings(data);

    // Notify all renderer processes that Last.fm settings have changed
    if (mainWindow) {
      mainWindow.webContents.send("lastFmSettingsChanged", data);
    }

    return result;
  } catch (error) {
    console.error("Error in updateLastFmSettings:", error);
    return false;
  }
});

app.on("window-all-closed", () => {
  app.quit();
});


================================================
FILE: main/helpers/create-window.ts
================================================
import {
  screen,
  BrowserWindow,
  BrowserWindowConstructorOptions,
  Rectangle,
} from "electron";
import Store from "electron-store";

export const createWindow = (
  windowName: string,
  options: BrowserWindowConstructorOptions,
): BrowserWindow => {
  const key = "window-state";
  const name = `window-state-${windowName}`;
  const store = new Store<Rectangle>({ name });
  const defaultSize = {
    width: options.width,
    height: options.height,
  };
  let state = {};

  const restore = () => store.get(key, defaultSize);

  const getCurrentPosition = () => {
    const position = win.getPosition();
    const size = win.getSize();
    return {
      x: position[0],
      y: position[1],
      width: size[0],
      height: size[1],
    };
  };

  const windowWithinBounds = (windowState, bounds) => {
    return (
      windowState.x >= bounds.x &&
      windowState.y >= bounds.y &&
      windowState.x + windowState.width <= bounds.x + bounds.width &&
      windowState.y + windowState.height <= bounds.y + bounds.height
    );
  };

  const resetToDefaults = () => {
    const bounds = screen.getPrimaryDisplay().bounds;
    return Object.assign({}, defaultSize, {
      x: (bounds.width - defaultSize.width) / 2,
      y: (bounds.height - defaultSize.height) / 2,
    });
  };

  const ensureVisibleOnSomeDisplay = (windowState) => {
    const visible = screen.getAllDisplays().some((display) => {
      return windowWithinBounds(windowState, display.bounds);
    });
    if (!visible) {
      // Window is partially or fully not visible now.
      // Reset it to safe defaults.
      return resetToDefaults();
    }
    return windowState;
  };

  const saveState = () => {
    if (!win.isMinimized() && !win.isMaximized()) {
      Object.assign(state, getCurrentPosition());
    }
    store.set(key, state);
  };

  state = ensureVisibleOnSomeDisplay(restore());

  const win = new BrowserWindow({
    ...state,
    ...options,
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
      ...options.webPreferences,
    },
  });

  win.on("close", saveState);

  return win;
};


================================================
FILE: main/helpers/db/connectDB.ts
================================================
import { and, eq, like, sql, or, exists, isNotNull } from "drizzle-orm";
import { albums, songs, settings, playlistSongs, playlists } from "./schema";
import fs from "fs";
import { parseFile, selectCover } from "music-metadata";
import path from "path";
import { BetterSQLite3Database, drizzle } from "drizzle-orm/better-sqlite3";
import * as schema from "./schema";
import { sqlite } from "./createDB";
import { app } from "electron";

export const db: BetterSQLite3Database<typeof schema> = drizzle(sqlite, {
  schema,
});

const APP_DATA = app.getPath("userData");
const ART_DIR = path.join(APP_DATA, "utilities/uploads/covers");

const audioExtensions = [
  ".mp3",
  ".mpeg",
  ".opus",
  ".ogg",
  ".oga",
  ".wav",
  ".aac",
  ".caf",
  ".m4a",
  ".m4b",
  ".mp4",
  ".weba",
  ".webm",
  ".dolby",
  ".flac",
];

const imageExtensions = [".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"];

const processedImages = new Map();

function isAudioFile(filePath: string): boolean {
  return audioExtensions.includes(path.extname(filePath).toLowerCase());
}

function findFirstImageInDirectory(dir: string): string | null {
  if (processedImages.has(dir)) {
    return processedImages.get(dir);
  }

  try {
    const files = fs.readdirSync(dir);
    for (const file of files) {
      const filePath = path.join(dir, file);
      const stat = fs.statSync(filePath);
      if (
        stat.isFile() &&
        imageExtensions.includes(path.extname(file).toLowerCase())
      ) {
        processedImages.set(dir, filePath);
        return filePath;
      }
    }
  } catch (error) {
    console.error(`Error reading directory ${dir}:`, error);
  }

  processedImages.set(dir, null);
  return null;
}

function readFilesRecursively(dir: string, batch = 100): string[] {
  let results: string[] = [];
  let stack = [dir];
  let count = 0;

  while (stack.length > 0 && count < batch) {
    const currentDir = stack.pop();
    try {
      const items = fs.readdirSync(currentDir);

      for (const item of items) {
        const itemPath = path.join(currentDir, item);
        try {
          const stat = fs.statSync(itemPath);

          if (stat.isDirectory()) {
            stack.push(itemPath);
          } else if (isAudioFile(itemPath)) {
            results.push(itemPath);
            count++;
            if (count >= batch) break;
          }
        } catch (err) {
          console.error(`Error accessing ${itemPath}:`, err);
        }
      }
    } catch (err) {
      console.error(`Error reading directory ${currentDir}:`, err);
    }
  }

  return results;
}

function scanEntireLibrary(dir: string): string[] {
  let results: string[] = [];

  try {
    const items = fs.readdirSync(dir);

    const chunkSize = 50;
    for (let i = 0; i < items.length; i += chunkSize) {
      const chunk = items.slice(i, i + chunkSize);

      for (const item of chunk) {
        const itemPath = path.join(dir, item);
        try {
          const stat = fs.statSync(itemPath);
          if (stat.isDirectory()) {
            results.push(...scanEntireLibrary(itemPath));
          } else if (isAudioFile(itemPath)) {
            results.push(itemPath);
          }
        } catch (err) {
          console.error(`Error accessing ${itemPath}:`, err);
        }
      }
    }
  } catch (err) {
    console.error(`Error reading directory ${dir}:`, err);
  }

  return results;
}

export const getLibraryStats = async () => {
  const songCount = await db.select({ count: sql`count(*)` }).from(songs);
  const albumCount = await db.select({ count: sql`count(*)` }).from(albums);
  const playlistCount = await db
    .select({ count: sql`count(*)` })
    .from(playlists);

  return {
    songs: songCount[0].count,
    albums: albumCount[0].count,
    playlists: playlistCount[0].count,
  };
};

export const getSettings = async () => {
  const settings = await db.select().from(schema.settings).limit(1);
  return settings[0];
};

export const updateSettings = async (data: any) => {
  const currentSettings = await db.select().from(settings);

  if (currentSettings[0].profilePicture) {
    try {
      fs.unlinkSync(currentSettings[0].profilePicture);
    } catch (error) {
      console.error("Error deleting old profile picture:", error);
    }
  }

  await db.update(settings).set({
    name: data.name,
    profilePicture: data.profilePicture,
  });

  return true;
};

export const getSongs = async (page: number = 1, limit: number = 30) => {
  return await db.query.songs.findMany({
    with: { album: true },
    limit: limit,
    offset: (page - 1) * limit,
    orderBy: (songs, { asc }) => [asc(songs.name)],
  });
};

export const getAlbums = async (page: number, limit: number = 15) => {
  // Get albums with pagination
  const albumsResult = await db
    .select()
    .from(albums)
    .orderBy(albums.name)
    .limit(limit)
    .offset((page - 1) * limit);

  // Get durations for these albums
  const albumsWithDuration = await Promise.all(
    albumsResult.map(async (album) => {
      // Get total duration from songs in this album
      const durationResult = await db
        .select({ totalDuration: sql`SUM(${songs.duration})` })
        .from(songs)
        .where(eq(songs.albumId, album.id));

      return {
        ...album,
        duration: durationResult[0]?.totalDuration || 0,
      };
    }),
  );

  return albumsWithDuration;
};

export const getPlaylists = async () => {
  return await db.select().from(playlists);
};

export const createPlaylist = async (data: any) => {
  let description: string;
  let cover: string;

  if (data.description) {
    description = data.description;
  } else {
    description = "An epic playlist created by you.";
  }

  if (data.cover) {
    cover = data.cover;
  } else {
    cover = null;
  }

  const playlist = await db.insert(playlists).values({
    name: data.name,
    description: description,
    cover: cover,
  });

  return playlist;
};

export const deletePlaylist = async (data: { id: number }) => {
  await db.transaction(async (tx) => {
    // Remove all links in playlistSongs
    await tx.delete(playlistSongs).where(eq(playlistSongs.playlistId, data.id));

    // Now delete the playlist
    const result = await tx.delete(playlists).where(eq(playlists.id, data.id));
    if ("changes" in result && result.changes === 0) {
      throw new Error(`Playlist ${data.id} not found`);
    }
  });

  return { message: `Playlist ${data.id} deleted successfully` };
};

export const updatePlaylist = async (data: any) => {
  let description: string;
  let cover: string;

  if (data.data.description) {
    description = data.data.description;
  } else {
    description = "An epic playlist created by you.";
  }

  if (data.cover) {
    cover = data.data.cover;
  }

  const playlist = await db
    .update(playlists)
    .set({
      name: data.data.name,
      description: description,
      cover: cover,
    })
    .where(eq(playlists.id, data.id));

  return playlist;
};

export const getAlbumWithSongs = async (id: number) => {
  const albumWithSongs = await db.query.albums.findFirst({
    where: eq(albums.id, id),
    with: {
      songs: {
        with: { album: true },
      },
    },
  });

  if (albumWithSongs) {
    // Calculate total duration from all songs in this album
    const totalDuration = albumWithSongs.songs.reduce(
      (total, song) => total + (song.duration || 0),
      0,
    );

    return {
      ...albumWithSongs,
      duration: totalDuration,
    };
  }

  return albumWithSongs;
};

export const getPlaylistWithSongs = async (id: number) => {
  const playlistWithSongs = await db.query.playlists.findFirst({
    where: eq(playlists.id, id),
    with: {
      songs: {
        with: {
          song: {
            with: { album: true },
          },
        },
      },
    },
  });

  return {
    ...playlistWithSongs,
    songs: playlistWithSongs.songs.map((playlistSong) => ({
      ...playlistSong.song,
      album: playlistSong.song.album,
    })),
  };
};

export const isSongFavorite = async (file: string) => {
  const song = await db.query.songs.findFirst({
    where: eq(songs.filePath, file),
  });

  if (!song) return false;

  const isFavourite = await db.query.playlistSongs.findFirst({
    where: and(
      eq(playlistSongs.playlistId, 1),
      eq(playlistSongs.songId, song.id),
    ),
  });

  return !!isFavourite;
};

export const addToFavourites = async (songId: number) => {
  const existingEntry = await db
    .select()
    .from(playlistSongs)
    .where(
      and(eq(playlistSongs.playlistId, 1), eq(playlistSongs.songId, songId)),
    );

  if (!existingEntry[0]) {
    await db.insert(playlistSongs).values({
      playlistId: 1,
      songId,
    });
  } else {
    await db
      .delete(playlistSongs)
      .where(
        and(eq(playlistSongs.playlistId, 1), eq(playlistSongs.songId, songId)),
      );
  }
};

export const searchDB = async (query: string) => {
  const lowerSearch = query.toLowerCase();

  const searchAlbums = await db.query.albums.findMany({
    where: like(albums.name, `%${lowerSearch}%`),
    limit: 5,
  });

  const searchPlaylists = await db.query.playlists.findMany({
    where: like(playlists.name, `%${lowerSearch}%`),
    limit: 5,
  });

  const searchSongs = await db.query.songs.findMany({
    where: like(songs.name, `%${lowerSearch}%`),
    with: {
      album: {
        columns: {
          id: true,
          cover: true,
        },
      },
    },
    limit: 5,
  });

  // Search for artists by querying unique artist names from the albums table
  const searchArtists = await db.query.albums.findMany({
    where: like(albums.artist, `%${lowerSearch}%`),
    columns: {
      artist: true,
    },
    limit: 5,
  });

  // Remove duplicate artists by name
  const uniqueArtists = Array.from(
    new Set(searchArtists.map((a) => a.artist)),
  ).map((name) => ({
    name,
  }));

  return {
    searchAlbums,
    searchPlaylists,
    searchSongs,
    searchArtists: uniqueArtists,
  };
};

export const addSongToPlaylist = async (playlistId: number, songId: number) => {
  const checkIfExists = await db.query.playlistSongs.findFirst({
    where: and(
      eq(playlistSongs.playlistId, playlistId),
      eq(playlistSongs.songId, songId),
    ),
  });

  if (checkIfExists) return false;

  await db.insert(playlistSongs).values({
    playlistId,
    songId,
  });

  return true;
};

export const removeSongFromPlaylist = async (
  playlistId: number,
  songId: number,
) => {
  await db
    .delete(playlistSongs)
    .where(
      and(
        eq(playlistSongs.playlistId, playlistId),
        eq(playlistSongs.songId, songId),
      ),
    );

  return true;
};

export const getRandomLibraryItems = async () => {
  const randomAlbums = await db
    .select()
    .from(albums)
    .orderBy(sql`RANDOM()`)
    .limit(10);

  // Add duration calculation for albums
  const albumsWithDuration = await Promise.all(
    randomAlbums.map(async (album) => {
      // Get total duration from songs in this album
      const durationResult = await db
        .select({ totalDuration: sql`SUM(${songs.duration})` })
        .from(songs)
        .where(eq(songs.albumId, album.id));

      return {
        ...album,
        duration: durationResult[0]?.totalDuration || 0,
      };
    }),
  );

  const randomSongs = await db.query.songs.findMany({
    with: { album: true },
    limit: 10,
    orderBy: sql`RANDOM()`,
  });

  return {
    albums: albumsWithDuration,
    songs: randomSongs,
  };
};

// Added incremental loading support
export const initializeData = async (
  musicFolder: string,
  incremental = false,
) => {
  if (!fs.existsSync(musicFolder)) {
    console.error("Music folder does not exist:", musicFolder);
    return false;
  }

  try {
    // Add default playlist if it doesn't exist
    const defaultPlaylist = await db
      .select()
      .from(playlists)
      .where(eq(playlists.id, 1));

    if (!defaultPlaylist[0]) {
      await db.insert(playlists).values({
        name: "Favourites",
        cover: null,
        description: "Songs liked by you.",
      });
    }

    // Update settings
    const existingSettings = await db
      .select()
      .from(settings)
      .where(eq(settings.id, 1));

    if (existingSettings[0]) {
      await db.update(settings).set({ musicFolder }).where(eq(settings.id, 1));
    } else {
      await db.insert(settings).values({ musicFolder });
    }

    // Create art directory if it doesn't exist
    if (!fs.existsSync(ART_DIR)) {
      await fs.promises.mkdir(ART_DIR, { recursive: true });
    }

    // First pass: Just load metadata or do a full scan based on incremental flag
    await processLibrary(musicFolder, incremental);

    return true;
  } catch (error) {
    console.error("Error initializing data:", error);
    return false;
  }
};

// Batch process files to reduce memory usage and improve UI responsiveness
async function processLibrary(musicFolder: string, incremental = false) {
  const startTime = Date.now();
  const dbFilePaths = await getAllFilePathsFromDb();

  if (incremental) {
    console.log("Starting incremental library scan...");

    // Scan only the immediate music folder first to reduce initial delay
    const initialBatch = scanImmediateDirectory(musicFolder);
    const batchSize = 100; // Increased from 50 for better throughput

    // Process the initial batch right away for quick UI updates
    await processBatch(initialBatch, dbFilePaths);

    // Process the rest of the library in the background
    setTimeout(async () => {
      // Use a more efficient scanning algorithm for the full scan
      const allFiles = scanEntireLibrary(musicFolder);
      console.log(`Found ${allFiles.length} files in music library`);

      // Skip files we've already processed in the initial batch
      for (let i = initialBatch.length; i < allFiles.length; i += batchSize) {
        const batch = allFiles.slice(i, i + batchSize);
        await processBatch(batch, dbFilePaths);

        // Yield to UI thread periodically but not too often (increased from 10ms)
        if (i % (batchSize * 5) === 0) {
          await new Promise((resolve) => setTimeout(resolve, 30));
        }
      }

      // Final cleanup - remove orphaned records
      await cleanupOrphanedRecords(allFiles);

      console.log(
        `Library processing completed in ${(Date.now() - startTime) / 1000} seconds`,
      );
    }, 1000); // Reduced from 2000ms for faster startup
  } else {
    // Do full scan immediately if not incremental
    const allFiles = scanEntireLibrary(musicFolder);
    console.log(`Found ${allFiles.length} files in music library`);

    // Process in larger batches since we're not concerned about UI responsiveness
    const batchSize = 300; // Increased from 200 for better throughput

    for (let i = 0; i < allFiles.length; i += batchSize) {
      const batch = allFiles.slice(i, i + batchSize);
      await processBatch(batch, dbFilePaths);

      // Still yield occasionally to prevent potential lockups
      if (i % (batchSize * 3) === 0) {
        await new Promise((resolve) => setTimeout(resolve, 20));
      }
    }

    await cleanupOrphanedRecords(allFiles);
    console.log(
      `Library processing completed in ${(Date.now() - startTime) / 1000} seconds`,
    );
  }
}

// Helper function to get all file paths from database
async function getAllFilePathsFromDb(): Promise<Set<string>> {
  const dbFiles = await db.select().from(songs);
  return new Set(dbFiles.map((file) => file.filePath));
}

// Scan only the immediate directory for quick initial loading
function scanImmediateDirectory(dir: string): string[] {
  let results: string[] = [];

  try {
    const items = fs.readdirSync(dir);

    // First collect all audio files in the current directory
    for (const item of items) {
      const itemPath = path.join(dir, item);
      try {
        const stat = fs.statSync(itemPath);
        if (!stat.isDirectory() && isAudioFile(itemPath)) {
          results.push(itemPath);
        }
      } catch (err) {
        console.error(`Error accessing ${itemPath}:`, err);
      }
    }

    // Then check immediate subdirectories (but not recursively)
    for (const item of items) {
      const itemPath = path.join(dir, item);
      try {
        const stat = fs.statSync(itemPath);
        if (stat.isDirectory()) {
          const subItems = fs.readdirSync(itemPath);
          for (const subItem of subItems) {
            const subItemPath = path.join(itemPath, subItem);
            try {
              const subStat = fs.statSync(subItemPath);
              if (!subStat.isDirectory() && isAudioFile(subItemPath)) {
                results.push(subItemPath);
              }
            } catch (err) {
              console.error(`Error accessing ${subItemPath}:`, err);
            }
          }
        }
      } catch (err) {
        console.error(`Error accessing ${itemPath}:`, err);
      }
    }
  } catch (err) {
    console.error(`Error reading directory ${dir}:`, err);
  }

  return results;
}

async function processBatch(files: string[], dbFilePaths: Set<string>) {
  const albumCache = new Map();

  for (const file of files) {
    try {
      if (!dbFilePaths.has(file)) {
        // New file - add to database
        await processAudioFile(file, albumCache);
      }
    } catch (error) {
      console.error(`Error processing file ${file}:`, error);
    }
  }
}

async function processAudioFile(file: string, albumCache: Map<string, any>) {
  try {
    // Use more efficient metadata parsing with stripped options
    const metadata = await parseFile(file, {
      skipPostHeaders: true,
      skipCovers: false, // Still need covers
      duration: true,
      includeChapters: false,
    });

    // Skip files with insufficient metadata
    if (!metadata.common.title) {
      return;
    }

    const albumFolder = path.dirname(file);
    let artPath = null;

    // Try to find album art in efficient order: embedded first, then folder
    if (metadata.common.picture && metadata.common.picture.length > 0) {
      const cover = selectCover(metadata.common.picture);
      if (cover) {
        artPath = await processEmbeddedArt(cover);
      }
    } else {
      // Fall back to external images if no embedded art is found
      const albumImage = findFirstImageInDirectory(albumFolder);
      if (albumImage) {
        artPath = await processAlbumArt(albumImage);
      }
    }

    // Get or create album with better caching
    let album;
    const albumKey = `${metadata.common.album || "Unknown Album"}-${metadata.common.artist || "Unknown Artist"}`;

    if (albumCache.has(albumKey)) {
      album = albumCache.get(albumKey);
    } else {
      // Optimize the database lookup for album
      const albumsFound = await db
        .select()
        .from(albums)
        .where(eq(albums.name, metadata.common.album || "Unknown Album"));

      if (albumsFound.length > 0) {
        album = albumsFound[0];

        // Update album if needed (only when data differs)
        const albumArtist =
          metadata.common.albumartist ||
          metadata.common.artist ||
          "Various Artists";
        if (
          album.artist !== albumArtist ||
          album.year !== metadata.common.year ||
          (artPath && album.cover !== artPath)
        ) {
          await db
            .update(albums)
            .set({
              artist: albumArtist,
              year: metadata.common.year,
              cover: artPath || album.cover,
            })
            .where(eq(albums.id, album.id));

          // Update cached version
          album.artist = albumArtist;
          album.year = metadata.common.year;
          album.cover = artPath || album.cover;
        }
      } else {
        // Create new album with a single transaction
        const [newAlbum] = await db
          .insert(albums)
          .values({
            name: metadata.common.album || "Unknown Album",
            artist:
              metadata.common.albumartist ||
              metadata.common.artist ||
              "Various Artists",
            year: metadata.common.year,
            cover: artPath,
          })
          .returning();

        album = newAlbum;
      }

      albumCache.set(albumKey, album);
    }

    // Add the song using pre-calculated values to avoid repeated operations
    await db.insert(songs).values({
      filePath: file,
      name: metadata.common.title,
      artist: metadata.common.artist || "Unknown Artist",
      duration: Math.round(metadata.format.duration || 0),
      albumId: album.id,
    });
  } catch (error) {
    console.error(`Error processing audio file ${file}:`, error);
  }
}

async function processAlbumArt(imagePath: string): Promise<string> {
  try {
    // Use a shorter hash method for faster processing
    const crypto = require("crypto");
    const imageExt = path.extname(imagePath).slice(1);

    // Generate hash from filename and modified time instead of reading the whole file
    // This is much faster for large image files
    const stats = fs.statSync(imagePath);
    const hashInput = `${imagePath}-${stats.size}-${stats.mtimeMs}`;
    const hash = crypto.createHash("md5").update(hashInput).digest("hex");

    const artPath = path.join(ART_DIR, `${hash}.${imageExt}`);

    // If the processed file already exists, return its path immediately
    if (fs.existsSync(artPath)) {
      return artPath;
    }

    // Only read the file if we need to process it
    const imageData = fs.readFileSync(imagePath);

    // For common image formats that don't need processing, just copy the file
    if (imageExt.match(/^(jpe?g|png|webp)$/i)) {
      await fs.promises.writeFile(artPath, imageData);
      return artPath;
    }

    // For other formats, we might want to convert them (implementation depends on available modules)
    // For now, just save as is
    await fs.promises.writeFile(artPath, imageData);
    return artPath;
  } catch (error) {
    console.error("Error processing album art:", error);
    return null;
  }
}

async function processEmbeddedArt(cover: any): Promise<string> {
  try {
    // If we don't have cover data, return early
    if (!cover || !cover.data) {
      return null;
    }

    // Generate a hash based on a small sample of the image data
    // Using the full data can be slow for large embedded images
    const sampleSize = Math.min(cover.data.length, 4096); // Sample first 4KB
    const sampleBuffer = cover.data.slice(0, sampleSize);

    const crypto = require("crypto");
    const hash = crypto.createHash("md5").update(sampleBuffer).digest("hex");

    const format = cover.format ? cover.format.split("/")[1] || "jpg" : "jpg";

    const artPath = path.join(ART_DIR, `${hash}.${format}`);

    // Skip writing if it already exists
    if (fs.existsSync(artPath)) {
      return artPath;
    }

    // Write the full image data
    await fs.promises.writeFile(artPath, cover.data);
    return artPath;
  } catch (error) {
    console.error("Error processing embedded art:", error);
    return null;
  }
}

async function cleanupOrphanedRecords(currentFiles: string[]) {
  // Create a set of current file paths for faster lookups
  const currentFilesSet = new Set(currentFiles);

  // Get all songs from the database
  const dbFiles = await db.select().from(songs);

  // Find songs that no longer exist
  const deletedFiles = dbFiles.filter(
    (dbFile) => !currentFilesSet.has(dbFile.filePath),
  );

  if (deletedFiles.length > 0) {
    console.log(`Removing ${deletedFiles.length} orphaned song records`);

    // Delete in batches to avoid locking the database for too long
    const batchSize = 50;
    for (let i = 0; i < deletedFiles.length; i += batchSize) {
      const batch = deletedFiles.slice(i, i + batchSize);

      await db.transaction(async (tx) => {
        for (const file of batch) {
          await tx
            .delete(playlistSongs)
            .where(eq(playlistSongs.songId, file.id));
          await tx.delete(songs).where(eq(songs.id, file.id));
        }
      });
    }
  }

  // Clean up empty albums
  const allAlbums = await db.select().from(albums);

  for (const album of allAlbums) {
    const songsInAlbum = await db
      .select()
      .from(songs)
      .where(eq(songs.albumId, album.id));

    if (songsInAlbum.length === 0) {
      await db.delete(albums).where(eq(albums.id, album.id));
    }
  }
}

// Migrate database to add columns that might be missing
export const migrateDatabase = async () => {
  try {
    console.log("Checking database schema for migrations...");

    // Check if LastFM columns exist in settings table
    const tableInfo = sqlite
      .prepare("PRAGMA table_info(settings)")
      .all() as Array<{ name: string }>;
    const columnNames = tableInfo.map((col) => col.name);

    const missingColumns = [];

    // Check for lastFmUsername column
    if (!columnNames.includes("lastFmUsername")) {
      missingColumns.push("lastFmUsername TEXT");
    }

    // Check for lastFmSessionKey column
    if (!columnNames.includes("lastFmSessionKey")) {
      missingColumns.push("lastFmSessionKey TEXT");
    }

    // Check for enableLastFm column
    if (!columnNames.includes("enableLastFm")) {
      missingColumns.push("enableLastFm INTEGER DEFAULT 0");
    }

    // Check for scrobbleThreshold column
    if (!columnNames.includes("scrobbleThreshold")) {
      missingColumns.push("scrobbleThreshold INTEGER DEFAULT 50");
    }

    // Add missing columns if any
    if (missingColumns.length > 0) {
      console.log(
        `Adding ${missingColumns.length} missing columns to settings table...`,
      );

      for (const columnDef of missingColumns) {
        const alterSql = `ALTER TABLE settings ADD COLUMN ${columnDef}`;
        sqlite.exec(alterSql);
        console.log(`Added column: ${columnDef}`);
      }

      console.log("Database migration completed successfully.");
    } else {
      console.log("Database schema is up to date, no migration needed.");
    }

    return true;
  } catch (error) {
    console.error("Error during database migration:", error);
    return false;
  }
};

// Helper function to send messages to the renderer process
function sendToRenderer(channel: string, data: any) {
  try {
    // Check if we have access to the webContents
    const { BrowserWindow } = require("electron");
    const win = BrowserWindow.getAllWindows()[0];
    if (win && win.webContents) {
      win.webContents.send(channel, data);
    }
  } catch (error) {
    console.error(`Failed to send message to renderer: ${error}`);
  }
}

export const getArtistWithAlbums = async (artist: string) => {
  try {
    if (!artist) {
      console.log("Missing artist name in getArtistWithAlbums");
      return {
        name: "Unknown Artist",
        albums: [],
        albumsWithSongs: [],
        songs: [],
        stats: null,
      };
    }

    // Get all albums by this artist
    const artistAlbums = await db
      .select()
      .from(albums)
      .where(eq(albums.artist, artist))
      .orderBy(albums.year);

    // Get all songs by this artist (across all albums)
    const artistSongs = await db.query.songs.findMany({
      where: eq(songs.artist, artist),
      with: {
        album: true,
      },
      orderBy: (songs, { asc }) => [asc(songs.name)],
    });

    // Group songs by albums for better organization
    const albumsWithSongs = await Promise.all(
      artistAlbums.map(async (album) => {
        const albumSongs = await db.query.songs.findMany({
          where: eq(songs.albumId, album.id),
          with: {
            album: true,
          },
          orderBy: (songs, { asc }) => [asc(songs.name)],
        });

        return {
          ...album,
          songs: albumSongs,
        };
      }),
    );

    // Calculate statistics
    const totalDuration = artistSongs.reduce(
      (sum, song) => sum + (song.duration || 0),
      0,
    );
    const genres = new Set<string>();
    const formats = new Set<string>();

    // Extract genres and formats from songs
    artistSongs.forEach((song) => {
      if (song.filePath) {
        const ext = song.filePath.split(".").pop()?.toUpperCase();
        if (ext) formats.add(ext);
      }
    });

    // Get year range
    const years = artistAlbums.filter((a) => a.year).map((a) => a.year);
    const yearRange =
      years.length > 0
        ? { start: Math.min(...years), end: Math.max(...years) }
        : null;

    // Get most played song (would need play count tracking, using random for now)
    const topSongs = artistSongs.slice(0, 5).map((song) => ({
      id: song.id,
      name: song.name,
      duration: song.duration,
      album: song.album?.name || "Unknown Album",
    }));

    const stats = {
      totalSongs: artistSongs.length,
      totalAlbums: artistAlbums.length,
      totalDuration,
      genres: Array.from(genres),
      formats: Array.from(formats),
      yearRange,
      topSongs,
    };

    return {
      name: artist,
      albums: artistAlbums,
      albumsWithSongs: albumsWithSongs,
      songs: artistSongs,
      stats,
    };
  } catch (error) {
    console.error(`Error in getArtistWithAlbums for "${artist}":`, error);
    return {
      name: artist || "Unknown Artist",
      albums: [],
      albumsWithSongs: [],
      songs: [],
      stats: null,
    };
  }
};

export const getAllArtists = async () => {
  try {
    const albumArtists = await db
      .selectDistinct({ artist: albums.artist })
      .from(albums)
      .where(isNotNull(albums.artist));

    const songArtists = await db
      .selectDistinct({ artist: songs.artist })
      .from(songs)
      .where(isNotNull(songs.artist));

    const artistNames = new Set<string>();
    albumArtists.forEach((a) => {
      if (a.artist) artistNames.add(a.artist);
    });
    songArtists.forEach((s) => {
      if (s.artist) artistNames.add(s.artist);
    });

    const artistStats = await db
      .select({
        artist: albums.artist,
        albumCount: sql<number>`COUNT(DISTINCT ${albums.id})`,
        cover: sql<string>`MAX(${albums.cover})`,
      })
      .from(albums)
      .where(isNotNull(albums.artist))
      .groupBy(albums.artist);

    const songStats = await db
      .select({
        artist: songs.artist,
        songCount: sql<number>`COUNT(*)`,
      })
      .from(songs)
      .where(isNotNull(songs.artist))
      .groupBy(songs.artist);

    const songCountMap = new Map<string, number>();
    songStats.forEach((s) => {
      if (s.artist) {
        songCountMap.set(s.artist, Number(s.songCount));
      }
    });

    // Combine the data
    const artistsWithDetails = Array.from(artistNames).map((artistName) => {
      const albumData = artistStats.find((a) => a.artist === artistName);
      return {
        name: artistName,
        albumCount: albumData ? Number(albumData.albumCount) : 0,
        songCount: songCountMap.get(artistName) || 0,
        cover: albumData?.cover || null,
      };
    });

    // Sort by artist name
    return artistsWithDetails.sort((a, b) =>
      a.name.localeCompare(b.name, undefined, { sensitivity: "base" }),
    );
  } catch (error) {
    console.error("Error getting all artists:", error);
    return [];
  }
};

export const searchSongs = async (query: string) => {
  if (!query || query.trim() === "") {
    return [];
  }

  // Normalize the search query
  const searchTerm = `%${query.toLowerCase().trim()}%`;

  // Efficiently search for songs matching the query across name, artist and album name
  const searchResults = await db.query.songs.findMany({
    where: or(
      like(songs.name, searchTerm),
      like(songs.artist, searchTerm),
      // Join with albums to search by album name
      exists(
        db
          .select()
          .from(albums)
          .where(
            and(eq(albums.id, songs.albumId), like(albums.name, searchTerm)),
          ),
      ),
    ),
    with: {
      album: true,
    },
    // Limit to a reasonable number to avoid performance issues
    limit: 100,
    orderBy: (songs, { asc }) => [asc(songs.name)],
  });

  return searchResults;
};

export const getAlbumsWithDuration = async (
  page: number = 1,
  limit: number = 15,
) => {
  // Get albums with pagination, including a more efficient duration calculation
  const albumsResult = await db
    .select()
    .from(albums)
    .orderBy(albums.name)
    .limit(limit)
    .offset((page - 1) * limit);

  // Get durations for these albums in a single batch query for better performance
  const albumIds = albumsResult.map((album) => album.id);

  // If no albums were found, return empty array
  if (albumIds.length === 0) {
    return [];
  }

  // Query total durations for all albums in a single database call
  const durationResults = await db
    .select({
      albumId: songs.albumId,
      totalDuration: sql`SUM(${songs.duration})`,
    })
    .from(songs)
    .where(sql`${songs.albumId} IN (${albumIds.join(",")})`)
    .groupBy(songs.albumId);

  // Create a duration lookup map for efficient access
  const durationMap = new Map();
  durationResults.forEach((result) => {
    durationMap.set(result.albumId, result.totalDuration || 0);
  });

  // Map the albums with their durations
  const albumsWithDurations = albumsResult.map((album) => {
    return {
      ...album,
      duration: durationMap.get(album.id) || 0,
    };
  });

  return albumsWithDurations;
};

// Add these functions at the end of the file

// LastFM related functions
export const updateLastFmSettings = async (data: {
  lastFmUsername: string;
  lastFmSessionKey: string;
  enableLastFm: boolean;
  scrobbleThreshold: number;
}) => {
  try {
    const currentSettings = await db.select().from(settings);

    if (currentSettings.length === 0) {
      // Create new settings if none exist
      await db.insert(settings).values({
        lastFmUsername: data.lastFmUsername,
        lastFmSessionKey: data.lastFmSessionKey,
        enableLastFm: data.enableLastFm,
        scrobbleThreshold: data.scrobbleThreshold || 50,
      });
    } else {
      // Update existing settings
      await db
        .update(settings)
        .set({
          lastFmUsername: data.lastFmUsername,
          lastFmSessionKey: data.lastFmSessionKey,
          enableLastFm: data.enableLastFm,
          scrobbleThreshold: data.scrobbleThreshold || 50,
        })
        .where(eq(settings.id, currentSettings[0].id));
    }

    return true;
  } catch (error) {
    console.error("Error updating LastFM settings:", error);
    return false;
  }
};

export const getLastFmSettings = async () => {
  try {
    const settingsRow = await db
      .select({
        lastFmUsername: settings.lastFmUsername,
        lastFmSessionKey: settings.lastFmSessionKey,
        enableLastFm: settings.enableLastFm,
        scrobbleThreshold: settings.scrobbleThreshold,
      })
      .from(settings)
      .limit(1);

    if (settingsRow.length === 0) {
      return {
        lastFmUsername: null,
        lastFmSessionKey: null,
        enableLastFm: false,
        scrobbleThreshold: 50,
      };
    }

    return settingsRow[0];
  } catch (error) {
    console.error("Error getting LastFM settings:", error);
    return {
      lastFmUsername: null,
      lastFmSessionKey: null,
      enableLastFm: false,
      scrobbleThreshold: 50,
    };
  }
};

================================================
FILE: main/helpers/db/createDB.ts
================================================
import Database from "better-sqlite3";
import { app } from "electron";
import path from "path";

export const sqlite = new Database(
  path.join(app.getPath("userData"), "wora.db"),
);

export const initDatabase = async () => {
  sqlite.exec(`
      CREATE TABLE IF NOT EXISTS settings (
        id INTEGER PRIMARY KEY,
        name TEXT,
        profilePicture TEXT,
        musicFolder TEXT
      );
      CREATE TABLE IF NOT EXISTS albums (
        id INTEGER PRIMARY KEY,
        name TEXT,
        artist TEXT,
        year INTEGER,
        cover TEXT
      );
      CREATE TABLE IF NOT EXISTS songs (
        id INTEGER PRIMARY KEY,
        filePath TEXT,
        name TEXT,
        artist TEXT,
        duration INTEGER,
        albumId INTEGER,
        FOREIGN KEY (albumId) REFERENCES albums(id)
      );
      CREATE TABLE IF NOT EXISTS playlists (
        id INTEGER PRIMARY KEY,
        name TEXT,
        description TEXT,
        cover TEXT
      );
      CREATE TABLE IF NOT EXISTS playlistSongs (
        playlistId INTEGER,
        songId INTEGER,
        FOREIGN KEY (playlistId) REFERENCES playlists(id),
        Foreign KEY (songId) REFERENCES songs(id)
      );
  `);
};


================================================
FILE: main/helpers/db/schema.ts
================================================
import { integer, sqliteTable, text, blob } from "drizzle-orm/sqlite-core";
import { relations } from "drizzle-orm";

export const settings = sqliteTable("settings", {
  id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }),
  name: text("name"),
  profilePicture: text("profilePicture"),
  musicFolder: text("musicFolder"),
  lastFmUsername: text("lastFmUsername"),
  lastFmSessionKey: text("lastFmSessionKey"),
  enableLastFm: integer("enableLastFm", { mode: "boolean" }).default(false),
  scrobbleThreshold: integer("scrobbleThreshold").default(50),
});

export const albums = sqliteTable("albums", {
  id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }),
  name: text("name"),
  artist: text("artist"),
  year: integer("year"),
  cover: text("cover"),
});

export const songs = sqliteTable("songs", {
  id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }),
  filePath: text("filePath"),
  name: text("name"),
  artist: text("artist"),
  duration: integer("duration"),
  albumId: integer("albumId").references(() => albums.id),
});

export const albumsRelations = relations(albums, ({ many }) => ({
  songs: many(songs),
}));

export const songsRelations = relations(songs, ({ one }) => ({
  album: one(albums, {
    fields: [songs.albumId],
    references: [albums.id],
  }),
}));

export const playlists = sqliteTable("playlists", {
  id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }),
  name: text("name").notNull().unique(),
  description: text("description").notNull(),
  cover: text("cover").notNull(),
});

export const playlistSongs = sqliteTable("playlistSongs", {
  playlistId: integer("playlistId").references(() => playlists.id, {
    onDelete: "cascade",
  }),
  songId: integer("songId").references(() => songs.id, {
    onDelete: "cascade",
  }),
});

export const playlistRelations = relations(playlists, ({ many }) => ({
  songs: many(playlistSongs),
}));

export const playlistSongRelations = relations(playlistSongs, ({ one }) => ({
  playlist: one(playlists, {
    fields: [playlistSongs.playlistId],
    references: [playlists.id],
  }),
  song: one(songs, { fields: [playlistSongs.songId], references: [songs.id] }),
}));


================================================
FILE: main/helpers/index.ts
================================================
export * from "./create-window";


================================================
FILE: main/helpers/lastfm-service.ts
================================================
import { ipcMain } from "electron";
import fetch from "node-fetch";
import * as crypto from "crypto";
import * as path from "path";
import * as fs from "fs";
import * as electronLog from "electron-log";

const lastFmLogger = electronLog.create({ logId: "lastfm" });
lastFmLogger.transports.file.fileName = "lastfm.log";
lastFmLogger.transports.file.level = "info";

const logLastFm = (
  message: string,
  data?: any,
  level: "info" | "error" | "warn" = "info",
) => {
  const shouldLogToConsole = process.env.NODE_ENV !== "production";
  switch (level) {
    case "error":
      lastFmLogger.error(message, data);
      if (shouldLogToConsole) console.error(`[LastFm] ${message}`, data || "");
      break;
    case "warn":
      lastFmLogger.warn(message, data);
      if (shouldLogToConsole) console.warn(`[LastFm] ${message}`, data || "");
      break;
    default:
      lastFmLogger.info(message, data);
      if (shouldLogToConsole) console.log(`[LastFm] ${message}`, data || "");
  }
};

const API_URL = "https://ws.audioscrobbler.com/2.0/";

const apiCache = new Map<string, { data: any; timestamp: number }>();
const CACHE_TTL = 15 * 60 * 1000;

const loadEnvVariables = () => {
  try {
    const envPath = path.join(process.cwd(), ".env.local");
    if (!fs.existsSync(envPath)) return false;

    logLastFm(`Loading environment variables from ${envPath}`);
    const envContent = fs.readFileSync(envPath, "utf-8");

    envContent.split("\n").forEach((line) => {
      const match = line.match(/^\s*([\w.-]+)\s*=\s*(.*)?\s*$/);
      if (match) {
        const key = match[1];
        let value = match[2] || "";
        if (
          value.length > 0 &&
          value.charAt(0) === '"' &&
          value.charAt(value.length - 1) === '"'
        ) {
          value = value.replace(/^"|"$/g, "");
        }
        process.env[key] = value;
      }
    });
    return true;
  } catch (error) {
    logLastFm("Error loading environment variables", error, "error");
    return false;
  }
};

if (process.env.NODE_ENV !== "production") {
  loadEnvVariables();
}

const DEV_API_KEY = process.env.LASTFM_API_KEY || "";
const DEV_API_SECRET = process.env.LASTFM_API_SECRET || "";

if (process.env.NODE_ENV !== "production") {
  if (!DEV_API_KEY || !DEV_API_SECRET) {
    logLastFm(
      "WARNING: Last.fm API credentials not found in environment variables",
      null,
      "warn",
    );
  }
}

const useBackend = process.env.NODE_ENV === "production" || 
                   process.env.USE_BACKEND === "true" ||
                   (!DEV_API_KEY || !DEV_API_SECRET);

// Get the backend URL based on environment
const getBackendUrl = (): string => {
  return process.env.NODE_ENV === "production"
    ? "https://wora-ten.vercel.app"
    : "http://localhost:3000";
};

/**
 * Forward Last.fm requests to the Vercel backend
 */
const forwardToBackend = async (
  endpoint: string,
  method: string = "GET",
  body?: any,
) => {
  try {
    const baseUrl = getBackendUrl();
    const url = `${baseUrl}/api/lastfm/${endpoint}`;

    const options: any = {
      method,
      headers: {
        "Content-Type": "application/json",
      },
    };

    if (body && method === "POST") {
      options.body = JSON.stringify(body);
    }

    const response = await fetch(url, options);
    return await response.json();
  } catch (error) {
    logLastFm("Error forwarding request to backend", error, "error");
    return {
      success: false,
      error: "Failed to communicate with the backend API",
    };
  }
};

/**
 * Generate a signature for Last.fm API
 */
const generateSignature = (params: Record<string, string>): string => {
  // Remove format and callback parameters
  const filteredParams = { ...params };
  delete filteredParams.format;
  delete filteredParams.callback;

  // Sort parameters alphabetically by name
  const sortedKeys = Object.keys(filteredParams).sort();

  // Concatenate parameters
  let signatureStr = "";
  for (const key of sortedKeys) {
    signatureStr += key + filteredParams[key];
  }

  // Append secret
  signatureStr += DEV_API_SECRET;

  // Create MD5 hash
  return crypto.createHash("md5").update(signatureStr).digest("hex");
};

/**
 * Make a direct request to Last.fm API
 */
const makeLastFmRequest = async (
  params: Record<string, string>,
  isAuthRequest: boolean = false,
): Promise<any> => {
  try {
    // Always add these parameters
    const requestParams: Record<string, string> = {
      ...params,
      api_key: DEV_API_KEY,
      format: "json",
    };

    // If this is an authenticated request, add signature
    if (isAuthRequest) {
      requestParams.api_sig = generateSignature(requestParams);
    }

    // Build query string
    const queryString = Object.entries(requestParams)
      .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
      .join("&");

    // Make the request
    const url = `${API_URL}?${queryString}`;

    const response = await fetch(url, {
      method: "POST",
    });

    const data = await response.json();

    // Check for errors
    if (data.error) {
      logLastFm(`API Error ${data.error}: ${data.message}`, null, "error");
      return {
        success: false,
        error: data.message,
        code: data.error,
      };
    }

    return data;
  } catch (error) {
    logLastFm("Error making Last.fm API request", error, "error");
    return {
      success: false,
      error: "Error making Last.fm API request",
    };
  }
};

/**
 * Generate MD5 hash for password authentication
 */
const getMD5Auth = (username: string, password: string): string => {
  const authString = username.toLowerCase() + password;
  return crypto.createHash("md5").update(authString).digest("hex");
};

/**
 * Initialize Last.fm IPC handlers
 */
export const initializeLastFmHandlers = () => {
  // Handler for log messages from renderer process - simplified to avoid duplicate logging
  ipcMain.on("lastfm:log", (_, data) => {
    const { level, message } = data;
    if (!level || !message) return;

    // Just pass to the right logger method
    switch (level) {
      case "error":
        lastFmLogger.error(message);
        break;
      case "warn":
        lastFmLogger.warn(message);
        break;
      default:
        lastFmLogger.info(message);
    }
  });

  // Handle authentication requests
  ipcMain.handle(
    "lastfm:authenticate",
    async (_, username: string, password: string) => {
      try {
        // In production, use the backend API
        if (useBackend) {
          const response = await forwardToBackend("auth", "POST", {
            username,
            password,
          });

          if (!response.success) {
            logLastFm("Authentication error", response.error, "error");
          }
          return response;
        }
        // In development, call Last.fm API directly
        else {
          // Use the mobile session API for desktop auth
          const params = {
            method: "auth.getMobileSession",
            username: username,
            password: password,
          };

          const response = await makeLastFmRequest(params, true);

          if (response.error) {
            return {
              success: false,
              error: response.error,
            };
          }

          // Return success with session
          return {
            success: true,
            session: response.session,
          };
        }
      } catch (error) {
        logLastFm("Error in authentication", error, "error");
        return {
          success: false,
          error: "Internal error during authentication",
        };
      }
    },
  );

  // Handle "now playing" updates
  ipcMain.handle("lastfm:updateNowPlaying", async (_, data) => {
    try {
      const { sessionKey, artist, track, album, duration } = data;

      if (!sessionKey || !artist || !track) {
        return { success: false, error: "Missing required parameters" };
      }

      // Use backend or direct API based on environment
      if (useBackend) {
        const response = await forwardToBackend("now-playing", "POST", data);
        if (!response.success) {
          logLastFm("Error updating now playing", response.error, "error");
        }
        return response;
      } else {
        // Call Last.fm API directly
        const params: Record<string, string> = {
          method: "track.updateNowPlaying",
          artist,
          track,
          sk: sessionKey,
        };

        // Add optional parameters if available
        if (album) params.album = album;
        if (duration) params.duration = duration;

        const response = await makeLastFmRequest(params, true);

        if (response.error) {
          return {
            success: false,
            error: response.message || "Failed to update now playing",
          };
        }

        return {
          success: true,
        };
      }
    } catch (error) {
      logLastFm("Error in updateNowPlaying", error, "error");
      return {
        success: false,
        error: "Internal error updating now playing status",
      };
    }
  });

  // Handle track scrobbling - simplified error handling
  ipcMain.handle("lastfm:scrobbleTrack", async (_, data) => {
    try {
      const { sessionKey, artist, track, album, timestamp, duration } = data;

      if (!sessionKey || !artist || !track) {
        return { success: false, error: "Missing required parameters" };
      }

      // Use backend or direct API based on environment
      if (useBackend) {
        const response = await forwardToBackend("scrobble", "POST", data);
        if (!response.success) {
          logLastFm("Error scrobbling track", response.error, "error");
        }
        return response;
      } else {
        // Call Last.fm API directly
        const params: Record<string, string> = {
          method: "track.scrobble",
          artist,
          track,
          timestamp: timestamp || Math.floor(Date.now() / 1000).toString(),
          sk: sessionKey,
        };

        // Add optional parameters if available
        if (album) params.album = album;
        if (duration) params.duration = duration;

        const response = await makeLastFmRequest(params, true);

        if (response.error) {
          return {
            success: false,
            error: response.message || "Failed to scrobble track",
          };
        }

        return {
          success: true,
        };
      }
    } catch (error) {
      logLastFm("Error in scrobbleTrack", error, "error");
      return { success: false, error: "Internal error scrobbling track" };
    }
  });

  // Handle get user info - simplified
  ipcMain.handle("lastfm:getUserInfo", async (_, username, sessionKey) => {
    try {
      if (!username) {
        return { success: false, error: "Username is required" };
      }

      // Use backend or direct API based on environment
      if (useBackend) {
        const response = await forwardToBackend(
          `user-info?username=${encodeURIComponent(username)}&sessionKey=${encodeURIComponent(sessionKey || "")}`,
        );

        if (!response.success) {
          logLastFm("Error getting user info", response.error, "error");
        }
        return response;
      } else {
        // Call Last.fm API directly
        const params: Record<string, string> = {
          method: "user.getInfo",
          user: username,
        };

        // Add session key if available for private data
        if (sessionKey) params.sk = sessionKey;

        const response = await makeLastFmRequest(params, !!sessionKey);

        if (response.error) {
          return {
            success: false,
            error: response.message || "Failed to get user info",
          };
        }

        return {
          success: true,
          user: response.user,
        };
      }
    } catch (error) {
      logLastFm("Error in getUserInfo", error, "error");
      return { success: false, error: "Internal error getting user info" };
    }
  });

  // Handle get track info - simplified
  ipcMain.handle("lastfm:getTrackInfo", async (_, artist, track, username) => {
    try {
      if (!artist || !track) {
        return { success: false, error: "Artist and track are required" };
      }

      // Use backend or direct API based on environment
      if (useBackend) {
        // Create query string
        let query = `artist=${encodeURIComponent(artist)}&track=${encodeURIComponent(track)}`;
        if (username) {
          query += `&username=${encodeURIComponent(username)}`;
        }

        // Forward track info request to backend
        const response = await forwardToBackend(`track-info?${query}`);
        if (!response.success) {
          logLastFm("Error getting track info", response.error, "error");
        }
        return response;
      } else {
        // Call Last.fm API directly
        const params: Record<string, string> = {
          method: "track.getInfo",
          artist,
          track,
        };

        // Add username if available for loved status
        if (username) params.username = username;

        const response = await makeLastFmRequest(params, false);

        if (response.error) {
          return {
            success: false,
            error: response.message || "Failed to get track info",
          };
        }

        return {
          success: true,
          track: response.track,
        };
      }
    } catch (error) {
      logLastFm("Error in getTrackInfo", error, "error");
      return { success: false, error: "Internal error getting track info" };
    }
  });

  // Handle get artist info
  ipcMain.handle("lastfm:getArtistInfo", async (_, artist) => {
    try {
      if (!artist) {
        return { success: false, error: "Artist name is required" };
      }

      // Use backend or direct API based on environment
      if (useBackend) {
        const query = `artist=${encodeURIComponent(artist)}`;
        const response = await forwardToBackend(`artist-info?${query}`);
        if (!response.success) {
          logLastFm("Error getting artist info", response.error, "error");
        }
        return response;
      } else {
        const params: Record<string, string> = {
          method: "artist.getInfo",
          artist,
          autocorrect: "1",
        };

        const response = await makeLastFmRequest(params, false);

        if (response.error) {
          return {
            success: false,
            error: response.message || "Failed to get artist info",
          };
        }

        return {
          success: true,
          artist: response.artist,
        };
      }
    } catch (error) {
      logLastFm("Error in getArtistInfo", error, "error");
      return { success: false, error: "Internal error getting artist info" };
    }
  });

  // Handle get artist top tracks
  ipcMain.handle("lastfm:getArtistTopTracks", async (_, artist) => {
    try {
      if (!artist) {
        return { success: false, error: "Artist name is required" };
      }

      // Use backend or direct API based on environment
      if (useBackend) {
        const query = `artist=${encodeURIComponent(artist)}`;
        const response = await forwardToBackend(`artist-top-tracks?${query}`);
        if (!response.success) {
          logLastFm("Error getting artist top tracks", response.error, "error");
        }
        return response;
      } else {
        const params: Record<string, string> = {
          method: "artist.getTopTracks",
          artist,
          limit: "10",
          autocorrect: "1",
        };

        const response = await makeLastFmRequest(params, false);

        if (response.error) {
          return {
            success: false,
            error: response.message || "Failed to get top tracks",
          };
        }

        return {
          success: true,
          toptracks: response.toptracks,
        };
      }
    } catch (error) {
      logLastFm("Error in getArtistTopTracks", error, "error");
      return { success: false, error: "Internal error getting top tracks" };
    }
  });

  // Handle get similar artists
  ipcMain.handle("lastfm:getSimilarArtists", async (_, artist) => {
    try {
      if (!artist) {
        return { success: false, error: "Artist name is required" };
      }

      // Use backend or direct API based on environment
      if (useBackend) {
        const query = `artist=${encodeURIComponent(artist)}`;
        const response = await forwardToBackend(`similar-artists?${query}`);
        if (!response.success) {
          logLastFm("Error getting similar artists", response.error, "error");
        }
        return response;
      } else {
        const params: Record<string, string> = {
          method: "artist.getSimilar",
          artist,
          limit: "6",
          autocorrect: "1",
        };

        const response = await makeLastFmRequest(params, false);

        if (response.error) {
          return {
            success: false,
            error: response.message || "Failed to get similar artists",
          };
        }

        return {
          success: true,
          similarartists: response.similarartists,
        };
      }
    } catch (error) {
      logLastFm("Error in getSimilarArtists", error, "error");
      return { success: false, error: "Internal error getting similar artists" };
    }
  });

  // Handle love/unlove track - simplified
  ipcMain.handle("lastfm:loveTrack", async (_, data) => {
    try {
      const { sessionKey, artist, track, love } = data;

      if (!sessionKey || !artist || !track || love === undefined) {
        return { success: false, error: "Missing required parameters" };
      }

      const action = love ? "love" : "unlove";

      // Use backend or direct API based on environment
      if (useBackend) {
        const response = await forwardToBackend("track-action", "POST", {
          sessionKey,
          artist,
          track,
          action,
        });

        if (!response.success) {
          logLastFm(`Error ${action} track`, response.error, "error");
        }
        return response;
      } else {
        // Call Last.fm API directly
        const params: Record<string, string> = {
          method: `track.${action}`,
          artist,
          track,
          sk: sessionKey,
        };

        const response = await makeLastFmRequest(params, true);

        if (response.error) {
          return {
            success: false,
            error: response.message || `Failed to ${action} track`,
          };
        }

        return {
          success: true,
        };
      }
    } catch (error) {
      logLastFm("Error in loveTrack", error, "error");
      return {
        success: false,
        error: `Internal error processing track love/unlove`,
      };
    }
  });

  // Log initialization only in development
  if (process.env.NODE_ENV !== "production") {
    logLastFm(
      `Using ${useBackend ? "backend API" : "direct API calls"} for Last.fm`,
    );
  }
};


================================================
FILE: main/preload.ts
================================================
import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron";

const handler = {
  send(channel: string, value: unknown) {
    ipcRenderer.send(channel, value);
  },
  on(channel: string, callback: (...args: unknown[]) => void) {
    const subscription = (_event: IpcRendererEvent, ...args: unknown[]) =>
      callback(...args);
    ipcRenderer.on(channel, subscription);

    return () => {
      ipcRenderer.removeListener(channel, subscription);
    };
  },
  async invoke(channel: string, ...args: unknown[]) {
    try {
      const result = await ipcRenderer.invoke(channel, ...args);
      return result;
    } catch (error) {
      console.error(`Error invoking channel ${channel}:`, error);
      throw error;
    }
  },
};

contextBridge.exposeInMainWorld("ipc", handler);

export type IpcHandler = typeof handler;


================================================
FILE: package.json
================================================
{
  "private": true,
  "name": "wora",
  "description": "🎧 A beautiful player for audiophiles.",
  "version": "0.4.0-beta2",
  "author": {
    "name": "Aaryan Kapoor",
    "email": "hi.aaryankapoor@gmail.com"
  },
  "main": "app/background.js",
  "scripts": {
    "dev": "nextron",
    "build": "nextron build",
    "postinstall": "electron-builder install-app-deps",
    "build:mac": "nextron build --mac --universal",
    "build:linux": "nextron build --linux",
    "build:win64": "nextron build --win --x64"
  },
  "dependencies": {
    "@hookform/resolvers": "^5.1.1",
    "@radix-ui/react-avatar": "^1.1.10",
    "@radix-ui/react-context-menu": "^2.2.15",
    "@radix-ui/react-dialog": "^1.1.14",
    "@radix-ui/react-label": "^2.1.7",
    "@radix-ui/react-progress": "^1.1.7",
    "@radix-ui/react-scroll-area": "^1.2.9",
    "@radix-ui/react-select": "^2.2.5",
    "@radix-ui/react-slider": "^1.3.5",
    "@radix-ui/react-slot": "^1.2.3",
    "@radix-ui/react-switch": "^1.2.5",
    "@radix-ui/react-tabs": "^1.1.12",
    "@radix-ui/react-tooltip": "^1.2.7",
    "@tabler/icons-react": "^3.34.0",
    "@tailwindcss/postcss": "^4.1.10",
    "@types/better-sqlite3": "^7.6.13",
    "@types/crypto-js": "^4.2.2",
    "@xhayper/discord-rpc": "^1.2.2",
    "axios": "^1.10.0",
    "better-sqlite3": "^12.0.0",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "cmdk": "^1.1.1",
    "drizzle-orm": "^0.44.2",
    "electron-log": "^5.4.1",
    "electron-serve": "^1.3.0",
    "electron-store": "^8.2.0",
    "embla-carousel-react": "^8.6.0",
    "eslint-config-next": "^15.3.4",
    "framer-motion": "^12.23.12",
    "howler": "^2.2.4",
    "last-fm": "^5.3.0",
    "music-metadata": "^7.14.0",
    "next-themes": "^0.4.6",
    "node-fetch": "2",
    "react-hook-form": "^7.58.1",
    "react-virtualized-auto-sizer": "^1.0.26",
    "react-window": "^1.8.11",
    "seamless-scroll-polyfill": "^2.3.4",
    "sonner": "^2.0.5",
    "tailwind-merge": "^3.3.1",
    "tailwindcss-animate": "^1.0.7",
    "zod": "^3.25.67"
  },
  "devDependencies": {
    "@electron/rebuild": "^4.0.1",
    "@types/howler": "^2.2.12",
    "@types/node": "^24.0.3",
    "@types/react": "^19.1.8",
    "autoprefixer": "^10.4.21",
    "drizzle-kit": "^0.31.2",
    "electron": "^32.2.7",
    "electron-builder": "^24.13.3",
    "next": "^15.3.4",
    "nextron": "^9.5.0",
    "postcss": "^8.4.49",
    "prettier": "3.6.0",
    "prettier-plugin-tailwindcss": "^0.6.13",
    "react": "^19.1.0",
    "react-dom": "^19.1.0",
    "rebuild": "^0.1.2",
    "tailwindcss": "^4.1.10",
    "typescript": "^5.8.3"
  },
  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}


================================================
FILE: renderer/components/ErrorBoundary.tsx
================================================
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { Button } from '@/components/ui/button';
import { IconAlertTriangle, IconRefresh } from '@tabler/icons-react';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

export default class ErrorBoundary extends Component<Props, State> {
  public state: State = {
    hasError: false,
    error: null,
  };

  public static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('Uncaught error:', error, errorInfo);
  }

  private handleReset = () => {
    this.setState({ hasError: false, error: null });
    window.location.reload();
  };

  public render() {
    if (this.state.hasError) {
      if (this.props.fallback) {
        return this.props.fallback;
      }

      return (
        <div className="flex h-full w-full flex-col items-center justify-center p-8">
          <div className="max-w-md text-center">
            <IconAlertTriangle size={48} className="mx-auto mb-4 text-red-500" />
            <h2 className="mb-2 text-xl font-semibold">Something went wrong</h2>
            <p className="mb-4 text-sm opacity-70">
              An unexpected error occurred. The application may not work correctly.
            </p>
            {process.env.NODE_ENV !== 'production' && this.state.error && (
              <details className="mb-4 rounded-lg bg-gray-100 p-3 text-left dark:bg-gray-800">
                <summary className="cursor-pointer text-xs font-medium">Error details</summary>
                <pre className="mt-2 overflow-auto text-xs">
                  {this.state.error.toString()}
                  {this.state.error.stack}
                </pre>
              </details>
            )}
            <Button
              onClick={this.handleReset}
              className="flex items-center gap-2"
            >
              <IconRefresh size={16} />
              Reload Application
            </Button>
          </div>
        </div>
      );
    }

    return this.props.children;
  }
}

================================================
FILE: renderer/components/LoadingSkeletons.tsx
================================================
import React from 'react';
import { Skeleton } from '@/components/ui/skeleton';

export function ArtistGridSkeleton({ count = 12, viewMode = 'grid-large' }: { count?: number; viewMode?: string }) {
  const isLarge = viewMode === 'grid-large';
  const gridClass = isLarge 
    ? "grid grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6"
    : viewMode === 'grid-small'
    ? "grid grid-cols-4 gap-3 sm:grid-cols-5 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10"
    : "space-y-1";

  if (viewMode === 'list') {
    return (
      <div className={gridClass}>
        {Array.from({ length: count }).map((_, i) => (
          <div key={i} className="flex items-center gap-4 p-3">
            <Skeleton className="h-12 w-12 rounded-lg" />
            <div className="flex-1">
              <Skeleton className="h-4 w-32 mb-2" />
              <Skeleton className="h-3 w-24" />
            </div>
          </div>
        ))}
      </div>
    );
  }

  return (
    <div className={gridClass}>
      {Array.from({ length: count }).map((_, i) => (
        <div key={i}>
          <Skeleton className={`aspect-square ${isLarge ? 'rounded-xl' : 'rounded-lg'}`} />
          <Skeleton className={`h-4 w-3/4 ${isLarge ? 'mt-3' : 'mt-2'} mb-1`} />
          <Skeleton className="h-3 w-1/2" />
        </div>
      ))}
    </div>
  );
}

export function AlbumGridSkeleton({ count = 12 }: { count?: number }) {
  return (
    <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
      {Array.from({ length: count }).map((_, i) => (
        <div key={i}>
          <Skeleton className="aspect-square rounded-lg" />
          <Skeleton className="h-4 w-3/4 mt-2 mb-1" />
          <Skeleton className="h-3 w-1/2" />
        </div>
      ))}
    </div>
  );
}

export function SongListSkeleton({ count = 10 }: { count?: number }) {
  return (
    <div className="space-y-1">
      {Array.from({ length: count }).map((_, i) => (
        <div key={i} className="flex items-center gap-4 p-3">
          <Skeleton className="h-12 w-12 rounded" />
          <div className="flex-1">
            <Skeleton className="h-4 w-48 mb-2" />
            <Skeleton className="h-3 w-32" />
          </div>
          <Skeleton className="h-4 w-12" />
        </div>
      ))}
    </div>
  );
}

export function ArtistDetailSkeleton() {
  return (
    <div>
      <div className="relative h-96 w-full overflow-hidden rounded-2xl">
        <Skeleton className="h-full w-full" />
        <div className="absolute bottom-6 left-6">
          <div className="flex items-end gap-6">
            <Skeleton className="h-52 w-52 rounded-xl" />
            <div>
              <Skeleton className="h-12 w-64 mb-2" />
              <Skeleton className="h-4 w-48" />
            </div>
          </div>
        </div>
      </div>
      <div className="mt-8 space-y-4">
        <Skeleton className="h-32 w-full rounded-lg" />
        <div className="grid grid-cols-2 gap-4 md:grid-cols-4">
          <Skeleton className="h-24 rounded-lg" />
          <Skeleton className="h-24 rounded-lg" />
          <Skeleton className="h-24 rounded-lg" />
          <Skeleton className="h-24 rounded-lg" />
        </div>
      </div>
    </div>
  );
}

================================================
FILE: renderer/components/PageTransition.tsx
================================================
import { motion, AnimatePresence, Transition } from 'framer-motion';
import { useRouter } from 'next/router';
import { ReactNode } from 'react';

interface PageTransitionProps {
  children: ReactNode;
}

// Option 1: Cross-fade (no wait, instant transition)
const pageVariants = {
  initial: {
    opacity: 0,
  },
  in: {
    opacity: 1,
  },
  out: {
    opacity: 0,
  },
};

const pageTransition: Transition = {
  type: 'tween',
  ease: 'easeOut',
  duration: 0.1, // Very fast
};

export default function PageTransition({ children }: PageTransitionProps) {
  const router = useRouter();

  return (
    <AnimatePresence mode="sync" initial={false}>  {/* sync = crossfade, wait = sequential */}
      <motion.div
        key={router.asPath}
        initial="initial"
        animate="in"
        exit="out"
        variants={pageVariants}
        transition={pageTransition}
        style={{ height: '100%' }}
      >
        {children}
      </motion.div>
    </AnimatePresence>
  );
}

================================================
FILE: renderer/components/PageTransitionMinimal.tsx
================================================
import { ReactNode } from 'react';

interface PageTransitionProps {
  children: ReactNode;
}

// Minimal approach: No transition, just instant page swap
// This is actually what many modern apps do (Spotify, Apple Music)
// The smooth scroll restoration gives enough visual continuity
export default function PageTransitionMinimal({ children }: PageTransitionProps) {
  return <>{children}</>;
}

================================================
FILE: renderer/components/main/lyrics.tsx
================================================
import { LyricLine } from "@/lib/helpers";
import React, { useEffect, useRef } from "react";
import { Badge } from "../ui/badge";
import { scrollIntoView } from "seamless-scroll-polyfill";
import { cn } from "@/lib/utils";

interface LyricsProps {
  lyrics: LyricLine[];
  currentLyric: LyricLine | null;
  onLyricClick: (time: number) => void;
  isSyncedLyrics: boolean;
}

const Lyrics: React.FC<LyricsProps> = React.memo(
  ({ lyrics, currentLyric, onLyricClick, isSyncedLyrics }) => {
    const lyricsRef = useRef<HTMLDivElement>(null);

    useEffect(() => {
      if (currentLyric && lyricsRef.current) {
        const currentLine = document.getElementById(
          `line-${currentLyric.time}`,
        );
        if (currentLine) {
          scrollIntoView(
            currentLine,
            {
              behavior: "smooth",
              block: "center",
            },
            {
              duration: 500,
            },
          );
        }
      }
    }, [currentLyric]);

    return (
      <div className="wora-border relative h-full w-full rounded-2xl bg-white/70 backdrop-blur-xl dark:bg-black/70">
        <div className="absolute right-6 bottom-5 z-50 flex items-center gap-2">
          <Badge>{isSyncedLyrics ? "Synced" : "Unsynced"}</Badge>
        </div>

        <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">
          <div
            ref={lyricsRef}
            className="no-scrollbar h-full w-full py-[33vh]"
            style={{ overflowY: "auto" }}
          >
            {lyrics.map((line) => (
              <p
                key={line.time}
                id={`line-${line.time}`}
                className={cn(
                  currentLyric?.time === line.time
                    ? "scale-125 font-semibold"
                    : "opacity-40",
                  "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",
                )}
                onClick={() => onLyricClick(line.time)}
              >
                {line.text}
              </p>
            ))}
          </div>
        </div>
      </div>
    );
  },
);

export default Lyrics;


================================================
FILE: renderer/components/main/navbar.tsx
================================================
import {
  IconDeviceDesktop,
  IconFocusCentered,
  IconInbox,
  IconList,
  IconMoon,
  IconSearch,
  IconSun,
  IconVinyl,
  IconUser,
  IconArrowLeft,
} from "@tabler/icons-react";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
  Command,
  CommandDialog,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command";
import { useEffect, useState, useCallback } from "react";
import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/router";
import { usePlayer } from "@/context/playerContext";
import Spinner from "@/components/ui/spinner";
import { useTheme } from "next-themes";

type Settings = {
  name: string;
  profilePicture: string;
};

type NavLink = {
  href: string;
  icon: React.ReactNode;
  label: string;
};

const Navbar = () => {
  const router = useRouter();
  const [open, setOpen] = useState(false);
  const [searchResults, setSearchResults] = useState([]);
  const [settings, setSettings] = useState<Settings | null>(null);
  const [search, setSearch] = useState("");
  const [loading, setLoading] = useState(false);
  const { setQueueAndPlay } = usePlayer();
  const { theme, setTheme } = useTheme();
  const [mounted, setMounted] = useState(false);
  const [canGoBack, setCanGoBack] = useState(false);
  const [isBackButtonVisible, setIsBackButtonVisible] = useState(false);

  useEffect(() => {
    setMounted(true);
    const checkBackButton = () => {
      const path = router.pathname;
      const isDetailPage = path.includes('/artists/[') || path.includes('/albums/[') || path.includes('/playlists/[');
      const shouldShow = isDetailPage && window.history.length > 1;
      
      if (shouldShow !== canGoBack) {
        setCanGoBack(shouldShow);
        if (shouldShow) {
          setTimeout(() => setIsBackButtonVisible(true), 50);
        } else {
          setIsBackButtonVisible(false);
        }
      }
    };
    checkBackButton();
    router.events.on('routeChangeComplete', checkBackButton);
    return () => {
      router.events.off('routeChangeComplete', checkBackButton);
    };
  }, [router, canGoBack]);

  const navLinks: NavLink[] = [
    {
      href: "/home",
      icon: <IconInbox stroke={2} className="w-5" />,
      label: "Home",
    },
    {
      href: "/playlists",
      icon: <IconVinyl stroke={2} size={20} />,
      label: "Playlists",
    },
    {
      href: "/songs",
      icon: <IconList stroke={2} size={20} />,
      label: "Songs",
    },
    {
      href: "/albums",
      icon: <IconFocusCentered stroke={2} size={20} />,
      label: "Albums",
    },
    {
      href: "/artists",
      icon: <IconUser stroke={2} size={20} />,
      label: "Artists",
    },
  ];

  const handleThemeToggle = () => {
    if (theme === "light") {
      setTheme("dark");
    } else if (theme === "dark") {
      setTheme("system");
    } else {
      setTheme("light");
    }
  };

  const renderIcon = () => {
    if (!mounted) {
      return <IconDeviceDesktop stroke={2} className="w-5" />;
    }
    if (theme === "light") {
      return <IconSun stroke={2} className="w-5" />;
    } else if (theme === "dark") {
      return <IconMoon stroke={2} className="w-5" />;
    } else {
      return <IconDeviceDesktop stroke={2} className="w-5" />;
    }
  };

  const isActive = (href: string): boolean => {
    if (href === "/home" && router.pathname === "/") {
      return true;
    }

    return (
      router.pathname === href ||
      (href !== "/home" && router.pathname.startsWith(href))
    );
  };

  const handleNavigation = useCallback(
    (href: string, e: React.MouseEvent) => {
      if (isActive(href)) {
        e.preventDefault();

        if (router.pathname === href) {
          const viewport = document.querySelector('[data-radix-scroll-area-viewport]');
          if (viewport) {
            (viewport as HTMLElement).scrollTop = 0;
          }
          
          if (href === "/albums") {
            window.ipc.send("resetAlbumsPageState", null);
          } else if (href === "/songs") {
            window.ipc.send("resetSongsPageState", null);
          } else if (href === "/playlists") {
            window.ipc.send("resetPlaylistsPageState", null);
          } else if (href === "/home") {
            window.ipc.send("resetHomePageState", null);
          } else if (href === "/artists") {
            window.ipc.send("resetArtistsPageState", null);
          }
        } else {
          // If navigating to a different page, just push the route
          router.push(href);
        }
      }
    },
    [router],
  );

  useEffect(() => {
    const down = (e) => {
      if (e.key === "f" && (e.metaKey || e.ctrlKey)) {
        e.preventDefault();
        setOpen((open) => !open);
      }
    };

    document.addEventListener("keydown", down);
    return () => document.removeEventListener("keydown", down);
  }, []);

  useEffect(() => {
    setLoading(true);

    if (!search) {
      setSearchResults([]);
      setLoading(false);
      return;
    }

    const delayDebounceFn = setTimeout(() => {
      window.ipc.invoke("search", search).then((response) => {
        const albums = response.searchAlbums;
        const playlists = response.searchPlaylists;
        const songs = response.searchSongs;
        const artists = response.searchArtists || [];

        setSearchResults([
          ...artists.map((artist: any) => ({ ...artist, type: "Artist" })),
          ...playlists.map((playlist: any) => ({
            ...playlist,
            type: "Playlist",
          })),
          ...albums.map((album: any) => ({ ...album, type: "Album" })),
          ...songs.map((song: any) => ({ ...song, type: "Song" })),
        ]);

        setLoading(false);
      });
    }, 1000);

    return () => clearTimeout(delayDebounceFn);
  }, [search]);

  const openSearch = () => setOpen(true);

  const handleItemClick = (item: any) => {
    if (item.type === "Album") {
      router.push(`/albums/${item.id}`);
    } else if (item.type === "Song") {
      setQueueAndPlay([item], 0);
    } else if (item.type === "Playlist") {
      router.push(`/playlists/${item.id}`);
    } else if (item.type === "Artist") {
      router.push(`/artists/${encodeURIComponent(item.name)}`);
    }
    setOpen(false);
  };

  useEffect(() => {
    window.ipc.invoke("getSettings").then((response) => {
      setSettings(response);
    });

    window.ipc.on("confirmSettingsUpdate", () => {
      window.ipc.invoke("getSettings").then((response) => {
        setSettings(response);
      });
    });
  }, []);

  return (
    <>
      <div className="flex h-full flex-col items-center justify-center gap-10">
        <TooltipProvider>
          <Tooltip delayDuration={0}>
            <TooltipTrigger>
              <Link href="/settings">
                <Avatar className="h-8 w-8">
                  <AvatarImage
                    src={`${settings && settings.profilePicture ? "wora://" + settings.profilePicture : "/userPicture.png"}`}
                  />
                </Avatar>
              </Link>
            </TooltipTrigger>
            <TooltipContent side="right" sideOffset={25}>
              <p>{settings && settings.name ? settings.name : "Wora User"}</p>
            </TooltipContent>
          </Tooltip>
          <div className="wora-border flex w-18 flex-col items-center gap-10 rounded-2xl p-8 transition-all duration-300 ease-in-out">
            <div
              className={`transition-all duration-300 ease-in-out ${
                canGoBack ? 'max-h-12 opacity-100' : 'max-h-0 opacity-0 -mt-10 overflow-hidden'
              }`}
            >
              {(canGoBack || isBackButtonVisible) && (
                <Tooltip delayDuration={0}>
                  <TooltipTrigger asChild>
                    <Button
                      variant="ghost"
                      onClick={() => router.back()}
                      className={`transition-all duration-300 ${
                        isBackButtonVisible ? 'scale-100 opacity-100' : 'scale-95 opacity-0'
                      }`}
                    >
                      <IconArrowLeft stroke={2} className="w-5" />
                    </Button>
                  </TooltipTrigger>
                  <TooltipContent side="right" sideOffset={50}>
                    <p>Back</p>
                  </TooltipContent>
                </Tooltip>
              )}
            </div>
            
            {navLinks.map((link) => (
              <Tooltip key={link.href} delayDuration={0}>
                <TooltipTrigger asChild>
                  <Button
                    variant="ghost"
                    className={isActive(link.href) && "opacity-100"}
                  >
                    <Link
                      href={link.href}
                      onClick={(e) => handleNavigation(link.href, e)}
                      className="flex h-full w-full items-center justify-center"
                    >
                      {link.icon}
                    </Link>
                  </Button>
                </TooltipTrigger>
                <TooltipContent side="right" sideOffset={50}>
                  <p>{link.label}</p>
                </TooltipContent>
              </Tooltip>
            ))}

            <Tooltip delayDuration={0}>
              <TooltipTrigger asChild>
                <Button variant="ghost" onClick={openSearch}>
                  <IconSearch stroke={2} className="w-5" />
                </Button>
              </TooltipTrigger>
              <TooltipContent side="right" sideOffset={50}>
                <p>Search</p>
              </TooltipContent>
            </Tooltip>
          </div>
          <Tooltip delayDuration={0}>
            <TooltipTrigger asChild>
              <Button variant="ghost" onClick={handleThemeToggle}>
                {renderIcon()}
              </Button>
            </TooltipTrigger>
            <TooltipContent side="right" sideOffset={25}>
              <p className="capitalize">Theme: {mounted ? theme : 'system'}</p>
            </TooltipContent>
          </Tooltip>
        </TooltipProvider>
      </div>

      <CommandDialog open={open} onOpenChange={setOpen}>
        <Command>
          <CommandInput
            placeholder="Search for a song, album or playlist..."
            value={search}
            onValueChange={setSearch}
          />
          <CommandList>
            {loading && (
              <div className="flex h-[325px] w-full items-center justify-center">
                <Spinner className="h-6 w-6" />
              </div>
            )}
            {search && !loading ? (
              <CommandGroup heading="Search Results" className="pb-2">
                {searchResults.map((item) => (
                  <CommandItem
                    key={`${item.type}-${item.id || item.name}`}
                    value={`${item.name}-${item.type}-${item.id || ""}`}
                    onSelect={() => handleItemClick(item)}
                    className="text-black dark:text-white"
                  >
                    <div className="flex h-full w-full items-center gap-2.5 mask-r-from-70%">
                      {(item.type === "Playlist" || item.type === "Album") && (
                        <div className="relative h-12 w-12 overflow-hidden rounded-lg shadow-xl transition duration-300">
                          <Image
                            className="object-cover"
                            src={`wora://${item.cover}`}
                            alt={item.name}
                            fill
                          />
                        </div>
                      )}
                      {item.type === "Artist" && (
                        <div className="dark:bg.white/10 flex h-12 w-12 items-center justify-center rounded-lg bg-black/10">
                          <IconUser stroke={1.5} size={24} />
                        </div>
                      )}
                      <div>
                        <p className="w-full overflow-hidden text-xs text-nowrap">
                          {item.name}
                          <span className="ml-1 opacity-50">({item.type})</span>
                        </p>
                        <p className="w-full text-xs opacity-50">
                          {item.type === "Playlist"
                            ? item.description
                            : item.type === "Artist"
                              ? "Artist"
                              : item.artist}
                        </p>
                      </div>
                    </div>
                  </CommandItem>
                ))}
              </CommandGroup>
            ) : (
              <div className="flex h-[325px] w-full items-center justify-center text-xs">
                <div className="dark:bg.white/10 ml-2 rounded-lg bg-black/5 px-1.5 py-1 shadow-xs">
                  ⌘ / Ctrl + F
                </div>
              </div>
            )}
          </CommandList>
        </Command>
      </CommandDialog>
    </>
  );
};

export default Navbar;


================================================
FILE: renderer/components/main/player.tsx
================================================
import Image from "next/image";
import { Button } from "@/components/ui/button";
import {
  IconArrowsShuffle2,
  IconBrandLastfm,
  IconCheck,
  IconClock,
  IconHeart,
  IconInfoCircle,
  IconList,
  IconListTree,
  IconMessage,
  IconPlayerPause,
  IconPlayerPlay,
  IconPlayerSkipBack,
  IconPlayerSkipForward,
  IconPlus,
  IconRepeat,
  IconRipple,
  IconVinyl,
  IconVolume,
  IconVolumeOff,
  IconX,
} from "@tabler/icons-react";
import React, { memo, useCallback, useEffect, useRef, useState } from "react";
import { Howl } from "howler";
import { FixedSizeList as List } from "react-window";
import { Slider } from "@/components/ui/slider";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
import Lyrics from "@/components/main/lyrics";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";
import {
  convertTime,
  isSyncedLyrics,
  parseLyrics,
  updateDiscordState,
  useAudioMetadata,
} from "@/lib/helpers";
import { Song, usePlayer } from "@/context/playerContext";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import Link from "next/link";
import { toast } from "sonner";
import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSub,
  ContextMenuSubContent,
  ContextMenuSubTrigger,
  ContextMenuTrigger,
} from "@/components/ui/context-menu";
import {
  initializeLastFMWithSession,
  scrobbleTrack,
  updateNowPlaying,
  isAuthenticated,
} from "@/lib/lastfm";
import AutoSizer from "react-virtualized-auto-sizer";
import ErrorBoundary from "@/components/ErrorBoundary";

const NotificationToast = ({ success, message }: { success: boolean; message: string }) => (
  <div className="flex w-fit items-center gap-2 text-xs">
    {success ? (
      <IconCheck className="text-green-400" stroke={2} size={16} />
    ) : (
      <IconX className="text-red-500" stroke={2} size={16} />
    )}
    {message}
  </div>
);

function getAlbumCoverUrl(song: Song | undefined): string {
  const cover = song?.album?.cover;
  if (!cover) return "/coverArt.png";
  if (cover.includes("://")) return cover;
  return `wora://${cover}`;
}

const QueuePanel = memo(({ queue, history, currentIndex, onSongSelect }: {
  queue: Song[];
  history: Song[];
  currentIndex: number;
  onSongSelect: (song: Song) => void;
}) => {
  const ITEM_HEIGHT = 80;

  const VirtualizedSongListItem = ({ index, style, data }: {
    index: number;
    style: React.CSSProperties;
    data: { songs: Song[]; onSongSelect: (song: Song) => void }
  }) => {
    const song = data.songs[index];

    return (
      <div style={style}>
        <li
          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"
          onClick={() => data.onSongSelect(song)}
        >
          <div className="relative min-h-14 min-w-14 overflow-hidden rounded-lg shadow-lg">
            <Image
              alt={song.name || "Track"}
              src={getAlbumCoverUrl(song)}
              fill
              priority={false}
              className="object-cover"
            />
          </div>
          <div className="w-4/5 overflow-hidden">
            <p className="truncate text-sm font-medium">{song.name}</p>
            <p className="truncate opacity-50">{song.artist}</p>
          </div>
        </li>
      </div>
    );
  };

  const queueSongs = queue.slice(currentIndex + 1);
  const historySongs = [...history].reverse();

  return (
    <div className="wora-border relative h-full w-full rounded-2xl bg-white/70 backdrop-blur-xl dark:bg-black/70 pointer-events-auto">
      <div className="h-utility w-full max-w-3xl px-6 pt-6 pointer-events-auto">
        <Tabs
          defaultValue="queue"
          className="flex h-full w-full flex-col gap-4 mask-b-from-70% pointer-events-auto"
        >
          <TabsList className="w-full pointer-events-auto">
            <TabsTrigger value="queue" className="w-full gap-2 cursor-pointer pointer-events-auto">
              <IconListTree stroke={2} size={15} /> Queue
            </TabsTrigger>
            <TabsTrigger value="history" className="w-full gap-2 cursor-pointer pointer-events-auto">
              <IconClock stroke={2} size={15} /> History
            </TabsTrigger>
          </TabsList>

          <TabsContent
            value="queue"
            className="flex-1 min-h-0 pointer-events-auto"
          >
            {queueSongs.length > 0 ? (
              <ErrorBoundary>
                <AutoSizer>
                  {({ height, width }) => (
                    <List
                      height={height}
                      width={width}
                      itemCount={queueSongs.length}
                      itemSize={ITEM_HEIGHT}
                      itemData={{ songs: queueSongs, onSongSelect }}
                      className="no-scrollbar pointer-events-auto"
                    >
                      {VirtualizedSongListItem}
                    </List>
                  )}
                </AutoSizer>
              </ErrorBoundary>
            ) : (
              <div className="flex h-40 items-center justify-center text-sm opacity-50 pointer-events-none">
                Queue is empty
              </div>
            )}
          </TabsContent>

          <TabsContent
            value="history"
            className="flex-1 min-h-0 pointer-events-auto"
          >
            {historySongs.length > 0 ? (
              <ErrorBoundary>
                <AutoSizer>
                  {({ height, width }) => (
                    <List
                      height={height}
                      width={width}
                      itemCount={historySongs.length}
                      overscanCount={5}
                      itemSize={ITEM_HEIGHT}
                      itemData={{ songs: historySongs, onSongSelect }}
                      className="no-scrollbar pointer-events-auto"
                    >
                      {VirtualizedSongListItem}
                    </List>
                  )}
                </AutoSizer>
              </ErrorBoundary>
            ) : (
              <div className="flex h-40 items-center justify-center text-sm opacity-50 pointer-events-none">
                No playback history
              </div>
            )}
          </TabsContent>
        </Tabs>
      </div>
    </div>
  );
});

export const Player = () => {
  // Player state
  const [seekPosition, setSeekPosition] = useState(0);
  const [volume, setVolume] = useState(0.5);
  const [previousVolume, setPreviousVolume] = useState(0.5);
  const [isMuted, setIsMuted] = useState(false);
  const [currentLyric, setCurrentLyric] = useState(null);
  const [showLyrics, setShowLyrics] = useState(false);
  const [showQueue, setShowQueue] = useState(false);
  const [isFavourite, setIsFavourite] = useState(false);
  const [playlists, setPlaylists] = useState([]);
  const [isClient, setIsClient] = useState(false);
  const [lastFmSettings, setLastFmSettings] = useState({
    lastFmUsername: null,
    lastFmSessionKey: null,
    enableLastFm: false,
    scrobbleThreshold: 50,
  });
  const [lastFmStatus, setLastFmStatus] = useState({
    isScrobbled: false,
    isNowPlaying: false,
    scrobbleTimerStarted: false,
    error: null,
    lastFmActive: false,
  });
  const scrobbleTimeout = useRef<NodeJS.Timeout | null>(null);

  // References
  const soundRef = useRef<Howl | null>(null);
  const seekUpdateInterval = useRef<NodeJS.Timeout | null>(null);
  const volumeSliderRef = useRef<HTMLDivElement | null>(null);

  // Get player context and song metadata
  const {
    song,
    nextSong,
    previousSong,
    queue,
    history,
    currentIndex,
    repeat,
    shuffle,
    toggleShuffle,
    toggleRepeat,
    jumpToSong,
    isPlaying,
    setIsPlaying,
  } = usePlayer();

  const { metadata, lyrics, favourite } = useAudioMetadata(song?.filePath);

  // Load Last.fm settings
  useEffect(() => {
    const loadLastFmSettings = async () => {
      try {
        const settings = await window.ipc.invoke("getLastFmSettings");
        setLastFmSettings(settings);

        // Initialize Last.fm with session key if available
        if (settings.lastFmSessionKey && settings.enableLastFm) {
          initializeLastFMWithSession(
            settings.lastFmSessionKey,
            settings.lastFmUsername || "",
          );
          setLastFmStatus((prev) => ({ ...prev, lastFmActive: true }));
          console.log("[Last.fm] Initialized with session key");
        } else {
          // Clear Last.fm status if disabled or no session
          setLastFmStatus((prev) => ({
            ...prev,
            lastFmActive: false,
            isScrobbled: false,
            isNowPlaying: false,
          }));
          console.log("[Last.fm] Disabled or no session key");
        }
      } catch (error) {
        console.error("[Last.fm] Error loading settings:", error);
      }
    };

    // Load settings initially
    loadLastFmSettings();

    // Set up listener for Last.fm settings changes
    const removeListener = window.ipc.on(
      "lastFmSettingsChanged",
      loadLastFmSettings,
    );

    return () => {
      removeListener();
    };
  }, []);

  // Reset scrobble status when song changes
  useEffect(() => {
    setLastFmStatus({
      isScrobbled: false,
      isNowPlaying: false,
      scrobbleTimerStarted: false,
      error: null,
      lastFmActive: lastFmStatus.lastFmActive,
    });

    if (scrobbleTimeout.current) {
      clearInterval(scrobbleTimeout.current);
      scrobbleTimeout.current = null;
    }
  }, [song]);

  // Last.fm scrobble handler
  const handleScrobble = useCallback(() => {
    if (
      !song ||
      !lastFmSettings.enableLastFm ||
      lastFmStatus.isScrobbled ||
      !isAuthenticated()
    ) {
      // Skip scrobble checks without verbose logging
      return;
    }

    // Clear existing timer if any
    if (scrobbleTimeout.current) {
      clearInterval(scrobbleTimeout.current);
      scrobbleTimeout.current = null;
    }

    const scrobbleIfThresholdReached = () => {
      if (!soundRef.current || lastFmStatus.isScrobbled) return;

      const duration = soundRef.current.duration();
      const currentPosition = soundRef.current.seek();
      const playedPercentage = (currentPosition / duration) * 100;

      // Only log in development
      if (process.env.NODE_ENV !== "production") {
        console.log(
          `[Last.fm] Position: ${playedPercentage.toFixed(1)}%, threshold: ${lastFmSettings.scrobbleThreshold}%`,
        );
      }

      if (playedPercentage >= lastFmSettings.scrobbleThreshold) {
        // Clear the interval immediately to prevent multiple scrobbles
        if (scrobbleTimeout.current) {
          clearInterval(scrobbleTimeout.current);
          scrobbleTimeout.current = null;
        }

        // Set scrobbled status immediately to prevent race conditions
        setLastFmStatus((prev) => ({ ...prev, isScrobbled: true }));

        // Minimal logging for production, log to file only for important events
        try {
          window.ipc.send("lastfm:log", {
            level: "info",
            message: `Scrobbling track: ${song.artist} - ${song.name} (${playedPercentage.toFixed(1)}%)`,
          });
        } catch (err) {
          // Silent error in production
        }

        // Scrobble the track
        scrobbleTrack(song)
          .then((success) => {
            if (!success) {
              setLastFmStatus((prev) => ({
                ...prev,
                error: "Failed to scrobble track",
                isScrobbled: false, // Reset scrobbled state to allow retrying
              }));
            }
          })
          .catch((err) => {
            // Log only the error message, not the entire error object
            try {
              window.ipc.send("lastfm:log", {
                level: "error",
                message: `Scrobble error: ${err?.message || "Unknown error"}`,
              });
            } catch (logErr) {
              // Silent fail in production
            }

            setLastFmStatus((prev) => ({
              ...prev,
              error: "Error scrobbling track",
              isScrobbled: false, // Reset scrobbled state to allow retrying
            }));
          });
      }
    };

    // Set timer to check scrobble threshold
    const checkInterval = 2000; // Check every 2 seconds
    scrobbleTimeout.current = setInterval(
      scrobbleIfThresholdReached,
      checkInterval,
    );

    return () => {
      if (scrobbleTimeout.current) {
        clearInterval(scrobbleTimeout.current);
        scrobbleTimeout.current = null;
      }
    };
  }, [song, lastFmSettings, lastFmStatus.isScrobbled]);

  // Player control functions - Define handlePlayPause earlier to avoid reference error
  const handlePlayPause = useCallback(() => {
    if (!soundRef.current) return;

    if (soundRef.current.playing()) {
      soundRef.current.pause();
    } else {
      soundRef.current.play();
    }
  }, []);

  const handleSeek = useCallback((value: number[]) => {
    if (!soundRef.current) return;

    soundRef.current.seek(value[0]);
    setSeekPosition(value[0]);
  }, []);

  const handleVolume = useCallback((value: number[]) => {
    // Store previous volume before muting (only if not currently muted)
    if (!isMuted && value[0] > 0.01) {
      setPreviousVolume(value[0]);
    }

    setIsMuted(value[0] === 0);
    setVolume(value[0]);
  }, [isMuted]);

  const toggleMute = useCallback(() => {
    if (!isMuted) {
      // Store current volume before muting
      if (volume > 0.01) {
        setPreviousVolume(volume);
      }

      setVolume(0);
      setIsMuted(true);

      // Directly apply mute to audio
      if (soundRef.current) {
        soundRef.current.mute(true);
      }
    } else {
      // Restore previous volume or default to 50%
      const restoreVolume = previousVolume > 0.05 ? previousVolume : 0.5;
      setVolume(restoreVolume);
      setPreviousVolume(restoreVolume); // Update previousVolume to the restored value
      setIsMuted(false);

      // Directly apply volume and unmute to audio to avoid desync
      if (soundRef.current) {
        soundRef.current.volume(restoreVolume);
        soundRef.current.mute(false);
      }
    }
  }, [isMuted, volume, previousVolume]);

  const handleVolumeWheel = useCallback((event: WheelEvent) => {
    event.preventDefault();
    const delta = event.deltaY > 0 ? -0.05 : 0.05; // Scroll down decreases, scroll up increases
    const newVolume = Math.max(0, Math.min(1, Math.round((volume + delta) * 100) / 100));
    console.log(`Volume changed: ${newVolume}`);
    handleVolume([newVolume]);
  }, [volume, handleVolume]);

  const toggleFavourite = useCallback((id: number) => {
    if (!id) return;

    window.ipc.send("addToFavourites", id);
    setIsFavourite((prev) => !prev);
  }, []);

  const handleKeyDown = useCallback((event: KeyboardEvent) => {
    // Only handle keyboard shortcuts if we're not focused on an input element
    if (event.target instanceof HTMLElement &&
      ['INPUT', 'TEXTAREA', 'SELECT'].includes(event.target.tagName)) {
      return;
    }

    // Spacebar for play/pause (prevent page scroll)
    if (event.code === 'Space') {
      event.preventDefault();
      handlePlayPause();
      return;
    }

    // Like/Dislike Song: Alt + Shift + B
    if (event.altKey && event.shiftKey && event.code === 'KeyB') {
      // No preventDefault needed for this combo
      if (song?.id) {
        toggleFavourite(song.id);
      }
      return;
    }

    // Shuffle: Alt + S (Mac) | Ctrl/Cmd + S (Windows)
    if (((event.altKey && navigator.platform.includes('Mac')) ||
      (event.ctrlKey && !navigator.platform.includes('Mac'))) &&
      event.code === 'KeyS') {
      event.preventDefault(); // Prevent browser save dialog
      toggleShuffle();
      return;
    }

    // Repeat: Alt + R (Mac) | Ctrl/Cmd + R (Windows)
    if (((event.altKey && navigator.platform.includes('Mac')) ||
      (event.ctrlKey && !navigator.platform.includes('Mac'))) &&
      event.code === 'KeyR') {
      event.preventDefault(); // Prevent browser refresh
      toggleRepeat();
      return;
    }

    // Mute/Unmute: M
    if (event.code === 'KeyM') {
      // No preventDefault needed for M key
      toggleMute();
      return;
    }

    // Go to Previous: Up Arrow
    if (event.code === 'ArrowUp') {
      event.preventDefault(); // Prevent page scroll
      previousSong();
      return;
    }

    // Go to Next: Down Arrow
    if (event.code === 'ArrowDown') {
      event.preventDefault(); // Prevent page scroll
      nextSong();
      return;
    }
  }, [handlePlayPause, song, toggleFavourite, toggleShuffle, toggleRepeat, toggleMute, previousSong, nextSong]);

  const handleLyricClick = useCallback((time: number) => {
    if (!soundRef.current) return;

    soundRef.current.seek(time);
    setSeekPosition(time);
  }, []);

  const toggleLyrics = useCallback(() => {
    setShowLyrics((prev) => !prev);
  }, []);

  const toggleQueue = useCallback(() => {
    setShowQueue((prev) => !prev);
  }, []);

  const addSongToPlaylist = useCallback(
    (playlistId: number, songId: number) => {
      window.ipc
        .invoke("addSongToPlaylist", { playlistId, songId })
        .then((response) => {
          toast(
            <NotificationToast
              success={response === true}
              message={
                response === true
                  ? "Song added to playlist"
                  : "Song already exists in playlist"
              }
            />,
          );
        })
        .catch(() => {
          toast(
            <NotificationToast
              success={false}
              message="Failed to add song to playlist"
            />,
          );
        });
    },
    [],
  );

  const handleSongSelect = useCallback((selectedSong: Song) => {
    // Find the song in the current queue and jump to it
    const songIndex = queue.findIndex(song => song.id === selectedSong.id);
    if (songIndex !== -1) {
      // Use the jumpToSong function which preserves history
      jumpToSong(songIndex);
    }
  }, [queue, jumpToSong]);

  // Enable client-side rendering
  useEffect(() => {
    setIsClient(true);

    // Load playlists once on component mount
    window.ipc
      .invoke("getAllPlaylists")
      .then(setPlaylists)
      .catch((err) => console.error("Failed to load playlists:", err));

    // Clean up on unmount
    return () => {
      if (seekUpdateInterval.current) {
        clearInterval(seekUpdateInterval.current);
      }

      if (scrobbleTimeout.current) {
        clearInterval(scrobbleTimeout.current);
      }
    };
  }, []);

  // Setup volume slider wheel event
  useEffect(() => {
    const volumeSlider = volumeSliderRef.current;
    if (!volumeSlider) return;

    volumeSlider.addEventListener('wheel', handleVolumeWheel, { passive: false });

    return () => {
      volumeSlider.removeEventListener('wheel', handleVolumeWheel);
    };
  }, [handleVolumeWheel]);

  useEffect(() => {
    document.addEventListener('keydown', handleKeyDown);

    return () => {
      document.removeEventListener('keydown', handleKeyDown);
    };
  }, [handleKeyDown]);

  // Update favorite status when song changes
  useEffect(() => {
    if (song) {
      setIsFavourite(favourite);
    }
  }, [song, favourite]);

  // Reset scrobble status when song changes
  useEffect(() => {
    setLastFmStatus({
      isScrobbled: false,
      isNowPlaying: false,
      scrobbleTimerStarted: false,
      error: null,
      lastFmActive: lastFmStatus.lastFmActive,
    });

    if (scrobbleTimeout.current) {
      clearInterval(scrobbleTimeout.current);
    }
  }, [song]);

  // Start scrobble timer when playing
  useEffect(() => {
    if (
      isPlaying &&
      song &&
      lastFmSettings.enableLastFm &&
      !lastFmStatus.scrobbleTimerStarted &&
      isAuthenticated()
    ) {
      // Send now playing update to Last.fm
      console.log("[Last.fm] Sending now playing update");
      updateNowPlaying(song)
        .then((success) => {
          setLastFmStatus((prev) => ({
            ...prev,
            isNowPlaying: success,
            scrobbleTimerStarted: true,
            error: success ? null : "Failed to update now playing",
          }));
          console.log("[Last.fm] Now playing update success:", success);
        })
        .catch((err) => {
          console.error("[Last.fm] Now playing error:", err);
          setLastFmStatus((prev) => ({
            ...prev,
            error: "Error updating now playing",
          }));
        });

      // Start scrobble timer
      handleScrobble();
    }
  }, [
    isPlaying,
    song,
    lastFmSettings,
    lastFmStatus.scrobbleTimerStarted,
    handleScrobble,
  ]);

  // Initialize or update audio when song changes
  useEffect(() => {
    // Clean up previous audio and intervals
    if (soundRef.current) {
      soundRef.current.unload();
    }

    if (seekUpdateInterval.current) {
      clearInterval(seekUpdateInterval.current);
    }

    // Reset seek position immediately when song changes
    setSeekPosition(0);

    // No song to play, exit early
    if (!song?.filePath) return;

    // Create new Howl instance
    const sound = new Howl({
      src: [`wora://${encodeURIComponent(song.filePath)}`],
      format: [song.filePath.split(".").pop()],
      html5: true,
      autoplay: true,
      preload: true,
      volume: isMuted ? 0 : volume,
      onload: () => {
        setSeekPosition(0);
        setIsPlaying(true);
        updateDiscordState(1, song);
        window.ipc.send("update-window", [true, song?.artist, song?.name]);
      },
      onloaderror: (error) => {
        console.error("Error loading audio:", error);
        setIsPlaying(false);
        toast(
          <NotificationToast success={false} message="Failed to load audio" />,
        );
      },
      onend: () => {
        setIsPlaying(false);
        window.ipc.send("update-window", [false, null, null]);
        if (!repeat) {
          nextSong();
        }
      },
      onplay: () => {
        setIsPlaying(true);
        window.ipc.send("update-window", [true, song?.artist, song?.name]);
      },
      onpause: () => {
        setIsPlaying(false);
        window.ipc.send("update-window", [false, false, false]);
      },
    });

    soundRef.current = sound;

    // Set up seek position updater
    seekUpdateInterval.current = setInterval(() => {
      if (sound.playing()) {
        setSeekPosition(sound.seek());
      }
    }, 100);

    // Clean up on unmount or when song changes
    return () => {
      sound.unload();
      if (seekUpdateInterval.current) {
        clearInterval(seekUpdateInterval.current);
      }
    };
  }, [song, nextSong]); // Removed volume and isMuted from dependencies

  // Handle lyrics updates
  useEffect(() => {
    if (!lyrics || !song || !isPlaying) return;

    // Only parse lyrics if they exist and are synced
    if (!isSyncedLyrics(lyrics)) return;

    const parsedLyrics = parseLyrics(lyrics);
    let lyricUpdateInterval: NodeJS.Timeout;

    const updateCurrentLyric = () => {
      if (!soundRef.current?.playing()) return;

      const currentSeek = soundRef.current.seek();
      const currentLyricLine = parsedLyrics.find((line, index) => {
        const nextLine = parsedLyrics[index + 1];
        return (
          currentSeek >= line.time && (!nextLine || currentSeek < nextLine.time)
        );
      });

      setCurrentLyric(currentLyricLine || null);
    };

    // Update lyrics less frequently than seek position (better performance)
    lyricUpdateInterval = setInterval(updateCurrentLyric, 500);

    return () => clearInterval(lyricUpdateInterval);
  }, [song, lyrics, isPlaying]);

  // Setup MediaSession API for media controls
  useEffect(() => {
    if (!song || !("mediaSession" in navigator)) return;

    const updateMediaSessionMetadata = async () => {
      if ("mediaSession" in navigator && song) {
        const toDataURL = (
          url: string,
          callback: (dataUrl: string) => void,
        ) => {
          const xhr = new XMLHttpRequest();
          xhr.onload = () => {
            const reader = new FileReader();
            reader.onloadend = () => callback(reader.result as string);
            reader.readAsDataURL(xhr.response);
          };
          xhr.open("GET", url);
          xhr.responseType = "blob";
          xhr.send();
        };

        const coverUrl = song.album?.cover
          ? song.album.cover.startsWith("/") || song.album.cover.includes("://")
            ? song.album.cover
            : `wora://${song.album.cover}`
          : "/coverArt.png";

        toDataURL(coverUrl, (dataUrl) => {
          navigator.mediaSession.metadata = new MediaMetadata({
            title: song?.name || "Unknown Title",
            artist: song?.artist || "Unknown Artist",
            album: song?.album?.name || "Unknown Album",
            artwork: [{ src: dataUrl }],
          });

          // Set application name for Windows Media Controller
          if ("mediaSession" in navigator) {
            // @ts-ignore - applicationName is not in the official type definitions but works in Windows
            navigator.mediaSession.metadata.applicationName = "Wora";
          }

          navigator.mediaSession.setActionHandler("play", handlePlayPause);
          navigator.mediaSession.setActionHandler("pause", handlePlayPause);
          navigator.mediaSession.setActionHandler(
            "previoustrack",
            previousSong,
          );
          navigator.mediaSession.setActionHandler("nexttrack", nextSong);
          navigator.mediaSession.setActionHandler("seekbackward", () => {
            if (soundRef.current) {
              soundRef.current.seek(Math.max(0, soundRef.current.seek() - 10));
            }
          });
          navigator.mediaSession.setActionHandler("seekforward", () => {
            if (soundRef.current) {
              soundRef.current.seek(
                Math.min(
                  soundRef.current.duration(),
                  soundRef.current.seek() + 10,
                ),
              );
            }
          });
        });
      }
    };

    updateMediaSessionMetadata();

    const removeMediaControlListener = window.ipc.on(
      "media-control",
      (command) => {
        switch (command) {
          case "play-pause":
            handlePlayPause();
            break;
          case "previous":
            previousSong();
            break;
          case "next":
            nextSong();
            break;
          default:
            break;
        }
      },
    );

    return () => {
      removeMediaControlListener();
    };
  }, [song, previousSong, nextSong]);

  // Apply volume and mute settings when they change
  useEffect(() => {
    if (!soundRef.current) return;

    // When unmuting, set volume first, then unmute
    if (!isMuted) {
      soundRef.current.volume(volume);
      soundRef.current.mute(false);
    } else {
      soundRef.current.mute(true);
    }
  }, [volume, isMuted]);

  // Apply repeat setting when it changes
  useEffect(() => {
    if (soundRef.current) {
      soundRef.current.loop(repeat);
    }
  }, [repeat]);

  // Server-side rendering placeholder
  if (!isClient) {
    return (
      <div className="wora-border h-28 w-full overflow-hidden rounded-2xl p-6">
        <div className="relative flex h-full w-full items-center">
          {/* Empty placeholder to prevent hydration errors */}
        </div>
      </div>
    );
  }

  return (
    <div>
      <div className="absolute top-0 right-0 w-full">
        {showLyrics && lyrics && (
          <Lyrics
            lyrics={parseLyrics(lyrics)}
            currentLyric={currentLyric}
            onLyricClick={handleLyricClick}
            isSyncedLyrics={isSyncedLyrics(lyrics)}
          />
        )}
      </div>

      <div className="!absolute top-0 right-0 w-96">
        {showQueue && <QueuePanel queue={queue} history={history} currentIndex={currentIndex} onSongSelect={handleSongSelect} />}
      </div>

      <div className="wora-border h-28 w-full overflow-hidden rounded-2xl p-6">
        <div className="relative flex h-full w-full items-center">
          <TooltipProvider>
            <div className="absolute left-0 flex w-1/4 items-center justify-start gap-4 overflow-hidden">
              {song ? (
                <ContextMenu>
                  <ContextMenuTrigger>
                    <Link
                      href={song.album?.id ? `/albums/${song.album.id}` : "#"}
                    >
                      <div className="relative min-h-17 min-w-17 overflow-hidden rounded-lg shadow-lg transition">
                        <Image
                          alt="Album Cover"
                          src={`wora://${song?.album.cover}`}
                          fill
                          priority={true}
                          className="object-cover object-center"
                        />
                      </div>
                    </Link>
                  </ContextMenuTrigger>

                  <ContextMenuContent className="w-64">
                    <Link href={`/albums/${song.album?.id}`}>
                      <ContextMenuItem className="flex items-center gap-2">
                        <IconVinyl stroke={2} size={14} />
                        Go to Album
                      </ContextMenuItem>
                    </Link>
                    <ContextMenuSub>
                      <ContextMenuSubTrigger className="flex items-center gap-2">
                        <IconPlus stroke={2} size={14} />
                        Add to Playlist
                      </ContextMenuSubTrigger>
                      <ContextMenuSubContent className="w-52">
                        {playlists.map((playlist) => (
                          <ContextMenuItem
                            key={playlist.id}
                            onClick={() =>
                              addSongToPlaylist(playlist.id, song.id)
                            }
                          >
                            <p className="w-full truncate">{playlist.name}</p>
                          </ContextMenuItem>
                        ))}
                      </ContextMenuSubContent>
                    </ContextMenuSub>
                  </ContextMenuContent>
                </ContextMenu>
              ) : (
                <div className="relative min-h-17 min-w-17 overflow-hidden rounded-lg shadow-lg">
                  <Image
                    alt="Album Cover"
                    src="/coverArt.png"
                    fill
                    priority={true}
                    className="object-cover"
                  />
                </div>
              )}

              <div className="w-full">
                <p className="truncate text-sm font-medium">
                  {song ? song.name : "Echoes of Emptiness"}
                </p>
                <Link
                  href={
                    song ? `/artists/${encodeURIComponent(song.artist)}` : "#"
                  }
                >
                  <p className="cursor-pointer truncate opacity-50 hover:underline hover:opacity-80">
                    {song ? song.artist : "The Void Ensemble"}
                  </p>
                </Link>
              </div>
            </div>

            <div className="absolute right-0 left-0 mx-auto flex h-full w-2/4 flex-col items-center justify-between gap-4">
              <div className="flex h-full w-full items-center justify-center gap-8">
                {metadata?.format?.lossless && (
                  <div className="flex">
                    <Tooltip delayDuration={0}>
                      <TooltipTrigger>
                        <IconRipple
                          stroke={2}
                          className="w-3.5 cursor-pointer"
                        />
                      </TooltipTrigger>
                      <TooltipContent side="left" sideOffset={25}>
                        Lossless [{metadata.format.bitsPerSample}/
                        {(metadata.format.sampleRate / 1000).toFixed(1)}kHz]
                      </TooltipContent>
                    </Tooltip>
                  </div>
                )}
                <Button
                  variant="ghost"
                  onClick={toggleShuffle}
                  className="relative opacity-100!"
                >
                  {!shuffle ? (
                    <IconArrowsShuffle2
                      stroke={2}
                      size={16}
                      className="wora-transition opacity-30! hover:opacity-100!"
                    />
                  ) : (
                    <div>
                      <IconArrowsShuffle2 stroke={2} size={16} />
                      <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>
                    </div>
                  )}
                </Button>

                <Button variant="ghost" onClick={previousSong}>
                  <IconPlayerSkipBack
                    stroke={2}
                    className="fill-black dark:fill-white"
                    size={15}
                  />
                </Button>

                <Button variant="ghost" onClick={handlePlayPause}>
                  {!isPlaying ? (
                    <IconPlayerPlay
                      stroke={2}
                      className="h-6 w-6 fill-black dark:fill-white"
                    />
                  ) : (
                    <IconPlayerPause
                      stroke={2}
                      className="h-6 w-6 fill-black dark:fill-white"
                    />
                  )}
                </Button>

                <Button variant="ghost" onClick={nextSong}>
                  <IconPlayerSkipForward
                    stroke={2}
                    className="h-4 w-4 fill-black dark:fill-white"
                  />
                </Button>

                <Button
                  variant="ghost"
                  onClick={toggleRepeat}
                  className="relative opacity-100!"
                >
                  {!repeat ? (
                    <IconRepeat
                      stroke={2}
                      size={15}
                      className="wora-transition opacity-30! hover:opacity-100!"
                    />
                  ) : (
                    <div>
                      <IconRepeat stroke={2} size={15} />
                      <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>
                    </div>
                  )}
                </Button>

                {lastFmSettings.enableLastFm &&
                  lastFmSettings.lastFmSessionKey &&
                  lastFmStatus.lastFmActive && (
                    <div className="absolute left-28">
                      <Tooltip delayDuration={0}>
                        <TooltipTrigger>
                          <IconBrandLastfm
                            stroke={2}
                            size={14}
                            className={`w-3.5 text-red-500 ${lastFmStatus.isScrobbled ? "" : lastFmStatus.isNowPlaying ? "animate-pulse" : "opacity-30"}`}
                          />
                        </TooltipTrigger>
                        <TooltipContent side="left" sideOffset={25}>
                          {lastFmStatus.error ? (
                            <p className="text-red-500">
                              Error: {lastFmStatus.error}
                            </p>
                          ) : lastFmStatus.isScrobbled ? (
                            <p>Scrobbled to Last.fm</p>
                          ) : lastFmStatus.isNowPlaying ? (
                            <p>
                              Now playing on Last.fm
                              <br />
                              Will scrobble at{" "}
                              {lastFmSettings.scrobbleThreshold}%
                            </p>
                          ) : (
                            <p>
                              Will scrobble at{" "}
                              {lastFmSettings.scrobbleThreshold}%
                            </p>
                          )}
                        </TooltipContent>
                      </Tooltip>
                    </div>
                  )}

                <div className="flex">
                  <Tooltip delayDuration={0}>
                    <TooltipTrigger asChild>
                      <Button
                        variant="ghost"
                        className="opacity-100! flex justify-center items-center"
                        onClick={() => toggleFavourite(song?.id)}
                        disabled={!song}
                      >
                        <IconHeart
                          stroke={2}
                          className={`w-3.5 text-red-500 ${isFavourite ? "fill-red-500" : "fill-none"}`}
                        />
                      </Button>
                    </TooltipTrigger>
                    <TooltipContent side="right" sideOffset={25}>
                      <p>
                        {!isFavourite
                          ? "Add to Favorites"
                          : "Remove from Favorites"}
                      </p>
                    </TooltipContent>
                  </Tooltip>
                </div>
              </div>

              <div className="relative flex h-full w-96 items-center px-4">
                <p className="absolute -left-8">{convertTime(seekPosition)}</p>
                <Slider
                  value={[seekPosition]}
                  onValueChange={handleSeek}
                  max={soundRef.current?.duration() || 0}
                  step={0.01}
                />
                <p className="absolute -right-8">
                  {convertTime(soundRef.current?.duration() || 0)}
                </p>
              </div>
            </div>

            <div className="absolute right-0 flex w-1/4 items-center justify-end gap-10">
              <div className="flex items-center gap-4">
                <Button
                  variant="ghost"
                  onClick={toggleMute}
                  className="opacity-100!"
                >
                  {!isMuted ? (
                    <IconVolume
                      stroke={2}
                      size={17.5}
                      className="wora-transition opacity-30! hover:opacity-100!"
                    />
                  ) : (
                    <IconVolumeOff
                      stroke={2}
                      size={17.5}
                      className="wora-transition opacity-30! hover:opacity-100!"
                    />
                  )}
                </Button>
                <Slider
                  ref={volumeSliderRef}
                  onValueChange={handleVolume}
                  value={[volume]}
                  max={1}
                  step={0.01}
                  className="w-24"
                />
              </div>

              <div className="flex items-center gap-4">
                {lyrics ? (
                  <Button variant="ghost" onClick={toggleLyrics}>
                    <IconMessage stroke={2} size={15} />
                  </Button>
                ) : (
                  <IconMessage
                    className="cursor-not-allowed text-red-500 opacity-75"
                    stroke={2}
                    size={15}
                  />
                )}

                <Dialog>
                  <DialogTrigger
                    className={
                      song
                        ? "opacity-30 duration-500 hover:opacity-100 cursor-pointer"
                        : "cursor-not-allowed text-red-500 opacity-75"
                    }
                    disabled={!song}
                  >
                    <IconInfoCircle stroke={2} size={15} />
                  </DialogTrigger>

                  {song && (
                    <DialogContent>
                      <DialogHeader>
                        <DialogTitle>Track Information</DialogTitle>
                        <DialogDescription>
                          Details for your currently playing song
                        </DialogDescription>
                      </DialogHeader>

                      <div className="flex gap-4 overflow-hidden text-xs">
                        {/* Album cover */}
                        <div className="h-full">
                          <div className="relative h-36 w-36 overflow-hidden rounded-xl">
                            <Image
                              alt={song.name || "Album"}
                              src={`wora://${song?.album.cover}`}
                              fill
                              className="object-cover"
                              quality={25}
                            />
                          </div>
                        </div>

                        {/* Track details */}
                        <div className="flex h-full w-full flex-col gap-0.5">
                          <p className="mb-4 truncate">
                            → {metadata?.common?.title} [
                            {metadata?.format?.codec || "Unknown"}]
                          </p>

                          <p className="truncate">
                            <span className="opacity-50">Artist:</span>{" "}
                            {metadata?.common?.artist || "Unknown"}
                          </p>

                          <p className="truncate">
                            <span className="opacity-50">Album:</span>{" "}
                            {metadata?.common?.album || "Unknown"}
                          </p>

                          <p className="truncate">
                            <span className="opacity-50">Codec:</span>{" "}
                            {metadata?.format?.codec || "Unknown"}
                          </p>

                          <p className="truncate">
                            <span className="opacity-50">Sample:</span>{" "}
                            {metadata?.format?.lossless
                              ? `Lossless [${metadata.format.bitsPerSample}/${(metadata.format.sampleRate / 1000).toFixed(1)}kHz]`
                              : "Lossy Audio"}
                          </p>

                          <p className="truncate">
                            <span className="opacity-50">Duration:</span>{" "}
                            {convertTime(soundRef.current?.duration() || 0)}
                          </p>

                          <p className="truncate">
                            <span className="opacity-50">Genre:</span>{" "}
                            {metadata?.common?.genre?.[0] || "Unknown"}
                          </p>

                          {lastFmSettings.enableLastFm &&
                            lastFmStatus.lastFmActive && (
                              <p className="truncate">
                                <span className="opacity-50">Last.fm:</span>{" "}
                                {lastFmStatus.error ? (
                                  <span className="text-red-500">
                                    Error: {lastFmStatus.error}
                                  </span>
                                ) : lastFmStatus.isScrobbled ? (
                                  "Scrobbled"
                                ) : lastFmStatus.isNowPlaying ? (
                                  <>
                                    Now playing (will scrobble at{" "}
                                    {lastFmSettings.scrobbleThreshold}%)
                                  </>
                                ) : (
                                  <>
                                    Waiting to scrobble at{" "}
                                    {lastFmSettings.scrobbleThreshold}%
                                  </>
                                )}
                              </p>
                            )}
                        </div>
                      </div>
                    </DialogContent>
                  )}
                </Dialog>

                <Button variant="ghost" onClick={toggleQueue}>
                  <IconList stroke={2} size={15} />
                </Button>
              </div>
            </div>
          </TooltipProvider>
        </div>
      </div>
    </div>
  );
};

export default Player;


================================================
FILE: renderer/components/themeProvider.tsx
================================================
"use client";

import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes";

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}


================================================
FILE: renderer/components/ui/actions.tsx
================================================
import {
  IconBox,
  IconLine,
  IconLineDashed,
  IconSquare,
  IconX,
} from "@tabler/icons-react";
import Image from "next/image";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";

type Data = {
  appVersion: string;
  isNotMac: boolean;
};

function Actions() {
  const [data, setData] = useState<Data>(null);
  const [isMaximized, setIsMaximized] = useState(false);

  useEffect(() => {
    window.ipc.invoke("getActionsData").then((response) => {
      setData(response);
    });
  }, []);

  return (
    <div className="drag absolute top-0 z-50 flex h-11 w-full items-center justify-end px-8 py-2.5">
      <div className="relative flex h-full w-full items-center justify-center">
        <div className="flex h-full items-center gap-2">
          <Image
            src={"/assets/Logo [Dark].ico"}
            alt="logo"
            width={16}
            height={16}
            className="hidden dark:block"
          />
          <Image
            src={"/assets/Logo.ico"}
            className="block dark:hidden"
            alt="logo"
            width={16}
            height={16}
          />
          Wora
        </div>
        <div className="no-drag absolute -right-2 top-0 flex h-full items-center gap-2.5">
          {data && data.isNotMac && (
            <>
              <Button
                variant="ghost"
                onClick={() => window.ipc.send("minimizeWindow", true)}
              >
                <IconLineDashed size={14} stroke={2} />
              </Button>
              <Button
                variant="ghost"
                onClick={() => {
                  setIsMaximized(!isMaximized);
                  window.ipc.send("maximizeWindow", !isMaximized);
                }}
              >
                <IconSquare size={11} stroke={2} />
              </Button>
              <Button
                variant="ghost"
                onClick={() => window.ipc.send("quitApp", true)}
              >
                <IconX size={14} stroke={2} />
              </Button>
            </>
          )}
        </div>
      </div>
    </div>
  );
}

export default Actions;


================================================
FILE: renderer/components/ui/album.tsx
================================================
import Image from "next/image";
import Link from "next/link";
import React from "react";

type Album = {
  id: string;
  name: string;
  artist: string;
  cover: string;
};

type AlbumCardProps = {
  album: Album;
};

const AlbumCard: React.FC<AlbumCardProps> = ({ album }) => {
  return (
    <Link href={`/albums/${album.id}`}>
      <div className="group/album wora-border wora-transition rounded-2xl p-5 hover:bg-black/5 dark:hover:bg-white/10">
        <div className="relative flex flex-col justify-between">
          <div className="relative w-full overflow-hidden rounded-xl pb-[100%] shadow-lg">
            <Image
              alt={album ? album.name : "Album Cover"}
              src={`wora://${album.cover}`}
              fill
              loading="lazy"
              className="z-10 cursor-pointer object-cover"
            />
          </div>
          <div className="mt-8 flex w-full flex-col overflow-clip">
            <p className="cursor-pointer mask-r-from-70% text-sm font-medium text-nowrap">
              {album.name}
            </p>
            <p className="mr-2 truncate opacity-50">{album.artist}</p>
          </div>
        </div>
      </div>
    </Link>
  );
};

export default AlbumCard;


================================================
FILE: renderer/components/ui/avatar.tsx
================================================
"use client";

import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";

import { cn } from "@/lib/utils";

const Avatar = React.forwardRef<
  React.ElementRef<typeof AvatarPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
  <AvatarPrimitive.Root
    ref={ref}
    className={cn(
      "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full shadow-lg",
      className,
    )}
    {...props}
  />
));
Avatar.displayName = AvatarPrimitive.Root.displayName;

const AvatarImage = React.forwardRef<
  React.ElementRef<typeof AvatarPrimitive.Image>,
  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
  <AvatarPrimitive.Image
    ref={ref}
    className={cn("aspect-square h-full w-full", className)}
    {...props}
  />
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;

const AvatarFallback = React.forwardRef<
  React.ElementRef<typeof AvatarPrimitive.Fallback>,
  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
  <AvatarPrimitive.Fallback
    ref={ref}
    className={cn(
      "flex h-full w-full items-center justify-center rounded-full bg-white/10",
      className,
    )}
    {...props}
  />
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;

export { Avatar, AvatarImage, AvatarFallback };


================================================
FILE: renderer/components/ui/badge.tsx
================================================
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";

import { cn } from "@/lib/utils";

const badgeVariants = cva(
  "inline-flex items-center rounded-full py-1 px-2 text-[0.675rem]",
  {
    variants: {
      variant: {
        default: "bg-black/5 dark:bg-white/10",
        secondary:
          "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",
        destructive:
          "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",
        outline: "text-neutral-950 dark:text-neutral-50",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  },
);

export interface BadgeProps
  extends React.HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof badgeVariants> {}

function Badge({ className, variant, ...props }: BadgeProps) {
  return (
    <div className={cn(badgeVariants({ variant }), className)} {...props} />
  );
}

export { Badge, badgeVariants };


================================================
FILE: renderer/components/ui/button.tsx
================================================
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";

import { cn } from "@/lib/utils";

const buttonVariants = cva(
  "cursor-pointer active:scale-90 inline-flex py-2.5 px-4 items-center gap-2 rounded-xl wora-transition",
  {
    variants: {
      variant: {
        default: "bg-white/70 dark:bg-black/30 hover:scale-95 wora-border",
        destructive: "bg-red-500/10 hover:scale-95 border border-red-500/15",
        outline:
          "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",
        ghost: "wora-transition opacity-30 hover:opacity-100 p-0",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  },
);

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean;
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button";
    return (
      <Comp
        className={cn(buttonVariants({ variant, className }))}
        ref={ref}
        {...props}
      />
    );
  },
);
Button.displayName = "Button";

export { Button, buttonVariants };


================================================
FILE: renderer/components/ui/carousel.tsx
================================================
"use client";

import * as React from "react";
import useEmblaCarousel, {
  type UseEmblaCarouselType,
} from "embla-carousel-react";

import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
  IconArrowLeft,
  IconArrowRight,
  IconChevronCompactLeft,
  IconChevronLeft,
  IconChevronRight,
} from "@tabler/icons-react";

type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];

type CarouselProps = {
  opts?: CarouselOptions;
  plugins?: CarouselPlugin;
  orientation?: "horizontal" | "vertical";
  setApi?: (api: CarouselApi) => void;
};

type CarouselContextProps = {
  carouselRef: ReturnType<typeof useEmblaCarousel>[0];
  api: ReturnType<typeof useEmblaCarousel>[1];
  scrollPrev: () => void;
  scrollNext: () => void;
  canScrollPrev: boolean;
  canScrollNext: boolean;
} & CarouselProps;

const CarouselContext = React.createContext<CarouselContextProps | null>(null);

function useCarousel() {
  const context = React.useContext(CarouselContext);

  if (!context) {
    throw new Error("useCarousel must be used within a <Carousel />");
  }

  return context;
}

const Carousel = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
  (
    {
      orientation = "horizontal",
      opts,
      setApi,
      plugins,
      className,
      children,
      ...props
    },
    ref,
  ) => {
    const [carouselRef, api] = useEmblaCarousel(
      {
        ...opts,
        axis: orientation === "horizontal" ? "x" : "y",
      },
      plugins,
    );
    const [canScrollPrev, setCanScrollPrev] = React.useState(false);
    const [canScrollNext, setCanScrollNext] = React.useState(false);

    const onSelect = React.useCallback((api: CarouselApi) => {
      if (!api) {
        return;
      }

      setCanScrollPrev(api.canScrollPrev());
      setCanScrollNext(api.canScrollNext());
    }, []);

    const scrollPrev = React.useCallback(() => {
      api?.scrollPrev();
    }, [api]);

    const scrollNext = React.useCallback(() => {
      api?.scrollNext();
    }, [api]);

    const handleKeyDown = React.useCallback(
      (event: React.KeyboardEvent<HTMLDivElement>) => {
        if (event.key === "ArrowLeft") {
          event.preventDefault();
          scrollPrev();
        } else if (event.key === "ArrowRight") {
          event.preventDefault();
          scrollNext();
        }
      },
      [scrollPrev, scrollNext],
    );

    React.useEffect(() => {
      if (!api || !setApi) {
        return;
      }

      setApi(api);
    }, [api, setApi]);

    React.useEffect(() => {
      if (!api) {
        return;
      }

      onSelect(api);
      api.on("reInit", onSelect);
      api.on("select", onSelect);

      return () => {
        api?.off("select", onSelect);
      };
    }, [api, onSelect]);

    return (
      <CarouselContext.Provider
        value={{
          carouselRef,
          api: api,
          opts,
          orientation:
            orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
          scrollPrev,
          scrollNext,
          canScrollPrev,
          canScrollNext,
        }}
      >
        <div
          ref={ref}
          onKeyDownCapture={handleKeyDown}
          className={cn("relative", className)}
          role="region"
          aria-roledescription="carousel"
          {...props}
        >
          {children}
        </div>
      </CarouselContext.Provider>
    );
  },
);
Carousel.displayName = "Carousel";

const CarouselContent = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
  const { carouselRef, orientation } = useCarousel();

  return (
    <div ref={carouselRef} className="overflow-hidden">
      <div
        ref={ref}
        className={cn(
          "flex",
          orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
          className,
        )}
        {...props}
      />
    </div>
  );
});
CarouselContent.displayName = "CarouselContent";

const CarouselItem = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
  const { orientation } = useCarousel();

  return (
    <div
      ref={ref}
      role="group"
      aria-roledescription="slide"
      className={cn(
        "min-w-0 shrink-0 grow-0 basis-full",
        orientation === "horizontal" ? "pl-4" : "pt-4",
        className,
      )}
      {...props}
    />
  );
});
CarouselItem.displayName = "CarouselItem";

const CarouselPrevious = React.forwardRef<
  HTMLButtonElement,
  React.ComponentProps<typeof Button>
>(({ className, variant = "ghost", ...props }, ref) => {
  const { orientation, scrollPrev, canScrollPrev } = useCarousel();

  return (
    <Button
      ref={ref}
      variant={variant}
      className={cn(
        "absolute h-8 w-8 rounded-full",
        orientation === "horizontal"
          ? "-left-12 top-1/2 -translate-y-1/2"
          : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
        className,
      )}
      disabled={!canScrollPrev}
      onClick={scrollPrev}
      {...props}
    >
      <IconChevronLeft stroke={2} size={28} />
    </Button>
  );
});
CarouselPrevious.displayName = "CarouselPrevious";

const CarouselNext = React.forwardRef<
  HTMLButtonElement,
  React.ComponentProps<typeof Button>
>(({ className, variant = "ghost", ...props }, ref) => {
  const { orientation, scrollNext, canScrollNext } = useCarousel();

  return (
    <Button
      ref={ref}
      variant={variant}
      className={cn(
        "absolute h-8 w-8 rounded-full",
        orientation === "horizontal"
          ? "-right-12 top-1/2 -translate-y-1/2"
          : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
        className,
      )}
      disabled={!canScrollNext}
      onClick={scrollNext}
      {...props}
    >
      <IconChevronRight stroke={2} size={28} />
    </Button>
  );
});
CarouselNext.displayName = "CarouselNext";

export {
  type CarouselApi,
  Carousel,
  CarouselContent,
  CarouselItem,
  CarouselPrevious,
  CarouselNext,
};


================================================
FILE: renderer/components/ui/command.tsx
================================================
"use client";

import * as React from "react";
import { type DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";

import { cn } from "@/lib/utils";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { IconSearch } from "@tabler/icons-react";

const Command = React.forwardRef<
  React.ElementRef<typeof CommandPrimitive>,
  React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
  <CommandPrimitive
    ref={ref}
    className={cn(
      "wora-transition no-scrollar max-h-96 min-h-96 overflow-hidden rounded-xl",
      className,
    )}
    {...props}
  />
));
Command.displayName = CommandPrimitive.displayName;

interface CommandDialogProps extends DialogProps {}

const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
  return (
    <Dialog {...props}>
      <DialogContent className="overflow-hidden p-0">
        <Command>{children}</Command>
      </DialogContent>
    </Dialog>
  );
};

const CommandInput = React.forwardRef<
  React.ElementRef<typeof CommandPrimitive.Input>,
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
  <div
    className="flex items-center border-b border-black/5 p-3.5 text-black dark:border-white/10 dark:text-white"
    cmdk-input-wrapper=""
  >
    <IconSearch className="mr-2 h-5 w-5 shrink-0 opacity-50" />
    <CommandPrimitive.Input
      ref={ref}
      className={cn(
        "flex w-full bg-transparent text-xs outline-hidden placeholder:text-black/50 dark:placeholder:text-white/50",
        className,
      )}
      {...props}
    />
  </div>
));

CommandInput.displayName = CommandPrimitive.Input.displayName;

const CommandList = React.forwardRef<
  React.ElementRef<typeof CommandPrimitive.List>,
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
  <CommandPrimitive.List
    ref={ref}
    className={cn(
      "no-scrollbar max-h-[325px] overflow-y-auto overflow-x-hidden rounded-2xl",
      className,
    )}
    {...props}
  />
));

CommandList.displayName = CommandPrimitive.List.displayName;

const CommandEmpty = React.forwardRef<
  React.ElementRef<typeof CommandPrimitive.Empty>,
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
  <CommandPrimitive.Empty
    ref={ref}
    className="flex h-full w-full items-center justify-center"
    {...props}
  />
));

CommandEmpty.displayName = CommandPrimitive.Empty.displayName;

const CommandGroup = React.forwardRef<
  React.ElementRef<typeof CommandPrimitive.Group>,
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
  <CommandPrimitive.Group
    ref={ref}
    className={cn(
      "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",
      className,
    )}
    {...props}
  />
));

CommandGroup.displayName = CommandPrimitive.Group.displayName;

const CommandSeparator = React.forwardRef<
  React.ElementRef<typeof CommandPrimitive.Separator>,
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
  <CommandPrimitive.Separator
    ref={ref}
    className={cn("-mx-1 h-px bg-neutral-200 dark:bg-neutral-800", className)}
    {...props}
  />
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;

const CommandItem = React.forwardRef<
  React.ElementRef<typeof CommandPrimitive.Item>,
  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
  <CommandPrimitive.Item
    ref={ref}
    className={cn(
      "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",
      className,
    )}
    {...props}
  />
));

CommandItem.displayName = CommandPrimitive.Item.displayName;

const CommandShortcut = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
  return (
    <span
      className={cn(
        "ml-auto text-xs tracking-widest text-neutral-500 dark:text-neutral-400",
        className,
      )}
      {...props}
    />
  );
};
CommandShortcut.displayName = "CommandShortcut";

export {
  Command,
  CommandDialog,
  CommandInput,
  CommandList,
  CommandEmpty,
  CommandGroup,
  CommandItem,
  CommandShortcut,
  CommandSeparator,
};


================================================
FILE: renderer/components/ui/context-menu.tsx
================================================
"use client";

import * as React from "react";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";

import { cn } from "@/lib/utils";
import {
  IconCheck,
  IconChevronRight,
  IconCircleFilled,
} from "@tabler/icons-react";

const ContextMenu = ContextMenuPrimitive.Root;

const ContextMenuTrigger = ContextMenuPrimitive.Trigger;

const ContextMenuGroup = ContextMenuPrimitive.Group;

const ContextMenuPortal = ContextMenuPrimitive.Portal;

const ContextMenuSub = ContextMenuPrimitive.Sub;

const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;

const ContextMenuSubTrigger = React.forwardRef<
  React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
    inset?: boolean;
  }
>(({ className, inset, children, ...props }, ref) => (
  <ContextMenuPrimitive.SubTrigger
    ref={ref}
    className={cn(
      "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",
      inset && "pl-8",
      className,
    )}
    {...props}
  >
    {children}
    <IconChevronRight className="ml-auto h-4 w-4" />
  </ContextMenuPrimitive.SubTrigger>
));
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;

const ContextMenuSubContent = React.forwardRef<
  React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
  <ContextMenuPrimitive.SubContent
    ref={ref}
    className={cn(
      "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",
      className,
    )}
    {...props}
  />
));
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;

const ContextMenuContent = React.forwardRef<
  React.ElementRef<typeof ContextMenuPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
  <ContextMenuPrimitive.Portal>
    <ContextMenuPrimitive.Content
      ref={ref}
      className={cn(
        "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",
        className,
      )}
      {...props}
    />
  </ContextMenuPrimitive.Portal>
));
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;

const ContextMenuItem = React.forwardRef<
  React.ElementRef<typeof ContextMenuPrimitiv
Download .txt
gitextract_a5817xvy/

├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── report_issue.yml
│   │   └── request_feature.yml
│   └── workflows/
│       └── release.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── bun.lockb
├── components.json
├── electron-builder.yml
├── main/
│   ├── background.ts
│   ├── helpers/
│   │   ├── create-window.ts
│   │   ├── db/
│   │   │   ├── connectDB.ts
│   │   │   ├── createDB.ts
│   │   │   └── schema.ts
│   │   ├── index.ts
│   │   └── lastfm-service.ts
│   └── preload.ts
├── package.json
├── renderer/
│   ├── components/
│   │   ├── ErrorBoundary.tsx
│   │   ├── LoadingSkeletons.tsx
│   │   ├── PageTransition.tsx
│   │   ├── PageTransitionMinimal.tsx
│   │   ├── main/
│   │   │   ├── lyrics.tsx
│   │   │   ├── navbar.tsx
│   │   │   └── player.tsx
│   │   ├── themeProvider.tsx
│   │   └── ui/
│   │       ├── actions.tsx
│   │       ├── album.tsx
│   │       ├── avatar.tsx
│   │       ├── badge.tsx
│   │       ├── button.tsx
│   │       ├── carousel.tsx
│   │       ├── command.tsx
│   │       ├── context-menu.tsx
│   │       ├── dialog.tsx
│   │       ├── form.tsx
│   │       ├── input.tsx
│   │       ├── label.tsx
│   │       ├── scroll-area.tsx
│   │       ├── select.tsx
│   │       ├── skeleton.tsx
│   │       ├── slider.tsx
│   │       ├── songs.tsx
│   │       ├── sonner.tsx
│   │       ├── spinner.tsx
│   │       ├── switch.tsx
│   │       ├── tabs.tsx
│   │       └── tooltip.tsx
│   ├── context/
│   │   └── playerContext.tsx
│   ├── hooks/
│   │   ├── useDebounce.ts
│   │   └── useScrollAreaRestoration.ts
│   ├── lib/
│   │   ├── albumCache.ts
│   │   ├── apiConfig.ts
│   │   ├── helpers.ts
│   │   ├── lastfm-client.ts
│   │   ├── lastfm.ts
│   │   ├── songCache.ts
│   │   └── utils.ts
│   ├── next-env.d.ts
│   ├── next.config.js
│   ├── pages/
│   │   ├── _app.tsx
│   │   ├── albums/
│   │   │   └── [slug].tsx
│   │   ├── albums.tsx
│   │   ├── artists/
│   │   │   ├── [name].tsx
│   │   │   └── index.tsx
│   │   ├── home.tsx
│   │   ├── playlists/
│   │   │   └── [slug].tsx
│   │   ├── playlists.tsx
│   │   ├── settings.tsx
│   │   ├── setup.tsx
│   │   └── songs.tsx
│   ├── postcss.config.js
│   ├── preload.d.ts
│   ├── styles/
│   │   └── globals.css
│   └── tsconfig.json
├── resources/
│   └── icon.icns
├── tsconfig.json
└── vercel/
    ├── next-env.d.ts
    ├── next.config.js
    ├── package.json
    ├── pages/
    │   ├── _app.tsx
    │   ├── api/
    │   │   ├── config.ts
    │   │   ├── index.ts
    │   │   ├── lastfm/
    │   │   │   ├── auth.ts
    │   │   │   ├── now-playing.ts
    │   │   │   ├── scrobble.ts
    │   │   │   ├── track-info.ts
    │   │   │   └── user-info.ts
    │   │   └── utils/
    │   │       └── lastfm.ts
    │   └── index.tsx
    ├── tsconfig.json
    └── vercel.json
Download .txt
SYMBOL INDEX (176 symbols across 54 files)

FILE: main/helpers/db/connectDB.ts
  constant APP_DATA (line 15) | const APP_DATA = app.getPath("userData");
  constant ART_DIR (line 16) | const ART_DIR = path.join(APP_DATA, "utilities/uploads/covers");
  function isAudioFile (line 40) | function isAudioFile(filePath: string): boolean {
  function findFirstImageInDirectory (line 44) | function findFirstImageInDirectory(dir: string): string | null {
  function readFilesRecursively (line 70) | function readFilesRecursively(dir: string, batch = 100): string[] {
  function scanEntireLibrary (line 104) | function scanEntireLibrary(dir: string): string[] {
  function processLibrary (line 540) | async function processLibrary(musicFolder: string, incremental = false) {
  function getAllFilePathsFromDb (line 604) | async function getAllFilePathsFromDb(): Promise<Set<string>> {
  function scanImmediateDirectory (line 610) | function scanImmediateDirectory(dir: string): string[] {
  function processBatch (line 659) | async function processBatch(files: string[], dbFilePaths: Set<string>) {
  function processAudioFile (line 674) | async function processAudioFile(file: string, albumCache: Map<string, an...
  function processAlbumArt (line 780) | async function processAlbumArt(imagePath: string): Promise<string> {
  function processEmbeddedArt (line 818) | async function processEmbeddedArt(cover: any): Promise<string> {
  function cleanupOrphanedRecords (line 851) | async function cleanupOrphanedRecords(currentFiles: string[]) {
  function sendToRenderer (line 955) | function sendToRenderer(channel: string, data: any) {

FILE: main/helpers/lastfm-service.ts
  constant API_URL (line 33) | const API_URL = "https://ws.audioscrobbler.com/2.0/";
  constant CACHE_TTL (line 36) | const CACHE_TTL = 15 * 60 * 1000;
  constant DEV_API_KEY (line 72) | const DEV_API_KEY = process.env.LASTFM_API_KEY || "";
  constant DEV_API_SECRET (line 73) | const DEV_API_SECRET = process.env.LASTFM_API_SECRET || "";

FILE: main/preload.ts
  method send (line 4) | send(channel: string, value: unknown) {
  method on (line 7) | on(channel: string, callback: (...args: unknown[]) => void) {
  method invoke (line 16) | async invoke(channel: string, ...args: unknown[]) {
  type IpcHandler (line 29) | type IpcHandler = typeof handler;

FILE: renderer/components/ErrorBoundary.tsx
  type Props (line 5) | interface Props {
  type State (line 10) | interface State {
  class ErrorBoundary (line 15) | class ErrorBoundary extends Component<Props, State> {
    method getDerivedStateFromError (line 21) | public static getDerivedStateFromError(error: Error): State {
    method componentDidCatch (line 25) | public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    method render (line 34) | public render() {

FILE: renderer/components/LoadingSkeletons.tsx
  function ArtistGridSkeleton (line 4) | function ArtistGridSkeleton({ count = 12, viewMode = 'grid-large' }: { c...
  function AlbumGridSkeleton (line 41) | function AlbumGridSkeleton({ count = 12 }: { count?: number }) {
  function SongListSkeleton (line 55) | function SongListSkeleton({ count = 10 }: { count?: number }) {
  function ArtistDetailSkeleton (line 72) | function ArtistDetailSkeleton() {

FILE: renderer/components/PageTransition.tsx
  type PageTransitionProps (line 5) | interface PageTransitionProps {
  function PageTransition (line 28) | function PageTransition({ children }: PageTransitionProps) {

FILE: renderer/components/PageTransitionMinimal.tsx
  type PageTransitionProps (line 3) | interface PageTransitionProps {
  function PageTransitionMinimal (line 10) | function PageTransitionMinimal({ children }: PageTransitionProps) {

FILE: renderer/components/main/lyrics.tsx
  type LyricsProps (line 7) | interface LyricsProps {

FILE: renderer/components/main/navbar.tsx
  type Settings (line 37) | type Settings = {
  type NavLink (line 42) | type NavLink = {

FILE: renderer/components/main/player.tsx
  function getAlbumCoverUrl (line 84) | function getAlbumCoverUrl(song: Song | undefined): string {

FILE: renderer/components/themeProvider.tsx
  function ThemeProvider (line 7) | function ThemeProvider({ children, ...props }: ThemeProviderProps) {

FILE: renderer/components/ui/actions.tsx
  type Data (line 12) | type Data = {
  function Actions (line 17) | function Actions() {

FILE: renderer/components/ui/album.tsx
  type Album (line 5) | type Album = {
  type AlbumCardProps (line 12) | type AlbumCardProps = {

FILE: renderer/components/ui/badge.tsx
  type BadgeProps (line 25) | interface BadgeProps
  function Badge (line 29) | function Badge({ className, variant, ...props }: BadgeProps) {

FILE: renderer/components/ui/button.tsx
  type ButtonProps (line 25) | interface ButtonProps

FILE: renderer/components/ui/carousel.tsx
  type CarouselApi (line 18) | type CarouselApi = UseEmblaCarouselType[1];
  type UseCarouselParameters (line 19) | type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
  type CarouselOptions (line 20) | type CarouselOptions = UseCarouselParameters[0];
  type CarouselPlugin (line 21) | type CarouselPlugin = UseCarouselParameters[1];
  type CarouselProps (line 23) | type CarouselProps = {
  type CarouselContextProps (line 30) | type CarouselContextProps = {
  function useCarousel (line 41) | function useCarousel() {

FILE: renderer/components/ui/command.tsx
  type CommandDialogProps (line 26) | interface CommandDialogProps extends DialogProps {}

FILE: renderer/components/ui/form.tsx
  type FormFieldContextValue (line 18) | type FormFieldContextValue<
  type FormItemContextValue (line 65) | type FormItemContextValue = {

FILE: renderer/components/ui/input.tsx
  type InputProps (line 5) | interface InputProps

FILE: renderer/components/ui/skeleton.tsx
  function Skeleton (line 3) | function Skeleton({

FILE: renderer/components/ui/songs.tsx
  type Playlist (line 38) | type Playlist = {
  type SongsProps (line 43) | type SongsProps = {

FILE: renderer/components/ui/sonner.tsx
  type ToasterProps (line 6) | type ToasterProps = React.ComponentProps<typeof Sonner>;

FILE: renderer/components/ui/spinner.tsx
  type SpinnerProps (line 4) | interface SpinnerProps extends React.HTMLAttributes<SVGElement> {}

FILE: renderer/context/playerContext.tsx
  type Song (line 12) | interface Song {
  type PlayerState (line 26) | interface PlayerState {
  type PlayerContextType (line 37) | interface PlayerContextType extends PlayerState {
  function findSongIndexById (line 85) | function findSongIndexById(songs: Song[], id: number): number {

FILE: renderer/hooks/useDebounce.ts
  function useDebounce (line 3) | function useDebounce<T>(value: T, delay: number = 300): T {

FILE: renderer/hooks/useScrollAreaRestoration.ts
  constant DEBUG (line 4) | const DEBUG = false;
  constant MAX_RESTORE_ATTEMPTS (line 5) | const MAX_RESTORE_ATTEMPTS = 10;
  constant RESTORE_ATTEMPT_DELAY (line 6) | const RESTORE_ATTEMPT_DELAY = 100;
  constant MAX_RESTORE_TIME (line 7) | const MAX_RESTORE_TIME = 2000;
  function useScrollAreaRestoration (line 9) | function useScrollAreaRestoration(key: string) {

FILE: renderer/lib/albumCache.ts
  type Album (line 2) | interface Album {
  type AlbumCacheStore (line 12) | interface AlbumCacheStore {
  constant CACHE_TTL (line 44) | const CACHE_TTL = 5 * 60 * 1000;
  class AlbumCache (line 46) | class AlbumCache {
    method constructor (line 50) | private constructor() {
    method getInstance (line 69) | public static getInstance(): AlbumCache {
    method sortAlbums (line 77) | public sortAlbums(
    method getAllAlbums (line 137) | public getAllAlbums(): Album[] {
    method getFilteredAlbums (line 142) | public getFilteredAlbums(): Album[] {
    method getSearchResults (line 147) | public getSearchResults(): Album[] {
    method getPage (line 152) | public getPage(): number {
    method isInitialized (line 157) | public isInitialized(): boolean {
    method hasMore (line 162) | public hasMore(): boolean {
    method getSortSettings (line 167) | public getSortSettings(): { sortBy: string; sortOrder: string } {
    method getViewMode (line 175) | public getViewMode(): "grid" | "compact-grid" | "list" {
    method getLastSearchQuery (line 180) | public getLastSearchQuery(): string {
    method isStale (line 185) | public isStale(): boolean {
    method setAllAlbums (line 190) | public setAllAlbums(albums: Album[]): void {
    method addAlbums (line 197) | public addAlbums(newAlbums: Album[]): void {
    method setFilteredAlbums (line 210) | public setFilteredAlbums(albums: Album[]): void {
    method setSearchResults (line 216) | public setSearchResults(albums: Album[], query: string): void {
    method updatePagination (line 223) | public updatePagination(page: number, hasMore: boolean): void {
    method updateSortSettings (line 230) | public updateSortSettings(sortBy: string, sortOrder: string): void {
    method updateViewMode (line 237) | public updateViewMode(viewMode: "grid" | "compact-grid" | "list"): void {
    method getAlbumWithSongs (line 243) | public async getAlbumWithSongs(albumId: number): Promise<any> {
    method setInitialized (line 270) | public setInitialized(): void {
    method resetCache (line 276) | public resetCache(): void {
    method resetState (line 284) | public resetState(resetData: Partial<AlbumCacheStore>): void {
    method saveToLocalStorage (line 327) | private saveToLocalStorage(): void {

FILE: renderer/lib/apiConfig.ts
  constant PROD_API_URL (line 5) | const PROD_API_URL = "https://wora-ten.vercel.app";
  constant DEV_API_URL (line 6) | const DEV_API_URL = "http://localhost:3000";
  constant API_BASE_URL (line 9) | const API_BASE_URL = isDev ? DEV_API_URL : PROD_API_URL;

FILE: renderer/lib/helpers.ts
  type MetadataResponse (line 6) | interface MetadataResponse {
  type LyricLine (line 11) | interface LyricLine {
  function fetchCover (line 31) | async function fetchCover(artist: string, album: string) {
  type DiscordState (line 157) | interface DiscordState {

FILE: renderer/lib/lastfm-client.ts
  constant LASTFM_API_BASE (line 10) | const LASTFM_API_BASE = `${API_BASE_URL}/api/lastfm`;

FILE: renderer/lib/lastfm.ts
  type Song (line 4) | interface Song {
  type LastFmUserCache (line 18) | interface LastFmUserCache {
  constant CACHE_EXPIRY_MS (line 29) | const CACHE_EXPIRY_MS = 24 * 60 * 60 * 1000;

FILE: renderer/lib/songCache.ts
  type SongCacheStore (line 5) | interface SongCacheStore {
  constant CACHE_TTL (line 33) | const CACHE_TTL = 5 * 60 * 1000;
  class SongCache (line 35) | class SongCache {
    method constructor (line 39) | private constructor() {
    method getInstance (line 58) | public static getInstance(): SongCache {
    method sortSongs (line 66) | public sortSongs(songs: Song[], sortBy: string, sortOrder: string): So...
    method getAllSongs (line 92) | public getAllSongs(): Song[] {
    method getFilteredSongs (line 97) | public getFilteredSongs(): Song[] {
    method getSearchResults (line 102) | public getSearchResults(): Song[] {
    method getPage (line 107) | public getPage(): number {
    method isInitialized (line 112) | public isInitialized(): boolean {
    method hasMore (line 117) | public hasMore(): boolean {
    method getSortSettings (line 122) | public getSortSettings(): { sortBy: string; sortOrder: string } {
    method getLastSearchQuery (line 130) | public getLastSearchQuery(): string {
    method isStale (line 135) | public isStale(): boolean {
    method setAllSongs (line 140) | public setAllSongs(songs: Song[]): void {
    method addSongs (line 147) | public addSongs(newSongs: Song[]): void {
    method setFilteredSongs (line 158) | public setFilteredSongs(songs: Song[]): void {
    method setSearchResults (line 164) | public setSearchResults(songs: Song[], query: string): void {
    method updatePagination (line 171) | public updatePagination(page: number, hasMore: boolean): void {
    method updateSortSettings (line 178) | public updateSortSettings(sortBy: string, sortOrder: string): void {
    method setInitialized (line 185) | public setInitialized(): void {
    method resetCache (line 191) | public resetCache(): void {
    method resetState (line 199) | public resetState(resetData: Partial<SongCacheStore>): void {
    method saveToLocalStorage (line 239) | private saveToLocalStorage(): void {

FILE: renderer/lib/utils.ts
  function cn (line 4) | function cn(...inputs: ClassValue[]) {

FILE: renderer/pages/_app.tsx
  constant SPECIAL_LAYOUTS (line 14) | const SPECIAL_LAYOUTS = ["/setup"];
  function App (line 16) | function App({ Component, pageProps }) {

FILE: renderer/pages/albums.tsx
  type ViewMode (line 34) | type ViewMode = "grid-large" | "grid-small" | "list";
  function Albums (line 37) | function Albums() {

FILE: renderer/pages/albums/[slug].tsx
  type Album (line 16) | type Album = {
  function Album (line 25) | function Album() {

FILE: renderer/pages/artists/[name].tsx
  type Album (line 20) | type Album = {
  type Artist (line 29) | type Artist = {
  type ArtistInfo (line 50) | type ArtistInfo = {
  type TopTrack (line 72) | type TopTrack = {
  function ArtistView (line 81) | function ArtistView() {

FILE: renderer/pages/artists/index.tsx
  type ArtistItem (line 26) | type ArtistItem = {
  type ViewMode (line 33) | type ViewMode = "grid-large" | "grid-small" | "list";
  type SortBy (line 34) | type SortBy = "name" | "albums" | "songs";
  type SortOrder (line 35) | type SortOrder = "asc" | "desc";
  function ArtistsPage (line 38) | function ArtistsPage() {

FILE: renderer/pages/home.tsx
  function Home (line 16) | function Home() {

FILE: renderer/pages/playlists.tsx
  function Playlists (line 38) | function Playlists() {

FILE: renderer/pages/playlists/[slug].tsx
  type Playlist (line 47) | type Playlist = {
  function Playlist (line 55) | function Playlist() {

FILE: renderer/pages/settings.tsx
  type Settings (line 59) | type Settings = {
  type LastFmSettings (line 69) | type LastFmSettings = {
  function Settings (line 76) | function Settings() {

FILE: renderer/pages/setup.tsx
  function Setup (line 10) | function Setup() {

FILE: renderer/pages/songs.tsx
  function AllSongs (line 25) | function AllSongs() {

FILE: renderer/preload.d.ts
  type Window (line 4) | interface Window {

FILE: vercel/pages/_app.tsx
  function App (line 3) | function App({ Component, pageProps }: AppProps) {

FILE: vercel/pages/api/config.ts
  constant LASTFM_CONFIG (line 3) | const LASTFM_CONFIG = {

FILE: vercel/pages/api/index.ts
  function handler (line 3) | function handler(req: NextApiRequest, res: NextApiResponse) {

FILE: vercel/pages/api/lastfm/auth.ts
  function handler (line 6) | async function handler(

FILE: vercel/pages/api/lastfm/now-playing.ts
  function handler (line 5) | async function handler(

FILE: vercel/pages/api/lastfm/scrobble.ts
  function handler (line 5) | async function handler(

FILE: vercel/pages/api/lastfm/track-info.ts
  function handler (line 4) | async function handler(

FILE: vercel/pages/api/lastfm/user-info.ts
  function handler (line 5) | async function handler(

FILE: vercel/pages/index.tsx
  function Home (line 3) | function Home() {
Condensed preview — 97 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (490K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 796,
    "preview": "# These are supported funding model platforms\n\ngithub: hiaaryan\npatreon: # Replace with a single Patreon username\nopen_c"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/report_issue.yml",
    "chars": 2668,
    "preview": "name: Bug Report 👾\ndescription: File a bug report.\ntitle: \"[Bug]: \"\nlabels: [\"bug\"]\nbody:\n  - type: markdown\n    attribu"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/request_feature.yml",
    "chars": 2214,
    "preview": "name: Feature Request 🌟\ndescription: Suggest a new feature or enhancement.\ntitle: \"[Feature]: \"\nlabels: [\"enhancement\"]\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 2690,
    "preview": "name: Release and Build\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  check-version-change:\n    runs-on: ubuntu-lates"
  },
  {
    "path": ".gitignore",
    "chars": 130,
    "preview": "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.productio"
  },
  {
    "path": ".prettierignore",
    "chars": 30,
    "preview": "build\ncoverage\napp\ndist\n.yarn\n"
  },
  {
    "path": ".prettierrc",
    "chars": 102,
    "preview": "{\n  \"plugins\": [\"prettier-plugin-tailwindcss\"],\n  \"tailwindConfig\": \"./renderer/tailwind.config.js\"\n}\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 5984,
    "preview": "<p align=\"center\">\n  <img src=\"https://github.com/playwora/wora/blob/main/renderer/public/github/Header.png?raw=true\" al"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 4023,
    "preview": "<p align=\"center\">\n  <img src=\"https://github.com/playwora/wora/blob/main/renderer/public/github/Header.png?raw=true\" al"
  },
  {
    "path": "LICENSE",
    "chars": 1061,
    "preview": "MIT License\n\nCopyright (c) 2024 Wora\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof th"
  },
  {
    "path": "README.md",
    "chars": 4330,
    "preview": "> [!IMPORTANT]  \n> There is a migrated version which is being built with tauri (rust 🦀). During this time contributions "
  },
  {
    "path": "components.json",
    "chars": 346,
    "preview": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {"
  },
  {
    "path": "electron-builder.yml",
    "chars": 956,
    "preview": "appId: com.wora.player\nproductName: Wora\ncopyright: Copyright © 2024 Aaryan Kapoor\ndirectories:\n  output: dist\n  buildRe"
  },
  {
    "path": "main/background.ts",
    "chars": 16441,
    "preview": "import path from \"path\";\nimport { Menu, Tray, app, dialog, ipcMain, shell } from \"electron\";\nimport serve from \"electron"
  },
  {
    "path": "main/helpers/create-window.ts",
    "chars": 2127,
    "preview": "import {\n  screen,\n  BrowserWindow,\n  BrowserWindowConstructorOptions,\n  Rectangle,\n} from \"electron\";\nimport Store from"
  },
  {
    "path": "main/helpers/db/connectDB.ts",
    "chars": 35428,
    "preview": "import { and, eq, like, sql, or, exists, isNotNull } from \"drizzle-orm\";\nimport { albums, songs, settings, playlistSongs"
  },
  {
    "path": "main/helpers/db/createDB.ts",
    "chars": 1192,
    "preview": "import Database from \"better-sqlite3\";\nimport { app } from \"electron\";\nimport path from \"path\";\n\nexport const sqlite = n"
  },
  {
    "path": "main/helpers/db/schema.ts",
    "chars": 2234,
    "preview": "import { integer, sqliteTable, text, blob } from \"drizzle-orm/sqlite-core\";\nimport { relations } from \"drizzle-orm\";\n\nex"
  },
  {
    "path": "main/helpers/index.ts",
    "chars": 33,
    "preview": "export * from \"./create-window\";\n"
  },
  {
    "path": "main/helpers/lastfm-service.ts",
    "chars": 19078,
    "preview": "import { ipcMain } from \"electron\";\nimport fetch from \"node-fetch\";\nimport * as crypto from \"crypto\";\nimport * as path f"
  },
  {
    "path": "main/preload.ts",
    "chars": 836,
    "preview": "import { contextBridge, ipcRenderer, IpcRendererEvent } from \"electron\";\n\nconst handler = {\n  send(channel: string, valu"
  },
  {
    "path": "package.json",
    "chars": 2772,
    "preview": "{\n  \"private\": true,\n  \"name\": \"wora\",\n  \"description\": \"🎧 A beautiful player for audiophiles.\",\n  \"version\": \"0.4.0-bet"
  },
  {
    "path": "renderer/components/ErrorBoundary.tsx",
    "chars": 2192,
    "preview": "import React, { Component, ErrorInfo, ReactNode } from 'react';\nimport { Button } from '@/components/ui/button';\nimport "
  },
  {
    "path": "renderer/components/LoadingSkeletons.tsx",
    "chars": 3269,
    "preview": "import React from 'react';\nimport { Skeleton } from '@/components/ui/skeleton';\n\nexport function ArtistGridSkeleton({ co"
  },
  {
    "path": "renderer/components/PageTransition.tsx",
    "chars": 989,
    "preview": "import { motion, AnimatePresence, Transition } from 'framer-motion';\nimport { useRouter } from 'next/router';\nimport { R"
  },
  {
    "path": "renderer/components/PageTransitionMinimal.tsx",
    "chars": 395,
    "preview": "import { ReactNode } from 'react';\n\ninterface PageTransitionProps {\n  children: ReactNode;\n}\n\n// Minimal approach: No tr"
  },
  {
    "path": "renderer/components/main/lyrics.tsx",
    "chars": 2293,
    "preview": "import { LyricLine } from \"@/lib/helpers\";\nimport React, { useEffect, useRef } from \"react\";\nimport { Badge } from \"../u"
  },
  {
    "path": "renderer/components/main/navbar.tsx",
    "chars": 13285,
    "preview": "import {\n  IconDeviceDesktop,\n  IconFocusCentered,\n  IconInbox,\n  IconList,\n  IconMoon,\n  IconSearch,\n  IconSun,\n  IconV"
  },
  {
    "path": "renderer/components/main/player.tsx",
    "chars": 44977,
    "preview": "import Image from \"next/image\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  IconArrowsShuffle2,\n  IconBr"
  },
  {
    "path": "renderer/components/themeProvider.tsx",
    "chars": 321,
    "preview": "\"use client\";\n\nimport * as React from \"react\";\nimport { ThemeProvider as NextThemesProvider } from \"next-themes\";\nimport"
  },
  {
    "path": "renderer/components/ui/actions.tsx",
    "chars": 2172,
    "preview": "import {\n  IconBox,\n  IconLine,\n  IconLineDashed,\n  IconSquare,\n  IconX,\n} from \"@tabler/icons-react\";\nimport Image from"
  },
  {
    "path": "renderer/components/ui/album.tsx",
    "chars": 1229,
    "preview": "import Image from \"next/image\";\nimport Link from \"next/link\";\nimport React from \"react\";\n\ntype Album = {\n  id: string;\n "
  },
  {
    "path": "renderer/components/ui/avatar.tsx",
    "chars": 1445,
    "preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\";\n\nimport { cn }"
  },
  {
    "path": "renderer/components/ui/badge.tsx",
    "chars": 1115,
    "preview": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \""
  },
  {
    "path": "renderer/components/ui/button.tsx",
    "chars": 1410,
    "preview": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"cla"
  },
  {
    "path": "renderer/components/ui/carousel.tsx",
    "chars": 6228,
    "preview": "\"use client\";\n\nimport * as React from \"react\";\nimport useEmblaCarousel, {\n  type UseEmblaCarouselType,\n} from \"embla-car"
  },
  {
    "path": "renderer/components/ui/command.tsx",
    "chars": 4767,
    "preview": "\"use client\";\n\nimport * as React from \"react\";\nimport { type DialogProps } from \"@radix-ui/react-dialog\";\nimport { Comma"
  },
  {
    "path": "renderer/components/ui/context-menu.tsx",
    "chars": 7653,
    "preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as ContextMenuPrimitive from \"@radix-ui/react-context-menu\";\n\nim"
  },
  {
    "path": "renderer/components/ui/dialog.tsx",
    "chars": 3560,
    "preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\n\nimport { cn }"
  },
  {
    "path": "renderer/components/ui/form.tsx",
    "chars": 4246,
    "preview": "import * as React from \"react\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { Slot } from \"@radix-ui"
  },
  {
    "path": "renderer/components/ui/input.tsx",
    "chars": 721,
    "preview": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface InputProps\n  extends React.InputHTM"
  },
  {
    "path": "renderer/components/ui/label.tsx",
    "chars": 734,
    "preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { cva, ty"
  },
  {
    "path": "renderer/components/ui/scroll-area.tsx",
    "chars": 1771,
    "preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\";\nimpor"
  },
  {
    "path": "renderer/components/ui/select.tsx",
    "chars": 4577,
    "preview": "import * as React from \"react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport {\n  IconCheck,\n  IconC"
  },
  {
    "path": "renderer/components/ui/skeleton.tsx",
    "chars": 283,
    "preview": "import { cn } from \"@/lib/utils\";\n\nfunction Skeleton({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) "
  },
  {
    "path": "renderer/components/ui/slider.tsx",
    "chars": 947,
    "preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SliderPrimitive from \"@radix-ui/react-slider\";\n\nimport { cn }"
  },
  {
    "path": "renderer/components/ui/songs.tsx",
    "chars": 16078,
    "preview": "import React, {\n  useEffect,\n  useState,\n  useCallback,\n  memo,\n  useMemo,\n  useRef,\n  forwardRef,\n  useImperativeHandle"
  },
  {
    "path": "renderer/components/ui/sonner.tsx",
    "chars": 1197,
    "preview": "\"use client\";\n\nimport { useTheme } from \"next-themes\";\nimport { Toaster as Sonner } from \"sonner\";\n\ntype ToasterProps = "
  },
  {
    "path": "renderer/components/ui/spinner.tsx",
    "chars": 1856,
    "preview": "import * as React from \"react\";\nimport { cn } from \"@/lib/utils\"; // Ensure the path to cn utility is correct\n\nexport in"
  },
  {
    "path": "renderer/components/ui/switch.tsx",
    "chars": 1173,
    "preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\";\n\nimport { cn "
  },
  {
    "path": "renderer/components/ui/tabs.tsx",
    "chars": 1972,
    "preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\n\nimport { cn } fro"
  },
  {
    "path": "renderer/components/ui/tooltip.tsx",
    "chars": 1162,
    "preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\n\nimport { cn"
  },
  {
    "path": "renderer/context/playerContext.tsx",
    "chars": 11654,
    "preview": "import { shuffleArray } from \"@/lib/helpers\";\nimport React, {\n  createContext,\n  useState,\n  useContext,\n  ReactNode,\n  "
  },
  {
    "path": "renderer/hooks/useDebounce.ts",
    "chars": 391,
    "preview": "import { useEffect, useState } from 'react';\n\nexport function useDebounce<T>(value: T, delay: number = 300): T {\n  const"
  },
  {
    "path": "renderer/hooks/useScrollAreaRestoration.ts",
    "chars": 6019,
    "preview": "import { useEffect, useRef } from 'react';\nimport { useRouter } from 'next/router';\n\nconst DEBUG = false;\nconst MAX_REST"
  },
  {
    "path": "renderer/lib/albumCache.ts",
    "chars": 9522,
    "preview": "// Global album cache to persist album data between page navigations\ninterface Album {\n  id: number;\n  name: string;\n  a"
  },
  {
    "path": "renderer/lib/apiConfig.ts",
    "chars": 494,
    "preview": "// Configure API URLs based on environment\nconst isDev = process.env.NODE_ENV === \"development\";\n\n// Your Vercel deploym"
  },
  {
    "path": "renderer/lib/helpers.ts",
    "chars": 5668,
    "preview": "import { Song } from \"@/context/playerContext\";\nimport axios from \"axios\";\nimport { IAudioMetadata } from \"music-metadat"
  },
  {
    "path": "renderer/lib/lastfm-client.ts",
    "chars": 8018,
    "preview": "import { Song } from \"@/context/playerContext\";\nimport { API_BASE_URL } from \"./apiConfig\";\n\n// Session information\nlet "
  },
  {
    "path": "renderer/lib/lastfm.ts",
    "chars": 8422,
    "preview": "// Last.fm Client for Electron - uses IPC to securely communicate with the backend API\n\n// Types\ninterface Song {\n  id?:"
  },
  {
    "path": "renderer/lib/songCache.ts",
    "chars": 6992,
    "preview": "// Global song cache to persist song data between page navigations\nimport { Song } from \"@/context/playerContext\";\n\n// D"
  },
  {
    "path": "renderer/lib/utils.ts",
    "chars": 169,
    "preview": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: C"
  },
  {
    "path": "renderer/next-env.d.ts",
    "chars": 213,
    "preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edite"
  },
  {
    "path": "renderer/next.config.js",
    "chars": 429,
    "preview": "/** @type {import('next').NextConfig} */\n\nmodule.exports = {\n  // Use output: 'export' since we're integrating with Elec"
  },
  {
    "path": "renderer/pages/_app.tsx",
    "chars": 2889,
    "preview": "import \"@/styles/globals.css\";\nimport Actions from \"@/components/ui/actions\";\nimport Navbar from \"@/components/main/navb"
  },
  {
    "path": "renderer/pages/albums/[slug].tsx",
    "chars": 4714,
    "preview": "import React, { useEffect, useState, useRef } from \"react\";\nimport Image from \"next/image\";\nimport { useRouter } from \"n"
  },
  {
    "path": "renderer/pages/albums.tsx",
    "chars": 25346,
    "preview": "import React, { useEffect, useState, useCallback, useRef } from \"react\";\nimport { useScrollAreaRestoration } from \"@/hoo"
  },
  {
    "path": "renderer/pages/artists/[name].tsx",
    "chars": 24055,
    "preview": "import React, { useEffect, useState, useRef } from \"react\";\nimport { useRouter } from \"next/router\";\nimport Image from \""
  },
  {
    "path": "renderer/pages/artists/index.tsx",
    "chars": 17248,
    "preview": "import React, { useEffect, useState, useMemo, useCallback, memo } from \"react\";\nimport { useRouter } from \"next/router\";"
  },
  {
    "path": "renderer/pages/home.tsx",
    "chars": 4229,
    "preview": "import React, { useEffect, useState, useRef } from \"react\";\nimport {\n  Carousel,\n  CarouselContent,\n  CarouselItem,\n  Ca"
  },
  {
    "path": "renderer/pages/playlists/[slug].tsx",
    "chars": 12167,
    "preview": "import React, { useEffect, useState } from \"react\";\nimport Image from \"next/image\";\nimport { useRouter } from \"next/rout"
  },
  {
    "path": "renderer/pages/playlists.tsx",
    "chars": 8506,
    "preview": "\"use client\";\nimport React, { useEffect, useState } from \"react\";\nimport Image from \"next/image\";\nimport Link from \"next"
  },
  {
    "path": "renderer/pages/settings.tsx",
    "chars": 27266,
    "preview": "import React, { useEffect, useState, useRef } from \"react\";\nimport {\n  IconArrowRight,\n  IconBrandLastfm,\n  IconCheck,\n "
  },
  {
    "path": "renderer/pages/setup.tsx",
    "chars": 2277,
    "preview": "import Actions from \"@/components/ui/actions\";\nimport { Button } from \"@/components/ui/button\";\nimport Image from \"next/"
  },
  {
    "path": "renderer/pages/songs.tsx",
    "chars": 18863,
    "preview": "import React, { useEffect, useState, useCallback, useRef } from \"react\";\nimport { useScrollAreaRestoration } from \"@/hoo"
  },
  {
    "path": "renderer/postcss.config.js",
    "chars": 72,
    "preview": "module.exports = {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n"
  },
  {
    "path": "renderer/preload.d.ts",
    "chars": 112,
    "preview": "import { IpcHandler } from \"../main/preload\";\n\ndeclare global {\n  interface Window {\n    ipc: IpcHandler;\n  }\n}\n"
  },
  {
    "path": "renderer/styles/globals.css",
    "chars": 2096,
    "preview": "@import url(\"https://fonts.googleapis.com/css2?family=Maven+Pro:wght@400..900&display=swap\")\nlayer(base);\n\n@import \"tail"
  },
  {
    "path": "renderer/tsconfig.json",
    "chars": 235,
    "preview": "{\n  \"extends\": \"../tsconfig.json\",\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"excl"
  },
  {
    "path": "tsconfig.json",
    "chars": 569,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"sk"
  },
  {
    "path": "vercel/next-env.d.ts",
    "chars": 201,
    "preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edite"
  },
  {
    "path": "vercel/next.config.js",
    "chars": 200,
    "preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: true,\n  swcMinify: true,\n  images: {\n  "
  },
  {
    "path": "vercel/package.json",
    "chars": 403,
    "preview": "{\n  \"name\": \"wora-api\",\n  \"version\": \"0.4.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"nex"
  },
  {
    "path": "vercel/pages/_app.tsx",
    "chars": 150,
    "preview": "import type { AppProps } from \"next/app\";\n\nexport default function App({ Component, pageProps }: AppProps) {\n  return <C"
  },
  {
    "path": "vercel/pages/api/config.ts",
    "chars": 290,
    "preview": "// Configuration for Last.fm API\n// These credentials are stored on the server side and not exposed to clients\nexport co"
  },
  {
    "path": "vercel/pages/api/index.ts",
    "chars": 402,
    "preview": "import { NextApiRequest, NextApiResponse } from \"next\";\n\nexport default function handler(req: NextApiRequest, res: NextA"
  },
  {
    "path": "vercel/pages/api/lastfm/auth.ts",
    "chars": 2262,
    "preview": "import { NextApiRequest, NextApiResponse } from \"next\";\nimport { LASTFM_CONFIG } from \"../config\";\nimport { createFormDa"
  },
  {
    "path": "vercel/pages/api/lastfm/now-playing.ts",
    "chars": 1997,
    "preview": "import { NextApiRequest, NextApiResponse } from \"next\";\nimport { LASTFM_CONFIG } from \"../config\";\nimport { createFormDa"
  },
  {
    "path": "vercel/pages/api/lastfm/scrobble.ts",
    "chars": 2062,
    "preview": "import { NextApiRequest, NextApiResponse } from \"next\";\nimport { LASTFM_CONFIG } from \"../config\";\nimport { createFormDa"
  },
  {
    "path": "vercel/pages/api/lastfm/track-info.ts",
    "chars": 1708,
    "preview": "import { NextApiRequest, NextApiResponse } from \"next\";\nimport { LASTFM_CONFIG } from \"../config\";\n\nexport default async"
  },
  {
    "path": "vercel/pages/api/lastfm/user-info.ts",
    "chars": 2568,
    "preview": "import { NextApiRequest, NextApiResponse } from \"next\";\nimport { LASTFM_CONFIG } from \"../config\";\nimport { createFormDa"
  },
  {
    "path": "vercel/pages/api/utils/lastfm.ts",
    "chars": 1333,
    "preview": "import * as crypto from \"crypto\";\nimport { LASTFM_CONFIG } from \"../config\";\n\n/**\n * Generate MD5 hash for password auth"
  },
  {
    "path": "vercel/pages/index.tsx",
    "chars": 1132,
    "preview": "import React from \"react\";\n\nexport default function Home() {\n  return (\n    <div\n      style={{\n        padding: \"20px\","
  },
  {
    "path": "vercel/tsconfig.json",
    "chars": 575,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es2015\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    "
  },
  {
    "path": "vercel/vercel.json",
    "chars": 288,
    "preview": "{\n  \"framework\": \"nextjs\",\n  \"outputDirectory\": \".next\",\n  \"buildCommand\": \"npm run build\",\n  \"devCommand\": \"npm run dev"
  }
]

// ... and 2 more files (download for full content)

About this extraction

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

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

Copied to clipboard!