Full Code of iAJue/MoeKoeMusic for AI

main 88847bf44adf cached
98 files
893.1 KB
262.5k tokens
94 symbols
1 requests
Download .txt
Showing preview only (1,017K chars total). Download the full file or copy to clipboard to get everything.
Repository: iAJue/MoeKoeMusic
Branch: main
Commit: 88847bf44adf
Files: 98
Total size: 893.1 KB

Directory structure:
gitextract_l9rjb4a1/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   ├── discussion.yml
│   │   └── feature_request.yml
│   ├── PULL_REQUEST_TEMPLATE/
│   │   ├── bug_fix.md
│   │   ├── docs_update.md
│   │   └── feature_request.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── SECURITY.md
│   └── workflows/
│       ├── AiIssueCheck.yml
│       ├── AutoCloseIssues.yml
│       ├── release.yml
│       └── version.yml
├── .gitignore
├── .gitmodules
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── build/
│   ├── icons/
│   │   └── icon.icns
│   ├── installer.nsh
│   └── license.txt
├── docker-compose.yml
├── docs/
│   ├── README_en.md
│   ├── README_ja.md
│   ├── README_ko.md
│   ├── README_ru.md
│   └── README_tw.md
├── electron/
│   ├── appServices.js
│   ├── extensions/
│   │   ├── extensionIPC.js
│   │   ├── extensionManager.js
│   │   └── extensions.js
│   ├── language/
│   │   └── i18n.js
│   ├── main.js
│   ├── preload.cjs
│   └── services/
│       ├── apiService.js
│       ├── externalLinkHandler.js
│       ├── statusBarLyricsService.js
│       └── updater.js
├── index.html
├── nginx.conf
├── package.json
└── src/
    ├── App.vue
    ├── assets/
    │   ├── style/
    │   │   └── PlayerControl.css
    │   └── themes/
    │       └── dark.css
    ├── components/
    │   ├── AlbumGrid.vue
    │   ├── ArtistGrid.vue
    │   ├── BirthdayEasterEgg.vue
    │   ├── ContextMenu.vue
    │   ├── CustomModal.vue
    │   ├── Disclaimer.vue
    │   ├── ExtensionManager.vue
    │   ├── Header.vue
    │   ├── MessageNotification.vue
    │   ├── PlayerControl.vue
    │   ├── PlaylistGrid.vue
    │   ├── PlaylistSelectModal.vue
    │   ├── QueueList.vue
    │   ├── StatusBarLyrics.vue
    │   ├── TitleBar.vue
    │   └── player/
    │       ├── AudioController.js
    │       ├── Helpers.js
    │       ├── LyricsHandler.js
    │       ├── MediaSession.js
    │       ├── PlaybackMode.js
    │       ├── ProgressBar.js
    │       ├── SongQueue.js
    │       └── index.js
    ├── language/
    │   ├── en.json
    │   ├── ja.json
    │   ├── ko.json
    │   ├── ru.json
    │   ├── zh-CN.json
    │   └── zh-TW.json
    ├── layouts/
    │   └── HomeLayout.vue
    ├── main.js
    ├── plugins/
    │   ├── MessagePlugin.js
    │   └── ModalPlugin.js
    ├── router/
    │   └── router.js
    ├── stores/
    │   ├── musicQueue.js
    │   └── store.js
    ├── utils/
    │   ├── apiBaseUrl.js
    │   ├── i18n.js
    │   ├── request.js
    │   └── utils.js
    └── views/
        ├── CloudDrive.vue
        ├── Discover.vue
        ├── Home.vue
        ├── Library.vue
        ├── LocalMusic.vue
        ├── Login.vue
        ├── Lyrics.vue
        ├── PlaylistDetail.vue
        ├── Ranking.vue
        ├── Search.vue
        ├── Settings.vue
        └── VideoPlayer.vue

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

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: "🐞 Bug报告"
description: 创建一个报告来帮助我们改进产品
title: '[Bug]: '
labels: ['bug']
body:
  - type: markdown
    attributes:
      value: |
        **在开始之前...**

        此表单仅用于提交Bug报告。如果您有使用问题或不确定这是否真的是一个Bug,请确保:

        - 阅读[文档](https://music.moekoe.cn/)
        - 搜索是否有类似的问题 - 它可能已经被回答或修复
        - 提供有效的 Bug 描述和明确的问题内容,否则 issue 将可能被直接关闭或忽略处理

        如果您发现一个旧的、已关闭的问题在最新版本中仍然存在,请使用下面的表单打开一个新问题。
  - type: input
    id: version
    attributes:
      label: 产品版本
      placeholder: 例如:@ MoeKoe Music V1.4.3 - darwin
    validations:
      required: true
  - type: textarea
    id: steps-to-reproduce
    attributes:
      label: 复现步骤
      description: |
        我们需要做什么才能复现这个bug?请提供清晰简洁的复现说明,这对我们及时分类您的问题很重要。
      placeholder: |
        1. 打开...
        2. 点击...
        3. 滚动到...
        4. 查看错误
    validations:
      required: true
  - type: textarea
    id: expected
    attributes:
      label: 预期行为
      description: 您期望看到什么?
    validations:
      required: true
  - type: textarea
    id: actually-happening
    attributes:
      label: 实际行为
      description: 实际发生了什么?
    validations:
      required: true
  - type: textarea
    id: system-info
    attributes:
      label: 系统信息
      description: 操作系统、网络环境、设备等
      placeholder: |
        - 操作系统: [例如 Windows 10, macOS 12.0, Linux]
        - 网络环境: [例如 中国, 日本, 移动, WiFi]
        - 设备信息: [例如 笔记本, GTX1060, 16G, 台式机]
  - type: textarea
    id: additional-comments
    attributes:
      label: 其他补充说明
      description: 例如:一些关于您如何遇到这个bug的背景/上下文。

================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
  - name: 🌈 社区交流
    url: https://github.com/iAJue/MoeKoeMusic/discussions
    about: 加入我们的社区进行交流和讨论
  - name: 📚 项目文档
    url: https://music.moekoe.cn/
    about: 查看项目文档以获取更多帮助信息
  - name: 🛠️ 开发与编译
    url: https://github.com/iAJue/MoeKoeMusic?tab=readme-ov-file#%EF%B8%8F-%E5%BC%80%E5%8F%91
    about: 了解项目如何进行开发和编译
  - name: 🤝 贡献指南
    url: https://github.com/iAJue/MoeKoeMusic/blob/main/CONTRIBUTING.md
    about: 了解如何为项目做出贡献

================================================
FILE: .github/ISSUE_TEMPLATE/discussion.yml
================================================
name: "💬 讨论问题"
description: 提出一个需要讨论的话题或问题
title: '[讨论]: '
labels: ['question']
body:
  - type: markdown
    attributes:
      value: |
        **欢迎参与讨论!**
        
        这个模板适用于那些不属于Bug报告或新特性请求的讨论话题。
        例如:设计决策、架构问题、最佳实践问题等。
        
        在开始之前,请确保:
        
        - 您已经搜索过现有的讨论,避免重复
        - 您的问题足够清晰,以便其他人能够理解和参与讨论
        
        或前往 [社区](https://github.com/iAJue/MoeKoeMusic/discussions) 进行讨论
  - type: textarea
    id: topic
    attributes:
      label: 讨论主题
      description: 请简明扼要地描述您想讨论的主题
      placeholder: 我想讨论关于...
    validations:
      required: true
  - type: textarea
    id: context
    attributes:
      label: 背景和上下文
      description: 请提供一些背景信息,帮助其他人理解为什么这个话题值得讨论
      placeholder: |
        这个话题在以下情况下很重要...
        我遇到了以下挑战...
    validations:
      required: true
  - type: textarea
    id: questions
    attributes:
      label: 关键问题
      description: 您希望通过此讨论解答哪些问题?
      placeholder: |
        1. 我们应该如何处理...?
        2. 什么是...的最佳实践?
        3. 社区对...有什么看法?
    validations:
      required: true
  - type: textarea
    id: proposed-ideas
    attributes:
      label: 您的想法
      description: 您对这个话题有什么想法或建议?分享您的初步思考
      placeholder: 我认为我们可以...
    validations:
      required: false
  - type: dropdown
    id: topic-area
    attributes:
      label: 话题领域
      description: 这个讨论主要涉及哪个领域?
      options:
        - 架构设计
        - 用户体验
        - 性能优化
        - 开发流程
        - 文档改进
        - 社区建设
        - 其他
    validations:
      required: false
  - type: textarea
    id: additional-info
    attributes:
      label: 补充信息
      description: 还有什么其他信息可以帮助丰富这次讨论?
      placeholder: 相关资源、链接、截图等...
    validations:
      required: false 

================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: "✨ 新特性请求"
description: 为项目提出一个新想法或建议
title: '[新需求]: '
labels: ['enhancement']
body:
  - type: markdown
    attributes:
      value: |
        **感谢您的新特性建议!**

        请花点时间填写以下表单,以便我们更好地理解您的需求。
        在提交之前,请确保:
        
        - 搜索现有的issues和讨论,确保这个特性尚未被提出
        - 确认这是一个新特性而不是bug修复
        - 有完整的上下文链,帮助我们评估是否是值得添加的特性
  - type: textarea
    id: problem-description
    attributes:
      label: 问题描述
      description: 您想解决什么问题?请简明扼要地描述您遇到的问题或痛点。
      placeholder: 我在使用产品时遇到的问题是...
    validations:
      required: true
  - type: textarea
    id: solution-description
    attributes:
      label: 解决方案描述
      description: 您建议如何解决这个问题?请描述您期望的功能或改进。
      placeholder: 我希望产品能够...
    validations:
      required: true
  - type: textarea
    id: alternatives
    attributes:
      label: 替代方案
      description: 您考虑过哪些替代解决方案或功能?
      placeholder: 我也考虑过通过...来解决这个问题
    validations:
      required: false
  - type: textarea
    id: additional-context
    attributes:
      label: 其他上下文
      description: 您还有什么其他信息、截图或示例可以帮助我们更好地理解这个特性请求?
      placeholder: 其他相关信息...
    validations:
      required: false
  - type: dropdown
    id: importance
    attributes:
      label: 重要程度
      description: 您认为这个特性对您使用产品的重要程度如何?
      options:
        - 必需(无法没有它)
        - 重要(显著改善体验)
        - 一般(有帮助但不关键)
        - 微小(锦上添花)
    validations:
      required: false

================================================
FILE: .github/PULL_REQUEST_TEMPLATE/bug_fix.md
================================================
---
name: 🐞 Bug Fix | 修复 Bug
about: 修复现有的问题或异常
title: 'fix: 修复 [问题名称]'
labels: bug
---

## 🐛 修复了什么?What is fixed?

请简要说明修复的 Bug 内容:
> Describe the bug that has been fixed.

---

## 🔍 修复方法 How was it fixed?

> 简述你是如何定位并解决该问题的。
Explain your approach or steps taken to fix the issue.

---

## ✅ 测试验证 How did you test?

- [ ] 本地测试通过 Passed local tests
- [ ] 已测试关键路径功能 Tested critical paths
- [ ] 附加截图或日志(如适用)Attached screenshot/logs (if applicable)

---

## 🔗 相关 Issues / Related Issues

> Closes #xxx 或 Related to #xxx


================================================
FILE: .github/PULL_REQUEST_TEMPLATE/docs_update.md
================================================
---
name: 📝 Docs Update | 文档更新
about: 提交文档相关的修改
title: 'docs: 更新 [文档主题]'
labels: documentation
---

## 📄 更新内容 Description

请说明你更新了哪些文档,以及为什么更新它们:
> Describe what parts of the documentation were updated and why.

---

## 📌 文档类型 Type of Docs

- [ ] 使用说明 Usage guide
- [ ] 开发者文档 Developer guide
- [ ] API 文档 API references
- [ ] 项目规范 Project rules
- [ ] 其他 Other: _________

---

## ✅ 其他说明 Notes

> 是否与代码变更有关?是否同步更新了?
Mention if this change is associated with any code changes or dependencies.

---

## 🔗 相关 Issues / Related Issues

> Ref #xxx(如有关联)


================================================
FILE: .github/PULL_REQUEST_TEMPLATE/feature_request.md
================================================
---
name: ✨ New Feature | 新功能
about: 实现一个新功能或对现有功能进行增强
title: 'feat: 实现 [功能名称]'
labels: enhancement
---

## 🌟 实现了什么?What is added or changed?

请说明新增了哪些功能、修改了哪些行为:
> Describe the new functionality or changes introduced.

---

## 🎯 为什么需要这个改动?Why is it needed?

> 请说明这个功能的背景、价值或需求来源。
Explain the motivation behind this feature or change.

---

## 🧪 如何测试?How to test?

- [ ] 添加了单元测试 Added unit tests
- [ ] 手动验证通过 Verified manually
- [ ] 不需要额外测试 No additional test needed

---

## 🔗 相关 Issues / Related Issues

> Resolves #xxx 或 Implements #xxx


================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
## ✨ 变更类型 Type of Change

- [ ] Bug 修复 (fix)
- [ ] 新功能 (feat)
- [ ] 文档更新 (docs)
- [ ] 样式调整 (style)
- [ ] 重构 (refactor)
- [ ] 测试相关 (test)
- [ ] 构建/工具 (chore)
- [ ] 其他 (other):

---

## 📋 变更描述 Description

请简要描述此次变更内容:
> Describe your changes here.

---

## 🐛 如果是 Bug 修复,请描述问题和修复方法 Bug Fix

- 问题是什么?What was the problem?
- 如何修复的?How was it fixed?

---

## ✅ 测试验证 How did you test?

- [ ] 本地测试通过 Passed local tests
- [ ] 关键功能测试 Tested critical features
- [ ] 其他测试描述 (如自动化测试、手动测试等):
> Please describe testing details

---

## 📚 相关 Issues / Related Issues

> Closes #xxx 或 Related to #xxx

---

## 📷 截图 / Screenshots (如果适用)

> 如果有界面变动,附上截图或录屏


================================================
FILE: .github/SECURITY.md
================================================
# Security Policy

## Supported Versions

We actively maintain security updates for the following versions:

| Version | Supported          |
|---------|--------------------|
| 1.x     | ✅                 |
| 0.x     | ❌ (no longer supported) |

## Reporting a Vulnerability

If you discover a security vulnerability in this project, please follow these steps:

1. **Do not open a public issue.**  
   Please email us **privately** to allow time for remediation before public disclosure.

2. **Contact:**
   - 📧 Email: [MoeJue@qq.com](mailto:MoeJue@qq.com)
   - 🕒 Expected Response Time: 1–3 business days

3. **Information to include in your report:**
   - A clear and detailed description of the vulnerability
   - Steps to reproduce the issue
   - Potential impact or severity
   - (Optional) Any suggested fix or patch

4. **Responsible Disclosure Policy:**
   We believe in responsible disclosure and commit to:
   - Confirming and triaging valid reports promptly
   - Patching and releasing a fix within **30 days**
   - Crediting the reporter in the changelog (with consent)

## Security Best Practices (for users)

To keep your environment safe, we recommend:
- Always use the latest stable version
- Avoid using deprecated versions
- Keep your dependencies updated regularly
- Use a secure environment (e.g. HTTPS, proper file permissions)

---

🔐 Thank you for helping us keep our project safe and secure!


================================================
FILE: .github/workflows/AiIssueCheck.yml
================================================
name: AI Issue Checker

on:
  issues:
    types: [opened, edited] 

permissions:
  issues: write
  pull-requests: write

jobs:
  check-issue:
    if: |
      github.event_name == 'issues' &&
      (
        github.event.action == 'opened' ||
        (
          github.event.action == 'edited' &&
          github.event.issue.state == 'open' &&
          contains(github.event.issue.labels.*.name, 'invalid')
        )
      )
    runs-on: ubuntu-latest
    steps:
      - name: Debug API Key existence
        run: |
          if [ -z "${{ secrets.OPENAI_API_KEY }}" ]; then
            echo "❌ OPENAI_API_KEY is NOT set"
            exit 1
          else
            echo "✅ OPENAI_API_KEY is set"
            echo "API Key length: ${#OPENAI_API_KEY}"
          fi
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

      - name: Run AI Issue Validation
        uses: actions/github-script@v6
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          ISSUE_RULES: ${{ vars.ISSUE_RULES }}
          OPENAI_API_URL: ${{ secrets.OPENAI_API_URL }}
        with:
          script: |
            const issue = context.payload.issue
            const issueBody = issue.body || ""
            const rules = process.env.ISSUE_RULES

            const response = await fetch(`${process.env.OPENAI_API_URL}`, {
              method: "POST",
              headers: {
                "Content-Type": "application/json",
                "Authorization": `Bearer ${process.env.OPENAI_API_KEY}`
              },
              body: JSON.stringify({
                model: "gpt-4o-mini",
                messages: [
                  { role: "system", content: rules },
                  { role: "user", content: issueBody }
                ],
                max_tokens: 200
              })
            })

            const data = await response.json()
            let result
            try {
              result = JSON.parse(data.choices[0].message.content)
            } catch (e) {
              result = { valid: true, missing: [] }
            }

            const { data: comments } = await github.rest.issues.listComments({
              issue_number: issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo
            })

            const botComment = comments.find(c => c.user.type === "Bot" && c.body.includes("感谢提交 Issue"))

            if (!result.valid) {
              const missingText = result.missing.join("、")
              const reasonText = result.reason
              const newBody = `⚠️ 感谢提交 Issue,但你的内容缺少以下部分:**${missingText}**。\n\n**${reasonText}**\n\n请根据模板补充完整后再提交,谢谢!`

              if (botComment) {
                await github.rest.issues.updateComment({
                  comment_id: botComment.id,
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  body: newBody
                })
              } else {
                await github.rest.issues.createComment({
                  issue_number: issue.number,
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  body: newBody
                })
              }

              await github.rest.issues.addLabels({
                issue_number: issue.number,
                owner: context.repo.owner,
                repo: context.repo.repo,
                labels: ["invalid"]
              })
            } else {
              const passBody = `✅ 感谢提交 Issue,你的 Issue 已经符合要求了,我们会尽快处理~\n\n💖 听说点了Star许愿的成功率更高哦~`

              if (botComment) {
                await github.rest.issues.updateComment({
                  comment_id: botComment.id,
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  body: passBody
                })
              } else {
                await github.rest.issues.createComment({
                  issue_number: issue.number,
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  body: passBody
                })
              }

              try {
                await github.rest.issues.removeLabel({
                  issue_number: issue.number,
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  name: "invalid"
                })
              } catch (e) {
              }
            }


================================================
FILE: .github/workflows/AutoCloseIssues.yml
================================================
name: Auto Close Invalid Issues

on:
  schedule:
    - cron: "0 3 * * *"   # 每天 UTC 03:00 运行一次
  workflow_dispatch:

permissions:
  issues: write

jobs:
  close-invalid:
    runs-on: ubuntu-latest
    steps:
      - name: Close invalid issues
        uses: actions/github-script@v6
        with:
          script: |
            const { data: issues } = await github.rest.issues.listForRepo({
              owner: context.repo.owner,
              repo: context.repo.repo,
              state: "open",
              labels: "invalid",
              per_page: 100
            })

            const now = new Date()
            for (const issue of issues) {
              const updatedAt = new Date(issue.updated_at)
              const hoursSinceUpdate = (now - updatedAt) / (1000 * 60 * 60)

              if (hoursSinceUpdate > 24) {
                console.log(`Closing issue #${issue.number}`)

                const { data: comments } = await github.rest.issues.listComments({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  issue_number: issue.number
                })

                const botComment = comments.find(c => c.user.type === "Bot")

                const message = "⏳ 由于此 Issue 已标记为 **invalid** 并且超过 24 小时未更新,现自动关闭。"

                if (botComment) {
                  await github.rest.issues.updateComment({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    comment_id: botComment.id,
                    body: message
                  })
                } else {
                  await github.rest.issues.createComment({
                    owner: context.repo.owner,
                    repo: context.repo.repo,
                    issue_number: issue.number,
                    body: message
                  })
                }

                await github.rest.issues.update({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  issue_number: issue.number,
                  state: "closed"
                })
              }
            }

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

on:
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  contents: write

jobs:
  # 创建版本号
  increment-version:
    if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && contains(github.event.head_commit.message, 'release'))
    name: Increment Version
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.set-version.outputs.NEW_VERSION }}
      body: ${{ steps.create-release.outputs.RELEASE_BODY }}
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
          submodules: recursive
      - name: Increment version
        id: set-version
        run: |
          VERSION=$(git tag --sort=-v:refname | head -n 1)
          MAJOR=$(echo $VERSION | awk -F. '{print $1}' | sed 's/v//')
          MINOR=$(echo $VERSION | awk -F. '{print $2}')
          PATCH=$(echo $VERSION | awk -F. '{print $3}')
          PATCH=$((PATCH + 1))
          if [ "$PATCH" -gt 9 ]; then
            PATCH=0
            MINOR=$((MINOR + 1))
          fi
          NEW_VERSION="v$MAJOR.$MINOR.$PATCH"
          echo "::set-output name=NEW_VERSION::$NEW_VERSION"

      - name: Create release body
        id: create-release
        run: |
          # 获取上一个tag
          PREV_TAG=$(git describe --tags --abbrev=0)
          # 获取最后一个tag到现在的所有commit信息
          COMMITS=$(git log ${PREV_TAG}..HEAD --pretty=format:"* %s")
          # 如果没有commit信息,使用默认消息
          if [ -z "$COMMITS" ]; then
            COMMITS="* Regular update and bug fixes"
          fi
          # 创建changelog链接
          CHANGELOG_LINK="https://github.com/${GITHUB_REPOSITORY}/compare/${PREV_TAG}...${{ steps.set-version.outputs.NEW_VERSION }}"
          # 创建release body
          RELEASE_BODY="## What's Changed\n\n${COMMITS}\n\n**Full Changelog**: [${PREV_TAG}...${{ steps.set-version.outputs.NEW_VERSION }}](${CHANGELOG_LINK})"
          # 将release body转义后输出
          echo "RELEASE_BODY<<EOF" >> $GITHUB_OUTPUT
          echo -e "$RELEASE_BODY" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ steps.set-version.outputs.NEW_VERSION }}
          token: ${{ secrets.GITHUB_TOKEN }}
          name: "Release ${{ steps.set-version.outputs.NEW_VERSION }}"
          body: ${{ steps.create-release.outputs.RELEASE_BODY }}
          prerelease: false
          draft: false


  build-and-upload:
    name: Build and Upload
    needs: increment-version
    strategy:
      matrix:
        node: [lts/*]
        os: [ubuntu-latest, windows-latest, macos-latest]
    runs-on: ${{ matrix.os }}
    timeout-minutes: 30
    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          submodules: true

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: ${{ matrix.node }}
          cache: 'npm' # use cache

      - name: Build
        env:
          os: ${{ runner.os == 'Windows' && 'win' || runner.os == 'macOS' && 'macos' || 'linux' }}
        run: |
          npm run install-all
          npm run build
          npm run electron:build:${{ env.os }}

      - name: Rename files - win
        if: runner.os == 'Windows'
        run: |
          mv "./dist_electron/MoeKoe_Music_Setup_${{ needs.increment-version.outputs.version }}.exe" "./MoeKoe_Music_Setup_${{ needs.increment-version.outputs.version }}.exe"
          mv "./dist_electron/MoeKoe_Music_Setup_${{ needs.increment-version.outputs.version }}.exe.blockmap" "./MoeKoe_Music_Setup_${{ needs.increment-version.outputs.version }}.exe.blockmap"
          mv "./dist_electron/latest.yml" "./latest.yml"
      - name: Rename files - mac
        if: runner.os == 'macOS'
        run: |
          mv "./dist_electron/MoeKoe Music-arm64.dmg" "./MoeKoe_Music_${{ needs.increment-version.outputs.version }}-arm64.dmg"
          mv "./dist_electron/MoeKoe Music-x64.dmg" "./MoeKoe_Music_${{ needs.increment-version.outputs.version }}-x64.dmg"
      - name: Rename files - linux
        if: runner.os == 'Linux'
        run: |
          mv "./dist_electron/MoeKoe Music.AppImage" "./MoeKoe_Music_${{ needs.increment-version.outputs.version }}.AppImage"
          mv "./dist_electron/MoeKoe Music.deb" "./MoeKoe_Music_${{ needs.increment-version.outputs.version }}_amd64.deb"

      - name: Upload to Release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ needs.increment-version.outputs.version }}
          files: |
            ./MoeKoe_Music_Setup_${{ needs.increment-version.outputs.version }}.exe
            ./MoeKoe_Music_Setup_${{ needs.increment-version.outputs.version }}.exe.blockmap
            ./latest.yml

            ./MoeKoe_Music_${{ needs.increment-version.outputs.version }}-arm64.dmg
            ./MoeKoe_Music_${{ needs.increment-version.outputs.version }}-x64.dmg

            ./MoeKoe_Music_${{ needs.increment-version.outputs.version }}.AppImage
            ./MoeKoe_Music_${{ needs.increment-version.outputs.version }}_amd64.deb


================================================
FILE: .github/workflows/version.yml
================================================
name: Version Check

on:
  workflow_dispatch:

permissions:
  contents: read

jobs:
  # 计算版本号
  check-version:
    if: github.event_name == 'workflow_dispatch'
    name: Check Next Version
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
      
      - name: Get latest tag
        run: |
          echo "Latest tag: $(git tag --sort=-v:refname | head -n 1)"
          
      - name: Calculate next version
        id: set-version
        run: |
          VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "v1.2.0")
          MAJOR=$(echo $VERSION | awk -F. '{print $1}' | sed 's/v//')
          MINOR=$(echo $VERSION | awk -F. '{print $2}')
          PATCH=$(echo $VERSION | awk -F. '{print $3}')
          PATCH=$((PATCH + 1))
          if [ "$PATCH" -gt 9 ]; then
            PATCH=0
            MINOR=$((MINOR + 1))
          fi
          NEW_VERSION="v$MAJOR.$MINOR.$PATCH"
          echo "::set-output name=NEW_VERSION::$NEW_VERSION"
          
      - name: Get previous tag
        run: |
          PREV_TAG=$(git describe --tags --abbrev=0)
          echo "Previous version: $PREV_TAG"
          
      - name: Display version information
        run: |
          echo "Current version: $(git describe --tags --abbrev=0)"
          echo "Next version will be: ${{ steps.set-version.outputs.NEW_VERSION }}"
          echo "Changes since last tag:"
          git log $(git describe --tags --abbrev=0)..HEAD --pretty=format:"* %s"
        


================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
.history

node_modules
dist_electron
dist
dist-ssr
*.local
test
bin/*
.serena/*
api/.serena/*

# Editor directories and files
.codebuddy/*
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
dev-dist/*
pnpm-lock.yaml
pnpm-workspace.yaml
vite.config.js*

plugins/extensions/*

================================================
FILE: .gitmodules
================================================
[submodule "api"]
	path = api
	url = https://github.com/MakcRe/KuGouMusicApi


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# 贡献者公约

## 我们的承诺

为了营造一个开放和友好的环境,作为贡献者和维护者,我们承诺让每个人都能参与我们的项目和社区,享受无骚扰的体验,无论年龄、体型、残疾、种族、性别特征、性别认同和表达、经验水平、教育、社会经济地位、国籍、个人外貌、种族、宗教或性别认同和取向如何。

## 我们的标准

有助于创造积极环境的行为包括:

- 使用友好和包容的语言
- 尊重不同的观点和经验
- 优雅地接受建设性的批评
- 关注对社区最有利的事情
- 对其他社区成员表示同理心

不可接受的行为包括:

- 使用性暗示的语言或图像
- 发表侮辱性或贬损性的评论
- 公开或私下的骚扰
- 未经明确许可发布他人的私人信息
- 其他不道德或专业上不适当的行为

## 我们的责任

项目维护者有责任澄清可接受行为的标准,并应对任何不可接受的行为采取适当和公平的纠正措施。

项目维护者有权和责任删除、编辑或拒绝不符合本行为准则的评论、提交、代码、wiki编辑、问题和其他贡献,或暂时或永久地禁止任何贡献者进行其他他们认为不适当、威胁、冒犯或有害的行为。

## 适用范围

本行为准则适用于项目空间和公共空间,当个人代表项目或其社区时。代表项目或社区的例子包括使用官方项目电子邮件地址、通过官方社交媒体账号发布信息,或在线上或线下活动中作为指定代表。项目维护者可以进一步定义和澄清代表项目的情况。

## 执行

可以通过联系项目团队来举报虐待、骚扰或其他不可接受的行为。所有投诉都将被审查和调查,并会做出必要和适当的回应。项目团队有义务对事件报告者保密。

不遵守或不执行行为准则的项目维护者可能会面临项目领导层决定的临时或永久性后果。

## 归属

本行为准则改编自[贡献者公约][homepage],版本 1.4,可在 https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 查看

[homepage]: https://www.contributor-covenant.org

## 翻译

本行为准则有多个语言版本。请访问 https://www.contributor-covenant.org/translations 查看其他语言版本。 

================================================
FILE: CONTRIBUTING.md
================================================
# 贡献指南

感谢您考虑为我们的项目做出贡献!无论是报告bug、提出新功能建议,还是提交代码,您的参与都将帮助我们改进这个项目。

## 目录

- [行为准则](#行为准则)
- [如何贡献](#如何贡献)
  - [报告Bug](#报告Bug)
  - [提出新特性](#提出新特性)
  - [参与讨论](#参与讨论)
  - [提交代码](#提交代码)
- [开发流程](#开发流程)
  - [环境设置](#环境设置)
  - [代码风格](#代码风格)
  - [测试](#测试)
  - [提交规范](#提交规范)
- [Pull Request流程](#Pull-Request流程)
- [分支管理策略](#分支管理策略)
- [发布流程](#发布流程)
- [联系我们](#联系我们)

## 行为准则

本项目遵循[贡献者公约](https://www.contributor-covenant.org/)行为准则。参与本项目,即表示您同意遵守此准则。不可接受的行为可以向项目维护者报告。

## 如何贡献

### 报告Bug

如果您发现了bug,请使用项目的Issue模板创建一个新的Issue,并尽可能详细地提供以下信息:

- 查看已有的 Issues,确保该问题尚未被报告
- 使用"🐞 Bug报告"模板
- 清晰描述发生了什么以及您期望的行为
- 提供详细的复现步骤
- 如可能,提供错误的截图或录屏
- 提供您使用的系统和浏览器信息

### 提出新特性

如果您希望看到新的功能或改进,请使用"✨ 新特性请求"模板创建一个Issue,并:

- 清晰描述您希望解决的问题
- 描述您想要的解决方案
- 考虑其他可能的替代解决方案
- 提供相关示例或参考(如果有)

### 参与讨论

对于需要更多社区意见的话题,请使用"💬 讨论问题"模板创建一个Issue,或者参与现有的讨论。您的洞见和观点对项目的发展至关重要。

### 提交代码

如果您希望通过代码贡献参与项目,请遵循以下步骤:

1. 查看现有Issues,找到您感兴趣的任务或问题
2. 在Issue上留言表示您计划处理这个问题,避免重复工作
3. Fork项目仓库到您的个人账号
4. 创建一个新的分支进行您的修改
5. 编写代码和测试
6. 提交Pull Request

## 开发流程

### 环境设置

1. **Fork 本仓库**  
   点击页面右上角的 `Fork` 按钮,复制仓库到你的 GitHub 账户。

2. **克隆仓库到本地**  
   使用 Git 克隆你 Fork 的仓库:
   ```bash
   git clone https://github.com/your-username/MoeKoeMusic.git
   ```

3. **创建一个新的分支**  
   为你的功能或修复创建一个新的分支:
   ```bash
   git checkout -b your-feature-branch
   ```

4. **安装依赖并进行开发**  
   请根据项目的文档安装所需的依赖,并开始开发。

5. **提交更改并推送**  
   完成开发后,提交并推送你的更改:
   ```bash
   git add .
   git commit -m "Your detailed commit message"
   git push origin your-feature-branch
   ```

6. **提交合并请求(PR)**  
   提交 PR 之前,请确保你的分支已经更新,并且没有冲突。打开你的 GitHub 仓库,点击 Compare & pull request 提交 PR。

### 代码风格

我们使用ESLint和Prettier来保持代码风格一致。在提交代码前,请确保您的代码符合项目的风格指南:

- 使用统一的代码格式(例如:缩进、空格、命名规范)
- 遵循项目的代码结构
- 请编写易于理解的代码,并添加必要的注释

### 测试

提交代码前,请确保所有测试通过:

```bash
npm run test
```

如果您添加了新功能,也请同时添加相应的测试。

### 提交规范

我们使用[约定式提交](https://www.conventionalcommits.org/)规范来格式化提交信息,基本格式如下:

```
<类型>[可选的作用域]: <描述>

[可选的正文]

[可选的脚注]
```

常用的提交类型包括:

- **feat**: 新功能
- **fix**: 修复Bug
- **docs**: 文档更新
- **style**: 代码风格调整(不影响代码逻辑)
- **refactor**: 代码重构
- **perf**: 性能优化
- **test**: 添加或更新测试
- **chore**: 构建过程或辅助工具的变动

另外我们也接受 **Gitmoji** 的提交风格:

| Emoji | 类型          | 说明                    | 示例                                    |
| ----- | ------------ | ----------------------- | ---------------------------------------- |
| ✨    | `feat`       | 新功能(Feature)         | `✨ feat: 添加搜索功能`                |
| 🐛    | `fix`        | 修复 Bug                 | `🐛 fix: 修复登录时闪退的问题`          |
| ♻️    | `refactor`   | 代码重构(非功能性更改)   | `♻️ refactor: 重构用户模块代码结构`    |
| 📝    | `docs`       | 修改文档                 | `📝 docs: 更新 README 安装说明`         |
| 🎨    | `style`      | 格式/排版修改(不影响代码逻辑) | `🎨 style: 调整代码缩进和格式`    |
| ✅    | `test`       | 添加或修改测试代码         | `✅ test: 添加用户服务单元测试`        |
| 🚀    | `perf`       | 性能优化                 | `🚀 perf: 提升图片加载速度`              |
| 🔧    | `chore`      | 构建配置/脚本/依赖等杂项   | `🔧 chore: 更新依赖包`                 |
| 🔥    | `remove`     | 删除无用代码或文件         | `🔥 remove: 删除未使用的组件`           |
| 📦    | `build`      | 打包相关改动(构建、CI)    | `📦 build: 配置打包输出目录`          |
| 🔀    | `merge`      | 合并分支                 | `🔀 merge: 合并 dev 分支`               |
| 🚧    | `wip`        | 开发中(Work In Progress) | `🚧 wip: 正在实现订单详情页面`         |
| ⬆️    | `upgrade`    | 升级依赖                 | `⬆️ upgrade: 升级 Electron 到 v28`       |
| ⬇️    | `downgrade`  | 降级依赖                 | `⬇️ downgrade: 降级 vue-router 到 v4.0.0`|
| 🐳    | `docker`     | 与 Docker 相关的更改      | `🐳 docker: 添加 Dockerfile`            |
| 💄    | `ui`         | 修改 UI 或样式           | `💄 ui: 优化按钮样式`                     |
| 💥    | `breaking`   | 破坏性更新(需注意兼容)    | `💥 breaking: 移除旧版 API`            |
| 📈    | `analytics`  | 数据分析或埋点代码         | `📈 analytics: 添加页面浏览统计`        |
| 🔖	  | `release`	  | 正式发布一个版本          |	`🔖 release: v1.2.0`                   |

## Pull Request流程

1. 确保您的Pull Request(PR)有一个清晰的标题和描述
2. 将您的PR关联到相关的Issue(如果有)
3. 确保所有自动化测试通过
4. 至少需要一名项目维护者的代码审查和批准
5. 如果需要更改,请在同一PR中进行修改
6. 一旦获得批准,您的代码将被合并到主分支

## 分支管理策略

我们使用以下分支命名和管理策略:

- `main`/`master`: 主分支,包含稳定、可发布的代码
- `develop`: 开发分支,包含最新的开发代码
- `feature/*`: 新功能分支,从`develop`分支创建
- `bugfix/*`: Bug修复分支,从`develop`分支创建
- `hotfix/*`: 紧急修复分支,从`main`分支创建
- `release/*`: 发布准备分支,从`develop`分支创建

## 发布流程

我们使用[语义化版本](https://semver.org/)进行版本管理:

- MAJOR版本:当你做了不兼容的API修改
- MINOR版本:当你做了向下兼容的功能性新增
- PATCH版本:当你做了向下兼容的问题修正

## 联系我们

如果您有任何问题或需要进一步的帮助,请通过以下方式联系我们:

- 在GitHub上创建Issue
- 在Blog中留言 [Blog](https://MoeJue.cn)

再次感谢你对项目的支持和贡献!

================================================
FILE: Dockerfile
================================================
# Stage 1: Build Frontend
FROM node:20-alpine AS frontend-builder
WORKDIR /app
COPY package*.json ./
# Remove electron and electron-builder from package.json
RUN node -e "const fs = require('fs'); const filePath = './package.json'; \
             let rawdata = fs.readFileSync(filePath); let packageJson = JSON.parse(rawdata); \
             if (packageJson.devDependencies) { \
               delete packageJson.devDependencies.electron; \
               delete packageJson.devDependencies['electron-builder']; \
             } \
             if (packageJson.dependencies) { \
               delete packageJson.dependencies.electron; \
             } \
             fs.writeFileSync(filePath, JSON.stringify(packageJson, null, 2));"
RUN npm install
COPY . .
RUN npm run build:docker

# Stage 2: Setup Combined App
FROM node:20-alpine
WORKDIR /app

# Install Nginx
RUN apk add --no-cache nginx

# Copy API code
COPY ./api ./api
# Install API dependencies
WORKDIR /app/api
RUN npm install --production
# Reset WORKDIR to /app
WORKDIR /app 

# Copy built frontend static assets from the builder stage
COPY --from=frontend-builder /app/dist/ ./dist/

# Expose ports
# For frontend served by 'serve'
EXPOSE 8080 
# For API
EXPOSE 6521 

# Copy Nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf

# Command to run both services
# API runs from /app/api directory, frontend served by Nginx
# CMD ["sh", "-c", "cd /app/api && node app.js & nginx -g 'daemon off;'"]
CMD sh -c "\
  echo 'client running @ http://127.0.0.1:8080/'; \
  cd /app/api && node app.js & nginx -g 'daemon off;'"


================================================
FILE: LICENSE
================================================
                    GNU GENERAL PUBLIC LICENSE
                       Version 2, June 1991

 Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The licenses for most software are designed to take away your
freedom to share and change it.  By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users.  This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it.  (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.)  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.

  To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have.  You must make sure that they, too, receive or can get the
source code.  And you must show them these terms so they know their
rights.

  We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.

  Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software.  If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.

  Finally, any free program is threatened constantly by software
patents.  We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary.  To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.

  The precise terms and conditions for copying, distribution and
modification follow.

                    GNU GENERAL PUBLIC LICENSE
   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

  0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License.  The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language.  (Hereinafter, translation is included without limitation in
the term "modification".)  Each licensee is addressed as "you".

Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope.  The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.

  1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.

You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.

  2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:

    a) You must cause the modified files to carry prominent notices
    stating that you changed the files and the date of any change.

    b) You must cause any work that you distribute or publish, that in
    whole or in part contains or is derived from the Program or any
    part thereof, to be licensed as a whole at no charge to all third
    parties under the terms of this License.

    c) If the modified program normally reads commands interactively
    when run, you must cause it, when started running for such
    interactive use in the most ordinary way, to print or display an
    announcement including an appropriate copyright notice and a
    notice that there is no warranty (or else, saying that you provide
    a warranty) and that users may redistribute the program under
    these conditions, and telling the user how to view a copy of this
    License.  (Exception: if the Program itself is interactive but
    does not normally print such an announcement, your work based on
    the Program is not required to print an announcement.)

These requirements apply to the modified work as a whole.  If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works.  But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.

Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.

In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.

  3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:

    a) Accompany it with the complete corresponding machine-readable
    source code, which must be distributed under the terms of Sections
    1 and 2 above on a medium customarily used for software interchange; or,

    b) Accompany it with a written offer, valid for at least three
    years, to give any third party, for a charge no more than your
    cost of physically performing source distribution, a complete
    machine-readable copy of the corresponding source code, to be
    distributed under the terms of Sections 1 and 2 above on a medium
    customarily used for software interchange; or,

    c) Accompany it with the information you received as to the offer
    to distribute corresponding source code.  (This alternative is
    allowed only for noncommercial distribution and only if you
    received the program in object code or executable form with such
    an offer, in accord with Subsection b above.)

The source code for a work means the preferred form of the work for
making modifications to it.  For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable.  However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.

If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.

  4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License.  Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.

  5. You are not required to accept this License, since you have not
signed it.  However, nothing else grants you permission to modify or
distribute the Program or its derivative works.  These actions are
prohibited by law if you do not accept this License.  Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.

  6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions.  You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.

  7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all.  For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.

If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.

It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices.  Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.

This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.

  8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded.  In such case, this License incorporates
the limitation as if written in the body of this License.

  9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

Each version is given a distinguishing version number.  If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation.  If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.

  10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission.  For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this.  Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.

                            NO WARRANTY

  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.

  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

Also add information on how to contact you by electronic and paper mail.

If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:

    Gnomovision version 69, Copyright (C) year name of author
    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.

You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary.  Here is a sample; alter the names:

  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
  `Gnomovision' (which makes passes at compilers) written by James Hacker.

  <signature of Ty Coon>, 1 April 1989
  Ty Coon, President of Vice

This General Public License does not permit incorporating your program into
proprietary programs.  If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library.  If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

================================================
FILE: README.md
================================================
<br />
<p align="center">
    <img src="https://github.com/iAJue/MoeKoeMusic/raw/main/images/logo.png" alt="Logo" width="156" height="156">
  <h2 align="center" style="font-weight: 600">MoeKoe Music</h2>
  <p align="center">
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/releases/latest"><img src="https://img.shields.io/github/v/release/MoeKoeMusic/MoeKoeMusic?style=flat-square" /></a>
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/stargazers"><img src="https://img.shields.io/github/stars/MoeKoeMusic/MoeKoeMusic?style=flat-square" /></a>
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/releases"><img src="https://img.shields.io/github/downloads/MoeKoeMusic/MoeKoeMusic/total?style=flat-square" /></a>
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/blob/main/LICENSE"><img src="https://img.shields.io/github/license/MoeKoeMusic/MoeKoeMusic?style=flat-square" /></a>
    <a href="https://github.com/iAJue"><img src="https://img.shields.io/badge/%F0%9F%8E%89_Create_by_iAJue-with_Love_%E2%9D%A4-pink?style=flat-square" /></a>
  </p>
  <p align="center">
    一款开源简洁高颜值的酷狗第三方客户端
    <br />
    <a href="https://github.com/iAJue/MoeKoeMusic/" target="blank"><strong>🌎 GitHub仓库</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/releases" target="blank"><strong>📦️ 下载安装包</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://MoeJue.cn" target="blank"><strong>💬 访问博客</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://Music.MoeKoe.cn" target="blank"><strong>🏠 项目主页</strong></a>
  </p>
  <p align="center">
    <a href="https://github.com/iAJue/MoeKoeMusic/README.md" target="blank"><strong>🇨🇳 简体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_tw.md" target="blank"><strong>🇨🇳 繁体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ja.md" target="blank"><strong>🇯🇵 日本語</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_en.md" target="blank"><strong>🇺🇸 English</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ko.md" target="blank"><strong>🇰🇷 한국어</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ru.md" target="blank"><strong>🇷🇺 Русский</strong></a>
    <br />
    <br />
  </p>
</p>

![images](https://github.com/iAJue/MoeKoeMusic/raw/main/images/1.png)

## ❤️ 前言

早在10年前后的样子,那会在用网页版QQ的时候我就已经开始使用酷狗音乐了(也是十来年的老粉了),所以这些年收藏的歌曲全部都在上面.后来我也尝试开始使用网易云或QQ音乐,也尝试把酷狗的歌单导入进去,但是效果都不尽人意.我听的大多是日漫OP,好多歌曲都没办法找到.

兜兜转转最后还是回到酷狗,但是在Mac端的酷狗,时常可能会出现不能播放的情况,虽说界面没什么功能,但也挺好的.在网友的安利下,我现在一直是在酷狗的[概念版](https://t1.kugou.com/d2tBza3CSV2)上听歌,并且是市面上为数不多能免费听VIP歌曲的音乐播放软件了,力推.

我在我的个人介绍页面说我特别喜欢听歌,尤其是日漫OP.怎么证明呢?(之前我网页版歌单也年久失修了)那就自己开发一个音乐播放器.


## ✨ 特性

- ✅ 使用 Vue.js 全家桶开发
- 🔴 酷狗账号登录(扫码/手机/账号登录)
- 📃 支持歌词显示
- 📻 每日推荐歌曲
- 🚫🤝 无任何社交功能
- 🔗 官方服务器直连, 无任何第三方 API
- ✔️ 每日自动领取VIP, 登录就是VIP
- 🎨 主题色切换 
- 👋 启动问候语
- ⚙️ 多平台支持
- 🛠 更多特性开发中

## 📢 Todo List
- [x] 📺 支持 MV 播放
- [x] 🌚 Light/Dark Mode 自动切换
- [x] 👆 支持 Touch Bar
- [x] 🖥️ 支持 PWA,可在 Chrome/Edge 里点击地址栏右边的 ➕ 安装到电脑
- [ ] 🎧 支持 Mpris
- [x] ⌨️ 全局快捷键
- [x] 🤟 多语言支持
- [x] 📻 桌面歌词
- [x] ⚙️ 系统架构优化
- [x] 🎶 歌曲、歌单/收藏、取消

更新日志请查看 [Commits](https://github.com/iAJue/MoeKoeMusic/commits/main/)

## 📦️ 安装

### 1. 客户端安装

访问本项目的 [Releases](https://github.com/iAJue/MoeKoeMusic/releases) 页面下载安装包。

### 2. WEB端安装(docker)

* 注意:部署后请开放服务器对应端口才可使用,或者使用反向代理实现域名访问。

    1. 方式一:快速启动(推荐)

    ```
    git clone https://github.com/iAJue/MoeKoeMusic.git
    cd MoeKoeMusic
    git submodule update --init --recursive
    docker compose up -d &
    ```

    2. ~~方式二:使用docker-compose一键安装 (镜像暂未上传官方)~~
    
    ```
    docker run -d --name MoeKoeMusic -p 8080:8080 -p 6521:6521 -e PORT=6521 -e platform=lite iajue/moekoe-music:latest
    ```

    3. 方式三:宝塔容器编排

    * 远程镜像,版本可能会落后于官方
    
    ```
    version: '3.3'
    
    services:
      moekoe-music:
        # 镜像地址
        image: registry.cn-wulanchabu.aliyuncs.com/youngxj/moekoe-music:latest
        container_name: moekoe-music # 容器名
        restart: unless-stopped # 自动重启
        build:
          context: .
          dockerfile: Dockerfile
        environment:
          - PORT=6521
          - platform=lite
        ports: # 端口映射
          - "8080:8080"  # 前端服务
          - "6521:6521"  # 接口服务
    
    ```
    
    复制内容上面的内容,粘贴到宝塔面板的容器编排里面,编排名称为MoeKoeMusic,点击部署即可。

### 3. 一键部署
[![使用 EdgeOne Pages 部署](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?template=https://github.com/iAJue/moekoemusic&install-command=npm%20install&output-directory=dist&root-directory=.%2F&build-command=npm%20run%20build&env=VITE_APP_API_URL)

需在环境变量(VITE_APP_API_URL)中填写自己的API地址

## ⚙️ 开发

1. 克隆本仓库

```sh
git clone --recurse-submodules https://github.com/iAJue/MoeKoeMusic.git
```

2. 进入目录并安装依赖

```sh
cd MoeKoeMusic
npm run install-all
```
3. 启动开发者模式
```sh
npm run dev
```
4. 打包项目
```sh
npm run build
```
5. 编译项目
  - Windows: 
  ```sh
  npm run electron:build:win [默认 NSIS 安装包]
  ```
  -	Linux: 
  ```sh
  npm run electron:build:linux [默认 AppImage 格式]
  ```
  -	macOS: 
  ```sh
  npm run electron:build:macos [默认 macOS 双架构]
  ```


更多命令请查看 `package.json` 文件 `scripts` 

## 👷‍♂️ 编译客户端

如果在 Release 页面没有找到适合你的设备的安装包的话,你可以根据下面的步骤来打包自己的客户端。

1. 安装 [Node.js](https://nodejs.org/en/),并确保 `Node.js` 版本 >= 18.0.0。

2. 使用 `git clone https://github.com/iAJue/MoeKoeMusic.git` 克隆本仓库到本地。

3. 使用 `npm install` 安装项目依赖。
4. 编译API服务端
    - Windows:
        ```sh
        npm run build:api:win
        ```
    - Linux:
        ```sh
        npm run build:api:linux
        ```
    - macOS:
      ```sh
      npm run build:api:macos
      ```

5. 选择下列的命令来打包适合的你的安装包,打包出来的文件在 `/dist_electron` 目录下。了解更多信息可访问 [electron-builder 文档](https://www.electron.build/cli)


#### 1. 打包 macOS 平台
   - 通用的 macOS 包(Intel 和 Apple Silicon 双架构):
   ```
   npm run electron:build -- --mac --universal
   ```
   - 仅 Intel 架构:
   ```
   npm run electron:build -- --mac --x64
   ```
   - 仅 Apple Silicon 架构:
   ```
   npm run electron:build -- --mac --arm64
   ```


#### 2. 打包 Windows 平台

   - 默认 NSIS 安装包(适合大多数 Windows 用户):
   ```
   npm run electron:build -- --win
   ```
   - 为 Windows 创建 EXE 文件和 Squirrel 安装包:
   ```
   npm run electron:build -- --win --ia32 --x64 --arm64 --target squirrel
   ```
       - --ia32 为 32 位 Windows 架构。
       - --x64 为 64 位 Windows 架构。
       - --arm64 为 ARM Windows 架构(Surface 等设备)。

   - 为 Windows 生成便携式的 EXE 文件(免安装):
   ```
   npm run electron:build -- --win --portable
   ```
#### 3. 打包 Linux 平台
   - 默认 AppImage 格式(适用于大多数 Linux 发行版):

   ```
   npm run electron:build -- --linux
   ```
   - snap(适用于 Ubuntu 和支持 snap 的发行版):
   ```
   npm run electron:build -- --linux --target snap
   ```
   - 	deb(适用于 Debian/Ubuntu 系列):
   ```
   npm run electron:build -- --linux --target deb
   ```
   - rpm(适用于 Red Hat/Fedora 系列):
   ```
   npm run electron:build -- --linux --target rpm
   ```
   - ARM64架构(ARM v8+): 
   ```
   npm run build:api:linux-arm64 //编译API
   npm run electron:build:linux-arm64 //编译主程序
   ```

#### 4. 打包所有平台

  如果需要同时生成 Windows、macOS 和 Linux 的安装包,可以使用以下命令:
  ```
  npm run electron:build -- -mwl
  ```

#### 5. 自定义编译设置

您可以根据需要添加其他选项来进一步自定义打包,例如指定 x64 和 arm64 架构,或选择不同的目标格式。

## ⭐ 支持项目

如果您觉得这个项目对您有帮助,欢迎给我们一个 Star!您的支持是我们持续改进的动力。

[![GitHub stars](https://img.shields.io/github/stars/iAJue/MoeKoeMusic.svg?style=social&label=Star)](https://github.com/iAJue/MoeKoeMusic)

## ✅ 反馈

如有任何问题或建议,欢迎提交 issue 或 pull request。

## ⚠️ 免责声明
0. 本程序是酷狗第三方客户端,并非酷狗官方,需要更完善的功能请下载官方客户端体验.
1. 本项目仅供学习使用,请尊重版权,请勿利用此项目从事商业行为及非法用途!
2. 使用本项目的过程中可能会产生版权数据。对于这些版权数据,本项目不拥有它们的所有权。为了避免侵权,使用者务必在 24 小时内清除使用本项目的过程中所产生的版权数据。
3. 由于使用本项目产生的包括由于本协议或由于使用或无法使用本项目而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害(包括但不限于因商誉损失、停工、计算机故障或故障引起的损害赔偿,或任何及所有其他商业损害或损失)由使用者负责。        
1. 禁止在违反当地法律法规的情况下使用本项目。对于使用者在明知或不知当地法律法规不允许的情况下使用本项目所造成的任何违法违规行为由使用者承担,本项目不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。    
2. 音乐平台不易,请尊重版权,支持正版。
3. 本项目仅用于对技术可行性的探索及研究,不接受任何商业(包括但不限于广告等)合作及捐赠。
4. 如果官方音乐平台觉得本项目不妥,可联系本项目更改或移除。
            

## 📜 开源许可

本项目仅供个人学习研究使用,禁止用于商业及非法用途。

基于 [GNU General Public License v2.0 (GPL-2.0)](https://github.com/iAJue/MoeKoeMusic/blob/main/LICENSE) 许可进行开源。

## 👍 灵感来源

API 源代码来自 [MakcRe/KuGouMusicApi](https://github.com/MakcRe/KuGouMusicApi) 

- [Apple Music](https://music.apple.com)
- [YouTube Music](https://music.youtube.com)
- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)
- [酷狗音乐](https://kugou.com/)

## 🖼️ 截图

![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/2.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/3.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/4.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/5.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/6.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/7.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/8.png)


## 🗓️ Star History

[![Star History Chart](https://api.star-history.com/svg?repos=iAJue/MoeKoeMusic&type=Date)](https://www.star-history.com/#iAJue/MoeKoeMusic&Date)


================================================
FILE: build/installer.nsh
================================================
!include "MUI.nsh"
!define MUI_FINISHPAGE_LINK_LOCATION "https://MoeJue.cn"
!define MUI_FINISHPAGE_LINK "访问作者(阿珏酱)主页"
!define MUI_FINISHPAGE_SHOWREADME_TEXT "访问 GitHub 项目主页"
!define MUI_FINISHPAGE_SHOWREADME "https://github.com/iAJue/MoeKoeMusic"
!insertmacro MUI_PAGE_WELCOME

; Register the moekoe:// protocol
!macro customInstall
  DeleteRegKey HKCR "moekoe"
  WriteRegStr HKCR "moekoe" "" "URL:MoeKoe Music Protocol"
  WriteRegStr HKCR "moekoe" "URL Protocol" ""
  WriteRegStr HKCR "moekoe\DefaultIcon" "" "$INSTDIR\${PRODUCT_NAME}.exe,0"
  WriteRegStr HKCR "moekoe\shell" "" ""
  WriteRegStr HKCR "moekoe\shell\open" "" ""
  WriteRegStr HKCR "moekoe\shell\open\command" "" '"$INSTDIR\${PRODUCT_NAME}.exe" "%1"'
!macroend

================================================
FILE: build/license.txt
================================================
MoeKoe Music Software License Agreement

1. Terms of Use
This software is released under the GPL-2.0 license. You are free to use, modify, and distribute this software, but must comply with all terms of the GPL-2.0 license. This means any modified versions must also be open-sourced under the same license.

2. Disclaimer
This software is provided "as is" without any express or implied warranties.

3. Copyright Notice
Copyright 2024 MoeKoe. All rights reserved.

4. Usage Restrictions
- Prohibited for any illegal purposes
- Prohibited for commercial use

5. Privacy Policy
This software collects necessary usage data to improve service quality.

6. Termination
Any violation of the terms in this agreement will result in automatic termination of the license.

By installing or using this software, you agree to accept all terms of this agreement.

================================================
FILE: docker-compose.yml
================================================
version: '3.3'

services:
  moekoe-music:
    container_name: moekoe-music # 容器名
    restart: unless-stopped # 自动重启
    build:
      context: .
      dockerfile: Dockerfile

    environment:
      - PORT=6521
      - platform=lite
    ports: # 端口映射
      - "8080:8080"  # 前端服务
      - "6521:6521"  # 接口服务


================================================
FILE: docs/README_en.md
================================================
> **Note**: This English document may not be updated in a timely manner. For the latest content, please refer to the [Simplified Chinese version](https://github.com/iAJue/MoeKoeMusic/README.md).
<br />
<p align="center">
<img src="https://github.com/iAJue/MoeKoeMusic/raw/main/images/logo.png " alt="Logo" width="156" height="156">
<h2 align="center" style="font-weight: 600">MoeKoe Music</h2>
  <p align="center">
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/releases/latest"><img src="https://img.shields.io/github/v/release/MoeKoeMusic/MoeKoeMusic?style=flat-square" /></a>
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/stargazers"><img src="https://img.shields.io/github/stars/MoeKoeMusic/MoeKoeMusic?style=flat-square" /></a>
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/releases"><img src="https://img.shields.io/github/downloads/MoeKoeMusic/MoeKoeMusic/total?style=flat-square" /></a>
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/blob/main/LICENSE"><img src="https://img.shields.io/github/license/MoeKoeMusic/MoeKoeMusic?style=flat-square" /></a>
    <a href="https://github.com/iAJue"><img src="https://img.shields.io/badge/%F0%9F%8E%89_Create_by_iAJue-with_Love_%E2%9D%A4-pink?style=flat-square" /></a>
  </p>
<p align="center">
An open-source, concise, and aesthetically pleasing third-party client for KuGou
<br />
<a href="https://github.com/iAJue/MoeKoeMusic/" target="blank"><strong>🌎 GitHub Repository</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="https://github.com/iAJue/MoeKoeMusic/releases" target="blank"><strong>📦️ Download Packages</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="https://MoeJue.cn" target="blank"><strong>💬 Visit Blog</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="https://Music.MoeKoe.cn" target="blank"><strong>🏠 Project Homepage</strong></a>

</p>
<p align="center">
    <a href="https://github.com/iAJue/MoeKoeMusic/README.md" target="blank"><strong>🇨🇳 简体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_tw.md" target="blank"><strong>🇨🇳 繁体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ja.md" target="blank"><strong>🇯🇵 日本語</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_en.md" target="blank"><strong>🇺🇸 English</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ko.md" target="blank"><strong>🇰🇷 한국어</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ru.md" target="blank"><strong>🇷🇺 Русский</strong></a>
    <br />
    <br />
  </p>
</p>

![images]( https://github.com/iAJue/MoeKoeMusic/raw/main/images/1.png )

## ❤️ Preface

As early as around 10 years ago, when I was using the web version of QQ, I had already started using Kugou Music (which I have been a fan of for over ten years), so all the songs I collected over the years were on it Later on, I also tried using NetEase Cloud or QQ Music, and tried importing Kugou's playlists into it, but the results were not satisfactory I mostly listen to Japanese anime OP, and I can't find many songs

After wandering around, I finally returned to Kugou. However, on the Mac version of Kugou, there may often be situations where it cannot be played. Although the interface does not have many functions, it is still quite good With the support of netizens, I have been working on [Kugou's concept version](https://t1.kugou.com/d2tBza3CSV2)Listen to music online, and it is one of the few music playback software on the market that allows you to listen to VIP songs for free. It is highly recommended

I said on my personal introduction page that I particularly enjoy listening to music, especially Japanese anime OP How can we prove it? (My web version of the playlist was also in disrepair for a long time before) So I'll develop my own music player


##  ✨  characteristic

-  ✅  Developing with Vue.js Family Bucket
-  🔴  KuGou account login (scan code/phone/account login)
-  📃  Support lyric display
-  📻  Daily recommended songs
-  🚫🤝  No social function
-  🔗  Official server direct connection, without any third-party APIs
-  ✔️  Automatically claim VIP every day, log in to become VIP
-  🎨  Theme color switching
-  👋  Initiate greetings
-  ⚙️  Multi platform support
-  🛠  More features under development

## 📢 Todo List
- [x]  📺  Support MV playback
- [x]  🌚 Light/Dark Mode  Automatic switching
- [x]  👆  Support Touch Bar
- [x]  🖥️  Support PWA, you can click on the right side of the address bar in Chrome/Edge ➕  Install to computer
- [ ]  🎧  Support Mpris
- [x]  ⌨️   Global shortcut keys
- [x]  🤟  Multi language support
- [x]  📻  Desktop Lyrics
- [x]  ⚙️  System architecture optimization
- [x]  🎶  Songs, playlists/favorites, cancellation

Please check the  for the update log [Commits](https://github.com/iAJue/MoeKoeMusic/commits/main/)

## 📦️ Installation

### 1. Client Installation

Visit the [Releases](https://github.com/iAJue/MoeKoeMusic/releases) page of this project to download the installation package.

### 2. Web Installation (Docker)

* Note: Please open the corresponding port on the server after deployment, or use a reverse proxy for domain access.

  1. Method 1: Quick Start (Recommended)

  ```
  git clone https://github.com/iAJue/MoeKoeMusic.git
  cd MoeKoeMusic
  git submodule update --init --recursive
  docker compose up -d &
  ```

  2. ~~Method 2: One-click installation using docker-compose (image not yet uploaded officially)~~
  
  ```
  docker run -d --name MoeKoeMusic -p 8080:8080 -p 6521:6521 -e PORT=6521 -e platform=lite iajue/moekoe-music:latest
  ```

  3. Method 3: Baota Container Orchestration

  * Remote image, version may be behind the official
  
  ```
  version: '3.3'
  
  services:
    moekoe-music:
    # Image address
    image: registry.cn-wulanchabu.aliyuncs.com/youngxj/moekoe-music:latest
    container_name: moekoe-music # Container name
    restart: unless-stopped # Auto restart
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      - PORT=6521
      - platform=lite
    ports: # Port mapping
      - "8080:8080"  # Frontend service
      - "6521:6521"  # API service
  
  ```
  
  Copy the content above and paste it into the container orchestration in the Baota panel, name the orchestration as MoeKoeMusic, and click deploy.
### 3. One-Click Deployment
[![使用 EdgeOne Pages 部署](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?template=https://github.com/iAJue/moekoemusic&install-command=npm%20install&output-directory=dist&root-directory=.%2F&build-command=npm%20run%20build&env=VITE_APP_API_URL)

You need to fill in your own API address in the environment variable VITE_APP_API_URL.

##  ⚙️  development

1. Clone this repository

```sh
git clone --recurse-submodules https://github.com/iAJue/MoeKoeMusic.git
```

2. Enter the directory and install dependencies

```sh
cd MoeKoeMusic
npm run install-all
```
3. Launch developer mode
```sh
npm run dev
```
4. Package project
```sh
npm run build
```
5. Compile the project
- Windows: 
```sh
Npm run electron: build: win [default NSIS installation package]
```
-	Linux: 
```sh
Npm run electron: build: Linux [default AppImage format]
```
-	macOS: 
```sh
Npm run electron: build: macos [default universal architecture]
```


For more commands, please refer to the ` package.json ` file ` scripts `

##  👷‍♂️  Compile client

If you cannot find the installation package suitable for your device on the Release page, you can follow the steps below to package your own client.

1. Install [Node.js](https://nodejs.org/en/)And ensure that the 'Node. js' version is>=18.0.0.

2. Use ` git clone https://github.com/iAJue/MoeKoeMusic.git `Clone this repository locally.

3. Use 'npm install' to install project dependencies.
4. Compile API server
- Windows:
```sh
npm run build:api:win
```
- Linux:
```sh
npm run build:api:linux
```
- macOS:
```sh
npm run build:api:macos
```

5. Choose the following command to package the appropriate installation package for you, and the packaged file should be located in the '/dits_electron' directory. For more information, please visit the [Electron Builder documentation](https://www.electron.build/cli )


#### 1.  Package macOS platform
- Universal macOS package (Intel and Apple Silicon dual architecture):
```
npm run electron:build -- --mac --universal
```
- Only Intel architecture:
```
npm run electron:build -- --mac --x64
```
- Only Apple Silicon architecture:
```
npm run electron:build -- --mac --arm64
```


#### 2.  Package Windows Platform

- Default NSIS installation package (suitable for most Windows users):
```
npm run electron:build -- --win
```
- Create EXE files and Squirrel installation packages for Windows:
```
npm run electron:build -- --win --ia32 --x64 --arm64 --target squirrel
```
  - Ia32 is a 32-bit Windows architecture.
  - X64 is a 64 bit Windows architecture.
  - Arm64 is based on ARM Windows architecture (for devices such as Surface).

- Generate portable EXE files for Windows (installation free):
```
npm run electron:build -- --win --portable
```
#### 3.  Packaging Linux Platform
- Default AppImage format (applicable to most Linux distributions):
```
npm run electron:build -- --linux
```
- Snap (for Ubuntu and Snap supported distributions):
```
npm run electron:build -- --linux --target snap
```
- Deb (applicable to the Debian/Ubuntu series):
```
npm run electron:build -- --linux --target deb
```
- RPM (applicable to Red Hat/Fedora series):
```
npm run electron:build -- --linux --target rpm
```

#### 4.  Package all platforms

If you need to generate installation packages for Windows, macOS, and Linux simultaneously, you can use the following command:
```
npm run electron:build -- -mwl
```

#### 5.  Custom compilation settings

You can add other options as needed to further customize the packaging, such as specifying x64 and arm64 architectures, or selecting different target formats.

## ⭐ Support This Project

If you find this project helpful, please consider giving us a star! Your support motivates us to keep improving.

[![GitHub stars](https://img.shields.io/github/stars/iAJue/MoeKoeMusic.svg?style=social&label=Star)](https://github.com/iAJue/MoeKoeMusic)

##  ☑️  feedback

If you have any questions or suggestions, please feel free to submit an issue or pull request.

## ⚠️ Disclaimers
0. This program is a third-party client of KuGou, not an official KuGou client. If you need more complete functions, please download the official client to experience it
1. This project is for learning purposes only. Please respect copyright and do not use this project for commercial activities or illegal purposes!
2. Copyright data may be generated during the use of this project. For these copyrighted data, this project does not own them. To avoid infringement, users must clear any copyright data generated during the use of this project within 24 hours.
3. The user shall be responsible for any direct, indirect, special, incidental, or consequential damages of any nature arising from the use of this project, including but not limited to damages caused by loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses.
            
1. It is prohibited to use this project in violation of local laws and regulations. Any illegal or irregular behavior caused by the use of this project by users who are aware or unaware that local laws and regulations do not allow it shall be borne by the users, and this project shall not be held responsible for any direct, indirect, special, incidental or consequential liability arising therefrom.
            
2. Music platforms are not easy, please respect copyright and support genuine versions.
3. This project is only for the exploration and research of technical feasibility, and does not accept any commercial (including but not limited to advertising, etc.) cooperation or donations.
If the official music platform finds this project inappropriate, they can contact this project to make changes or remove it.
            

##  📜  Open source license

This project is for personal learning and research purposes only, and is prohibited from being used for commercial or illegal purposes.

Based on [MIT license](https://opensource.org/licenses/MIT)License to open source.

## 👍 Inspiration source

The API source code comes from [MakcRe/KuGouMusicApi](https://github.com/MakcRe/KuGouMusicApi ) 

- [Apple Music]( https://music.apple.com )
- [YouTube Music]( https://music.youtube.com )
- [YesPlayMusic]( https://github.com/qier222/YesPlayMusic )
- [Cool Dog Music](https://kugou.com/ )

##  🖼️  screenshot

![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/2.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/3.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/4.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/5.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/6.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/7.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/8.png)

## 🗓️ Star History

[![Star History Chart](https://api.star-history.com/svg?repos=iAJue/MoeKoeMusic&type=Date)](https://www.star-history.com/#iAJue/MoeKoeMusic&Date)


================================================
FILE: docs/README_ja.md
================================================
> **注意**: この日本語ドキュメントはタイムリーに更新されない場合があります。最新の内容については[簡体字中国語版](https://github.com/iAJue/MoeKoeMusic/README.md)をご参照ください。
<br />
<p align="center">
<img src="https://github.com/iAJue/MoeKoeMusic/raw/main/images/logo.png" alt="Logo" width="156" height="156">
<h2 align="center" style="font-weight: 600">MoeKoe Music</h2>
  <p align="center">
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/releases/latest"><img src="https://img.shields.io/github/v/release/MoeKoeMusic/MoeKoeMusic?style=flat-square" /></a>
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/stargazers"><img src="https://img.shields.io/github/stars/MoeKoeMusic/MoeKoeMusic?style=flat-square" /></a>
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/releases"><img src="https://img.shields.io/github/downloads/MoeKoeMusic/MoeKoeMusic/total?style=flat-square" /></a>
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/blob/main/LICENSE"><img src="https://img.shields.io/github/license/MoeKoeMusic/MoeKoeMusic?style=flat-square" /></a>
    <a href="https://github.com/iAJue"><img src="https://img.shields.io/badge/%F0%9F%8E%89_Create_by_iAJue-with_Love_%E2%9D%A4-pink?style=flat-square" /></a>
  </p>
<p align="center">
オープンソースで簡潔で高ルックスのクールな犬のサードパーティクライアント
<br />
<a href="https://github.com/iAJue/MoeKoeMusic/" target="blank"><strong>🌎 GitHub倉庫</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="https://github.com/iAJue/MoeKoeMusic/releases" target="blank"><strong>📦️インストールパッケージのダウンロード</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="https://MoeJue.cn" target="blank"><strong>💬 ブログへのアクセス</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="https://Music.MoeKoe.cn" target="blank"><strong>🏠 プロジェクトホームページ</strong></a>
</p>
<p align="center">
    <a href="https://github.com/iAJue/MoeKoeMusic/README.md" target="blank"><strong>🇨🇳 简体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_tw.md" target="blank"><strong>🇨🇳 繁体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ja.md" target="blank"><strong>🇯🇵 日本語</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_en.md" target="blank"><strong>🇺🇸 English</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ko.md" target="blank"><strong>🇰🇷 한국어</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ru.md" target="blank"><strong>🇷🇺 Русский</strong></a>
    <br />
    <br />
</p>
</p>

![images](https://github.com/iAJue/MoeKoeMusic/raw/main/images/1.png)

### ❤️ はじめに

10年ほど前の様子では、Web版QQを使っている間に私はすでにクールな犬音楽を使い始めていたので(10年以上の古い粉でもある)、これらの年に所蔵されている曲はすべて上にあります。その後、私も網易雲やQQ音楽を使ってみたり、クールな犬の歌を導入してみたりしましたが、効果はあまりありませんでした。私が聴いているのはほとんど日漫OPで、多くの曲は見つけることができませんでした。

ぐるぐる回って結局クールドッグに戻るのですが、Mac側のクールドッグでは、時々再生できないことがあります。インタフェースはあまり機能していないとはいえ、いいですね。ネットユーザーのアンリの下で、私は今までクールな犬の[コンセプト版](https://t1.kugou.com/d2tBza3CSV2)で歌を聴いて、しかもVIP曲を無料で聴ける音楽再生ソフトは市販されていないので、力を入れてください。

私は私の個人紹介ページで、私は特に歌を聴くのが好きだと言っています。特に日漫OP.どうやって証明しますか。(以前は私のウェブ版の歌単も長年修理を怠っていました)では、自分で音楽プレーヤーを開発します。


## ✨ プロパティ

- ✅ Vue.jsファミリーバケツを用いた開発
- 🔴 クールドッグアカウント登録(スキャン/携帯/アカウント登録)
- 📃 歌詞表示のサポート
- 📻 毎日のおすすめ曲
- 🚫🤝 ソーシャル機能なし
- 🔗 サードパーティ製APIなしの公式サーバ直結
- ✔️ VIPは毎日自動で受け取り、ログインするとVIPになります
- 🎨 テーマカラー切り替え
- 👋 開始の挨拶
- ⚙️ マルチプラットフォームサポート
- 🛠 その他の機能開発中

## 📢 Todo List
- [x] 📺 MV再生をサポート
- [x] 🌚 Light/Dark Mode 自動切り替え
- [x] 👆 Touch Bar対応
- [x] 🖥️ PWA対応、Chrome/Edgeでアドレスバー右の➕ コンピュータにインストール
- [ ] 🎧 Mprisのサポート
- [x] ⌨️ ショートカットとグローバルショートカットのカスタマイズ
- [x] 🤟 多言語サポート
- [x] 📻 デスクトップ歌詞
- [x] ⚙️ システムアーキテクチャの最適化
- [x] 🎶 曲、歌/コレクション、キャンセル

更新ログは[Commits](https://github.com/iAJue/MoeKoeMusic/commits/main/)

## 📦️ インストール

### 1. クライアントのインストール

本プロジェクトの [Releases](https://github.com/iAJue/MoeKoeMusic/releases) ページにアクセスして、インストールパッケージをダウンロードしてください。

### 2. WEB版のインストール(docker)

* 注意:デプロイ後は、サーバーの対応ポートを開放する必要があります。または、リバースプロキシを使用してドメインアクセスを実現してください。

    1. 方法一:クイックスタート(推奨)

    ```
    git clone https://github.com/iAJue/MoeKoeMusic.git
    cd MoeKoeMusic
    git submodule update --init --recursive
    docker compose up -d &
    ```

    2. ~~方法二:docker-composeを使用したワンクリックインストール(イメージはまだ公式にアップロードされていません)~~
    
    ```
    docker run -d --name MoeKoeMusic -p 8080:8080 -p 6521:6521 -e PORT=6521 -e platform=lite iajue/moekoe-music:latest
    ```

    3. 方法三:宝塔コンテナ編成

    * リモートイメージ、バージョンは公式より遅れる可能性があります。
    
    ```
    version: '3.3'
    
    services:
      moekoe-music:
        # イメージアドレス
        image: registry.cn-wulanchabu.aliyuncs.com/youngxj/moekoe-music:latest
        container_name: moekoe-music # コンテナ名
        restart: unless-stopped # 自動再起動
        build:
          context: .
          dockerfile: Dockerfile
        environment:
          - PORT=6521
          - platform=lite
        ports: # ポートマッピング
          - "8080:8080"  # フロントエンドサービス
          - "6521:6521"  # APIサービス
    
    ```
    
    上記の内容をコピーして、宝塔パネルのコンテナ編成に貼り付け、編成名をMoeKoeMusicとして、デプロイをクリックしてください。
### 3. ワンクリックデプロイ
[![EdgeOne Pagesを使用してデプロイ](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?template=https://github.com/iAJue/moekoemusic&install-command=npm%20install&output-directory=dist&root-directory=.%2F&build-command=npm%20run%20build&env=VITE_APP_API_URL)

環境変数(VITE_APP_API_URL)に自分のAPIアドレスを入力する必要があります。

## ⚙️ かいはつ

1.本倉庫のクローニング

```sh
git clone --recurse-submodules https://github.com/iAJue/MoeKoeMusic.git
```

2.ディレクトリにアクセスして依存関係をインストールする

```sh
cd MoeKoeMusic
npm run install-all
```
3.開発者モデルの起動
```sh
npm run dev
```
4.パッケージ項目
```sh
npm run build
```
5.プロジェクトのコンパイル
- Windows: 
```sh
npm run electron:build:win[デフォルトNSISインストールパッケージ]
```
-	Linux: 
```sh
npm run electron:build:linux[デフォルトAppImageフォーマット]
```
-	macOS: 
```sh
npm run electron:build:macos[デフォルトの双架構]
```


詳細なコマンドは、「package.json」ファイル「scripts」を参照してください。

## 👷‍♂️ クライアントのコンパイル

Releaseページであなたに適したデバイスのインストールパッケージが見つからない場合は、次の手順に従って自分のクライアントをパッケージ化することができます。

1. [ノード.js](https://nodejs.org/en/)を選択し、` Node.js `バージョン>=18.0.0であることを確認します。

2. を使用する `git clonehttps://github.com/iAJue/MoeKoeMusic.git`この倉庫をローカルにクローニングします。

3. `npm install `を使用してプロジェクト依存性をインストールします。
4. APIサービス端末のコンパイル
- Windows:
```sh
npm run build:api:win
```
- Linux:
```sh
npm run build:api:linux
```
- macOS:
```sh
npm run build:api:macos
```

5. 次のコマンドを選択して適切なインストールパッケージをパッケージ化し、パッケージ化されたファイルは`/dist _ electron `ディレクトリの下にあります。詳細については、[electron-builderドキュメント](https://www.electron.build/cli)


#### 1. パッケージmacOSプラットフォーム
- 汎用のmacOSパッケージ(IntelとApple Siliconデュアルアーキテクチャ):
```
npm run electron:build -- --mac --universal
```
- Intelアーキテクチャのみ:
```
npm run electron:build -- --mac --x64
```
- Apple Siliconアーキテクチャのみ:
```
npm run electron:build -- --mac --arm64
```


#### 2. Windowsプラットフォームのパッケージ化

- デフォルトNSISインストールパッケージ(ほとんどのWindowsユーザー向け):
```
npm run electron:build -- --win
```
- Windows用のEXEファイルとSquirrelインストールパッケージを作成するには:
```
npm run electron:build -- --win --ia32 --x64 --arm64 --target squirrel
```
---ia 32は32ビットWindowsアーキテクチャです。
---x 64は64ビットWindowsアーキテクチャです。
---arm 64はARM Windowsアーキテクチャ(Surfaceなどのデバイス)です。

- Windows用にポータブルEXEファイルを生成する(インストール不要):
```
npm run electron:build -- --win --portable
```
#### 3. Linuxプラットフォームのパッケージ化
- デフォルトのAppImageフォーマット(ほとんどのLinuxリリース用):
```
npm run electron:build -- --linux
```
- snap(Ubuntuおよびsnapをサポートするリリース用):
```
npm run electron:build -- --linux --target snap
```
- deb(Debian/Ubuntuシリーズ用):
```
npm run electron:build -- --linux --target deb
```
- rpm(Red Hat/Fedoraシリーズ用):
```
npm run electron:build -- --linux --target rpm
```

#### 4. すべてのプラットフォームをパッケージ化

Windows、macOS、Linuxのインストールパッケージを同時に生成する必要がある場合は、次のコマンドを使用します。
```
npm run electron:build -- -mwl
```

#### 5. コンパイル設定のカスタマイズ

必要に応じて他のオプションを追加して、パッケージをさらにカスタマイズすることができます。たとえば、x 64とarm 64スキーマを指定したり、異なるターゲットフォーマットを選択したりすることができます。


## ⭐ プロジェクトをサポート

このプロジェクトがお役に立った場合は、ぜひ星を付けてください!あなたのサポートが私たちの継続的な改善の原動力です。

[![GitHub stars](https://img.shields.io/github/stars/iAJue/MoeKoeMusic.svg?style=social&label=Star)](https://github.com/iAJue/MoeKoeMusic)


## ☑️ フィードバック

何か質問やアドバイスがあれば、issueまたはpull requestを提出してください。

### ⚠️ 免責事項
0. 本プログラムはクールドッグの第三者クライアントであり、クールドッグの公式ではなく、より完全な機能が必要な場合は公式クライアント体験をダウンロードしてください。
1. 本プロジェクトは学習用にのみ使用されます。著作権を尊重し、このプロジェクトを利用して商業行為や不正な用途に従事しないでください。
2. 本プロジェクトの使用中に著作権データが生成される可能性があります。これらの著作権データに対して、本プロジェクトは所有権を持っていません。権利侵害を回避するために、使用者は、本プロジェクトを使用する過程で生成された著作権データを24時間以内に消去しなければならない。
3. 本事業の使用により生じた、本契約書の使用または使用不能によるいかなる性質を含む、直接的、間接的、特殊、偶発的または結果的な損害(名誉損失、操業停止、コンピュータ故障または故障による損害賠償、またはその他あらゆる商業的損害または損失を含むがこれらに限定されない)は、使用者が責任を負う。          
1. 現地の法律法規に違反した場合の本事業の使用を禁止する。使用者が現地の法律法規で許可されていないことを知っているか知らないかのうちに本プロジェクトを使用したことによるいかなる違法行為も使用者が負担し、本プロジェクトはこれによる直接、間接、特殊、偶然、または結果的な責任を負わない。          
2. 音楽プラットフォームは容易ではありません。著作権を尊重し、正規版をサポートしてください。
3. 本プロジェクトは技術の実現可能性の探索と研究にのみ使用され、いかなる商業(広告などを含むがこれに限らない)の協力と寄付を受けない。
4. 公式音楽プラットフォームが本プロジェクトに不具合があると感じた場合は、本プロジェクトに連絡して変更または削除することができます。
            

## 📜 オープンソースライセンス

本プロジェクトは個人学習研究用にのみ使用され、商業用及び不法用途に使用することは禁止されている。

[MIT license](https://opensource.org/licenses/MIT)オープンソースを許可する。

### 👍 インスピレーションソース

APIソースコードは[MakcRe/KuGouMusicApi](https://github.com/MakcRe/KuGouMusicApi) 

- [Apple Music](https://music.apple.com)
- [YouTube Music](https://music.youtube.com)
- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)
-[クールドッグミュージック](https://kugou.com/)

## 🖼️ スクリーンショット

![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/2.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/3.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/4.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/5.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/6.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/7.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/8.png)


## 🗓️ スター履歴

[![Star History Chart](https://api.star-history.com/svg?repos=iAJue/MoeKoeMusic&type=Date)](https://www.star-history.com/#iAJue/MoeKoeMusic&Date)


================================================
FILE: docs/README_ko.md
================================================
> **주의**: 이 한국어 문서는 업데이트가 지연될 수 있으니, 최신 내용은 [중국어 간체 버전](https://github.com/iAJue/MoeKoeMusic/README.md)을 참고하세요.
<br />
<p align="center">
<img src="https://github.com/iAJue/MoeKoeMusic/raw/main/images/logo.png" alt="Logo" width="156" height="156">
<h2 align="center" style="font-weight: 600">MoeKoe Music</h2>
  <p align="center">
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/releases/latest"><img src="https://img.shields.io/github/v/release/MoeKoeMusic/MoeKoeMusic?style=flat-square" /></a>
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/stargazers"><img src="https://img.shields.io/github/stars/MoeKoeMusic/MoeKoeMusic?style=flat-square" /></a>
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/releases"><img src="https://img.shields.io/github/downloads/MoeKoeMusic/MoeKoeMusic/total?style=flat-square" /></a>
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/blob/main/LICENSE"><img src="https://img.shields.io/github/license/MoeKoeMusic/MoeKoeMusic?style=flat-square" /></a>
    <a href="https://github.com/iAJue"><img src="https://img.shields.io/badge/%F0%9F%8E%89_Create_by_iAJue-with_Love_%E2%9D%A4-pink?style=flat-square" /></a>
  </p>
<p align="center">
오픈 소스 간결하고 용모가 높은 쿨도그 제3자 클라이언트
<br />
<a href="https://github.com/iAJue/MoeKoeMusic/" target="blank"><strong>🌎 GitHub창고</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="https://github.com/iAJue/MoeKoeMusic/releases" target="blank"><strong>📦️ 설치 패키지 다운로드 </strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="https://MoeJue.cn" target="blank"><strong>💬 블로그 방문 </strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="https://Music.MoeKoe.cn" target="blank"><strong>🏠 프로젝트 홈페이지</strong></a>
</p>
<p align="center">
    <a href="https://github.com/iAJue/MoeKoeMusic/README.md" target="blank"><strong>🇨🇳 简体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_tw.md" target="blank"><strong>🇨🇳 繁体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ja.md" target="blank"><strong>🇯🇵 日本語</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_en.md" target="blank"><strong>🇺🇸 English</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ko.md" target="blank"><strong>🇰🇷 한국어</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ru.md" target="blank"><strong>🇷🇺 Русский</strong></a>
    <br />
    <br />
  </p>
</p>

![images](https://github.com/iAJue/MoeKoeMusic/raw/main/images/1.png)

## ❤️ 머리말

일찍이 10년 전후의 모습, 그것은 웹 페이지 QQ를 사용할 때 나는 이미 쿠거우 음악을 사용하기 시작했다 (또한 10여 년의 오랜 팬이다). 그래서 요 몇 년 동안 소장한 노래는 모두 위에 있다.후에 나도 왕이윈이나 QQ음악을 사용하기 시작했고 쿠거우의 노래 리스트를 도입하려고 시도했지만 효과가 모두 만족스럽지 못했다.내가 들은 것은 대부분 일본 만화 OP이다. 많은 노래를 찾을 수 없다.

빙빙 돌다가 결국 쿠거우로 돌아간다. 그러나 Mac에 있는 쿠거우는 종종 재생할 수 없는 상황이 나타날 수 있다. 비록 인터페이스는 아무런 기능이 없지만 아주 좋다.네티즌의 안리하에 나는 지금 줄곧 쿨개의 [개념판](https://t1.kugou.com/d2tBza3CSV2) 에서 노래를 듣고, 또한 시중에서 VIP 노래를 무료로 들을 수 있는 몇 안 되는 음악 재생 소프트웨어입니다.

나는 나의 개인 소개 페이지에서 내가 특히 노래 듣는 것을 좋아한다고 말했다. 특히 일본 만화 OP.어떻게 증명하죠?(이전에 내 웹페이지 플레이리스트도 오랫동안 수리를 하지 않았다.) 그럼 스스로 음악 플레이어를 하나 개발해라.


## ✨ 특징

- ✅ Vue.js 패밀리 버킷으로 개발
- 🔴 쿠거우 계정 로그인 (코드/핸드폰/계정 로그인)
- 📃 가사 표시 지원
- 📻 매일 추천곡
- 🚫🤝 소셜 기능 없음
- 🔗 공식 서버 직접 연결, 타사 API 없음
- ✔️ 매일 VIP 자동 수령, 로그인하면 VIP
- 🎨 테마 색상 전환
- 👋 시작 인사말
- ⚙️ 다중 플랫폼 지원
- 🛠 더 많은 기능 개발 중

## 📢 Todo List
- [x] 📺 뮤직비디오 재생 지원
- [x] 🌚 Light/Dark Mode 자동 전환
- [x] 👆 Touch Bar 지원
- [x] 🖥️ PWA 지원, Chrome/Edge에서 주소 표시줄 오른쪽에 있는➕ 컴퓨터에 설치
- [ ] 🎧 지원 Mpris
- [x] ⌨️ 단축키 및 전역 단축키 사용자 정의
- [x] 🤟 다국어 지원
- [x] 📻 데스크톱 가사
- [x] ⚙️ 시스템 아키텍처 최적화
- [x] 🎶 노래, 트랙 리스트 / 모음, 취소

로그를 업데이트하려면 [Commits](https://github.com/iAJue/MoeKoeMusic/commits/main/)

## 📦️ 설치

### 1. 클라이언트 설치

본 프로젝트의 [Releases](https://github.com/iAJue/MoeKoeMusic/releases) 페이지를 방문하여 설치 패키지를 다운로드하세요.

### 2. WEB 설치 (docker)

* 주의: 배포 후 서버의 해당 포트를 개방해야 사용할 수 있습니다. 또는 역방향 프록시를 사용하여 도메인 접근을 구현할 수 있습니다.

  1. 방법 1: 빠른 시작 (추천)

  ```
  git clone https://github.com/iAJue/MoeKoeMusic.git
  cd MoeKoeMusic
  git submodule update --init --recursive
  docker compose up -d &
  ```

  2. ~~방법 2: docker-compose를 사용한 원클릭 설치 (이미지 미업로드 중)~~
  
  ```
  docker run -d --name MoeKoeMusic -p 8080:8080 -p 6521:6521 -e PORT=6521 -e platform=lite iajue/moekoe-music:latest
  ```

  3. 방법 3: 바오타 컨테이너 오케스트레이션

  * 원격 이미지, 버전이 공식보다 뒤떨어질 수 있음
  
  ```
  version: '3.3'
  
  services:
    moekoe-music:
    # 이미지 주소
    image: registry.cn-wulanchabu.aliyuncs.com/youngxj/moekoe-music:latest
    container_name: moekoe-music # 컨테이너명
    restart: unless-stopped # 자동 재시작
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      - PORT=6521
      - platform=lite
    ports: # 포트 매핑
      - "8080:8080"  # 프론트엔드 서비스
      - "6521:6521"  # API 서비스
  
  ```
  
  위의 내용을 복사하여 바오타 패널의 컨테이너 오케스트레이션에 붙여넣고, 오케스트레이션 이름을 MoeKoeMusic으로 설정한 후 배포를 클릭하면 됩니다.
### 3. 원클릭 배포
[![使用 EdgeOne Pages 部署](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?template=https://github.com/iAJue/moekoemusic&install-command=npm%20install&output-directory=dist&root-directory=.%2F&build-command=npm%20run%20build&env=VITE_APP_API_URL)

환경 변수 VITE_APP_API_URL에 본인의 API 주소를 입력해야 합니다.

## ⚙️ 개발

1. 본 창고 클론

```sh
git clone --recurse-submodules https://github.com/iAJue/MoeKoeMusic.git
```

2. 디렉터리에 들어가서 종속성 설치

```sh
cd MoeKoeMusic
npm run install-all
```
3. 개발자 모드 시작
```sh
npm run dev
```
4. 프로젝트 패키지
```sh
npm run build
```
5. 항목 컴파일
- Windows: 
```sh
npm run electron:build:win [기본 NSIS 설치 패키지]
```
-	Linux: 
```sh
npm run electron:build:linux [기본 AppImage 형식]
```
-	macOS: 
```sh
npm run electron:build:macos [기본 듀얼 아키텍처]
```


더 많은 명령은 `package.json` 파일 `scripts`를 참조하십시오.

## 👷‍♂️ 클라이언트 컴파일

Release 페이지에서 장치에 맞는 설치 패키지를 찾지 못하면 다음 단계에 따라 클라이언트를 포장할 수 있습니다.

1. 설치[Node.js](https://nodejs.org/en/) 및 `Node.js` 버전이 > = 18.0.0인지 확인합니다.

2. `git clone 사용https://github.com/iAJue/MoeKoeMusic.git'본 창고를 로컬로 복제합니다.

3. `npm install`을 사용하여 프로젝트 종속성을 설치합니다.
4. API 서버 컴파일
- Windows:
```sh
npm run build:api:win
```
- Linux:
```sh
npm run build:api:linux
```
- macOS:
```sh
npm run build:api:macos
```

1. 다음 명령을 선택하여 적합한 설치 패키지를 포장합니다. 포장된 파일은'/dist_electron'디렉터리에 있습니다.자세한 내용은 [electron-builder 문서](https://www.electron.build/cli)


#### 1. macOS 플랫폼 패키지
- 범용 macOS 패키지(Intel 및 Apple Silicon 듀얼 아키텍처):
```
npm run electron:build -- --mac --universal
```
- Intel 아키텍처만:
```
npm run electron:build -- --mac --x64
```
- Apple Silicon 아키텍처만:
```
npm run electron:build -- --mac --arm64
```


#### 2. Windows 플랫폼 패키지

- 기본 NSIS 설치 패키지(대부분의 Windows 사용자용):
```
npm run electron:build -- --win
```
- Windows용 EXE 파일 및 Squirrel 설치 패키지를 만듭니다.
```
npm run electron:build -- --win --ia32 --x64 --arm64 --target squirrel
```
-- ia32는 32비트 Windows 아키텍처입니다.

---x64는 64비트 Windows 아키텍처입니다.

-- arm64는 ARM Windows 아키텍처(Surface와 같은 장치)입니다.

- Windows용 휴대용 EXE 파일(설치되지 않음) 생성:
```
npm run electron:build -- --win --portable
```

#### 3. Linux 플랫폼 패키지
- 기본 AppImage 형식(대부분의 Linux 배포용):
```
npm run electron:build -- --linux
```
- snap(Ubuntu 및 snap 지원 릴리스용):
```
npm run electron:build -- --linux --target snap
```
- deb(Debian/Ubuntu 시리즈용):
```
npm run electron:build -- --linux --target deb
```
- rpm(Red Hat/Fedora 시리즈용):
```
npm run electron:build -- --linux --target rpm
```

#### 4. 모든 플랫폼 패키지

Windows, macOS 및 Linux를 모두 생성하는 설치 패키지가 필요한 경우 다음 명령을 사용할 수 있습니다.
```
npm run electron:build -- -mwl
```

#### 5. 컴파일 설정 사용자 정의

x64 및 arm64 스키마를 지정하거나 다른 대상 형식을 선택하는 등의 추가 옵션을 추가하여 패키지를 추가로 사용자 지정할 수 있습니다.


## ⭐ 프로젝트 지원

이 프로젝트가 도움이 되었다면 별을 눌러주세요! 여러분의 지원이 저희가 계속 개선할 수 있는 원동력입니다.

[![GitHub stars](https://img.shields.io/github/stars/iAJue/MoeKoeMusic.svg?style=social&label=Star)](https://github.com/iAJue/MoeKoeMusic)


## ☑️ 피드백

질문이나 제안이 있으면 issue 또는 pull request를 제출하십시오.

## ⚠️ 면책 조항
0. 본 프로그램은 쿠거우 제3자 클라이언트입니다. 쿠거우 공식이 아닙니다. 더 완벽한 기능이 필요하시면 공식 클라이언트 체험을 다운로드하십시오.
1.본 프로젝트는 학습용입니다.저작권을 존중하고 이 프로젝트를 상업행위 및 불법용도로 이용하지 마십시오!
2. 본 프로젝트를 사용하는 과정에서 저작권 데이터가 발생할 수 있습니다.본 프로젝트는 이러한 저작권 데이터에 대한 소유권이 없습니다.저작권 침해를 방지하기 위해 사용자는 본 프로젝트를 사용하는 과정에서 발생하는 저작권 데이터를 24시간 이내에 삭제해야 합니다.
3. 본 프로젝트의 사용으로 인해 발생하는 본 계약 또는 본 프로젝트의 사용 또는 사용 불가능으로 인해 발생하는 모든 성격을 포함하는 직접, 간접, 특수, 우연 또는 결과적 손해(상업권 손실, 업무 정지, 컴퓨터 고장 또는 고장으로 인한 손해 배상 또는 기타 모든 상업적 손해 또는 손실을 포함하되 이에 국한되지 않음)는 사용자의 책임이다.
            
1. 현지 법률과 법규를 위반한 경우 본 프로젝트의 사용을 금지한다.사용자가 현지 법률과 법규가 허용하지 않는 것을 뻔히 알거나 모르는 상황에서 본 프로젝트를 사용하여 초래된 어떠한 위법 행위도 사용자가 부담하고 본 프로젝트는 이로 인해 초래된 어떠한 직접, 간접, 특수, 우연 또는 결과적 책임을 지지 않는다.
            
2. 음악 플랫폼은 쉽지 않습니다.저작권을 존중하고 정품을 지원하십시오.
3. 본 프로젝트는 기술적 타당성에 대한 탐구 및 연구에만 사용되며 어떠한 상업(광고 등을 포함하되 이에 국한되지 않음) 합작과 기부도 받지 않습니다.
4.공식 음악 플랫폼이 본 프로젝트가 부적절하다고 생각되면 본 프로젝트에 연락하여 변경하거나 제거할 수 있습니다.
            

## 📜 오픈 소스 라이센스

본 프로젝트는 개인의 학습 연구에만 사용되며 상업 및 불법 용도로 사용되는 것을 금지합니다.

기반 [MIT license](https://opensource.org/licenses/MIT) 오픈 소스로 라이센스를 부여합니다.

## 👍 영감의 원천

API 소스 코드는 [MakcRe/KuGouMusicApi](https://github.com/MakcRe/KuGouMusicApi) 

- [Apple Music](https://music.apple.com)
- [YouTube Music](https://music.youtube.com)
- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)
- [쿨도그 뮤직](https://kugou.com/)

## 🖼️ 캡처

![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/2.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/3.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/4.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/5.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/6.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/7.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/8.png)

## 🗓️ 스타 히스토리

[![Star History Chart](https://api.star-history.com/svg?repos=iAJue/MoeKoeMusic&type=Date)](https://www.star-history.com/#iAJue/MoeKoeMusic&Date)


================================================
FILE: docs/README_ru.md
================================================
> **Note**: Этот документ на русском языке может не быть актуальным вовремя. Для актуального материала, пожалуйста, обратитесь к [версии на упрощенном китайском](https://github.com/iAJue/MoeKoeMusic/README.md).
<br />
<p align="center">
    <img src="https://github.com/iAJue/MoeKoeMusic/raw/main/images/logo.png" alt="Logo" width="156" height="156">
  <h2 align="center" style="font-weight: 600">MoeKoe Music</h2>
  <p align="center">
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/releases/latest"><img src="https://img.shields.io/github/v/release/MoeKoeMusic/MoeKoeMusic?style=flat-square" /></a>
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/stargazers"><img src="https://img.shields.io/github/stars/MoeKoeMusic/MoeKoeMusic?style=flat-square" /></a>
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/releases"><img src="https://img.shields.io/github/downloads/MoeKoeMusic/MoeKoeMusic/total?style=flat-square" /></a>
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/blob/main/LICENSE"><img src="https://img.shields.io/github/license/MoeKoeMusic/MoeKoeMusic?style=flat-square" /></a>
    <a href="https://github.com/iAJue"><img src="https://img.shields.io/badge/%F0%9F%8E%89_Create_by_iAJue-with_Love_%E2%9D%A4-pink?style=flat-square" /></a>
  </p>
  <p align="center">
    Открытый, лаконичный и красивый сторонний клиент для Kugou
    <br />
    <a href="https://github.com/iAJue/MoeKoeMusic/" target="blank"><strong>GitHub</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/releases" target="blank"><strong>Скачать</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://MoeJue.cn" target="blank"><strong>Блог</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://Music.MoeKoe.cn" target="blank"><strong>Сайт проекта</strong></a>
  </p>
  <p align="center">
    <a href="https://github.com/iAJue/MoeKoeMusic/README.md" target="blank"><strong>简体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_tw.md" target="blank"><strong>繁体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ja.md" target="blank"><strong>日本語</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_en.md" target="blank"><strong>English</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ko.md" target="blank"><strong>한국어</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ru.md" target="blank"><strong>Русский</strong></a>
    <br />
    <br />
  </p>
</p>

![images](https://github.com/iAJue/MoeKoeMusic/raw/main/images/1.png)

## ❤️ Предисловие

Ещё около 10 лет назад, когда я пользовался веб-версией QQ, я начал использовать Kugou Music (уже более десяти лет как фанат). За эти годы вся моя коллекция песен оказалась именно там. Позже я пытался перейти на NetEase Cloud или QQ Music и даже импортировать плейлисты из Kugou, но результаты были неудовлетворительными. Я в основном слушаю опенинги аниме, и многие треки просто невозможно найти на других платформах.

В итоге я вернулся к Kugou. Однако на Mac-версии Kugou периодически возникали проблемы с воспроизведением. По рекомендации друзей я теперь слушаю музыку через [концептуальную версию](https://t1.kugou.com/d2tBza3CSV2) Kugou — это одно из немногих приложений, где можно бесплатно слушать VIP-треки. Очень рекомендую.

На своей странице я писал, что очень люблю слушать музыку, особенно опенинги аниме. Как это доказать? Просто разработать собственный музыкальный плеер.


## ✨ Особенности

- ✅ Разработан на Vue.js
- 🔴 Авторизация через аккаунт Kugou (QR-код/телефон/логин)
- 📃 Отображение текстов песен
- 📻 Ежедневные рекомендации
- 🚫🤝 Никаких социальных функций
- 🔗 Прямое подключение к официальным серверам, без сторонних API
- ✔️ Автоматическое получение VIP ежедневно
- 🎨 Переключение цветовых тем
- 👋 Приветствие при запуске
- ⚙️ Поддержка нескольких платформ
- 🛠 Больше функций в разработке

## 📢 Список задач
- [x] 📺 Поддержка воспроизведения MV
- [x] 🌚 Автоматическое переключение Light/Dark Mode
- [x] 👆 Поддержка Touch Bar
- [x] 🖥️ Поддержка PWA — можно установить через Chrome/Edge, нажав ➕ в адресной строке
- [ ] 🎧 Поддержка Mpris
- [x] ⌨️ Глобальные горячие клавиши
- [x] 🤟 Мультиязычность
- [x] 📻 Текст песни на рабочем столе
- [x] ⚙️ Оптимизация архитектуры
- [x] 🎶 Добавление/удаление песен и плейлистов в избранное

История изменений: [Commits](https://github.com/iAJue/MoeKoeMusic/commits/main/)

## 📦️ Установка

### 1. Установка клиента

Скачайте установочный пакет на странице [Releases](https://github.com/iAJue/MoeKoeMusic/releases).

### 2. WEB-версия (Docker)

* Примечание: после развёртывания откройте соответствующий порт на сервере или используйте обратный прокси.

    1. Способ 1: Быстрый запуск (рекомендуется)

    ```
    git clone https://github.com/iAJue/MoeKoeMusic.git
    cd MoeKoeMusic
    git submodule update --init --recursive
    docker compose up -d &
    ```

    2. ~~Способ 2: docker-compose (образ пока не загружен официально)~~

    ```
    docker run -d --name MoeKoeMusic -p 8080:8080 -p 6521:6521 -e PORT=6521 -e platform=lite iajue/moekoe-music:latest
    ```

    3. Способ 3: Через панель управления

    * Удалённый образ, версия может отставать от официальной

    ```
    version: '3.3'

    services:
      moekoe-music:
        image: registry.cn-wulanchabu.aliyuncs.com/youngxj/moekoe-music:latest
        container_name: moekoe-music
        restart: unless-stopped
        build:
          context: .
          dockerfile: Dockerfile
        environment:
          - PORT=6521
          - platform=lite
        ports:
          - "8080:8080"  # Фронтенд
          - "6521:6521"  # API

    ```
### 3. Развертывание в один клик
[![Развернуть на EdgeOne Pages](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?template=https://github.com/iAJue/moekoemusic&install-command=npm%20install&output-directory=dist&root-directory=.%2F&build-command=npm%20run%20build&env=VITE_APP_API_URL)

Укажите свой API-адрес в переменной окружения (VITE_APP_API_URL)

## ⚙️ Разработка

1. Клонируйте репозиторий

```sh
git clone --recurse-submodules https://github.com/iAJue/MoeKoeMusic.git
```

2. Перейдите в директорию и установите зависимости

```sh
cd MoeKoeMusic
npm run install-all
```
3. Запустите режим разработки
```sh
npm run dev
```
4. Сборка проекта
```sh
npm run build
```
5. Компиляция
  - Windows:
  ```sh
  npm run electron:build:win
  ```
  - Linux:
  ```sh
  npm run electron:build:linux
  ```
  - macOS:
  ```sh
  npm run electron:build:macos
  ```


Больше команд в файле `package.json` в секции `scripts`

## 👷‍♂️ Сборка клиента

Если на странице Release нет подходящего пакета для вашего устройства, вы можете собрать клиент самостоятельно.

1. Установите [Node.js](https://nodejs.org/en/) версии >= 18.0.0.

2. Клонируйте репозиторий: `git clone https://github.com/iAJue/MoeKoeMusic.git`

3. Установите зависимости: `npm install`
4. Скомпилируйте API-сервер
    - Windows:
        ```sh
        npm run build:api:win
        ```
    - Linux:
        ```sh
        npm run build:api:linux
        ```
    - macOS:
      ```sh
      npm run build:api:macos
      ```

5. Выберите команду для сборки. Результат будет в директории `/dist_electron`. Подробнее: [electron-builder](https://www.electron.build/cli)


#### 1. macOS
   - Универсальный пакет (Intel и Apple Silicon):
   ```
   npm run electron:build -- --mac --universal
   ```
   - Только Intel:
   ```
   npm run electron:build -- --mac --x64
   ```
   - Только Apple Silicon:
   ```
   npm run electron:build -- --mac --arm64
   ```


#### 2. Windows

   - NSIS-установщик (для большинства пользователей):
   ```
   npm run electron:build -- --win
   ```
   - EXE + Squirrel:
   ```
   npm run electron:build -- --win --ia32 --x64 --arm64 --target squirrel
   ```
   - Портативная версия (без установки):
   ```
   npm run electron:build -- --win --portable
   ```
#### 3. Linux
   - AppImage (для большинства дистрибутивов):
   ```
   npm run electron:build -- --linux
   ```
   - snap (Ubuntu и совместимые):
   ```
   npm run electron:build -- --linux --target snap
   ```
   - deb (Debian/Ubuntu):
   ```
   npm run electron:build -- --linux --target deb
   ```
   - rpm (Red Hat/Fedora):
   ```
   npm run electron:build -- --linux --target rpm
   ```
   - ARM64:
   ```
   npm run build:api:linux-aarch64
   npm run electron:build:linux-aarch64
   ```

#### 4. Все платформы сразу
  ```
  npm run electron:build -- -mwl
  ```

#### 5. Дополнительные настройки

Вы можете добавить другие опции для настройки сборки, например, указать архитектуру x64 или arm64.

## ⭐ Поддержать проект

Если проект оказался полезным, поставьте Star! Ваша поддержка мотивирует нас продолжать развитие.

[![GitHub stars](https://img.shields.io/github/stars/iAJue/MoeKoeMusic.svg?style=social&label=Star)](https://github.com/iAJue/MoeKoeMusic)

## ✅ Обратная связь

По любым вопросам или предложениям создавайте issue или pull request.

## ⚠️ Отказ от ответственности
0. Это сторонний клиент для Kugou, не официальное приложение. Для полного функционала используйте официальный клиент.
1. Проект предназначен только для обучения. Уважайте авторские права и не используйте его в коммерческих или незаконных целях!
2. При использовании проекта могут создаваться данные, защищённые авторским правом. Проект не владеет этими данными. Во избежание нарушения авторских прав удаляйте такие данные в течение 24 часов.
3. Пользователь несёт ответственность за любой ущерб, возникший в результате использования проекта.
4. Запрещено использовать проект в нарушение местного законодательства.
5. Музыкальные платформы заслуживают поддержки. Уважайте авторские права, поддерживайте легальный контент.
6. Проект предназначен только для исследования технических возможностей. Коммерческое сотрудничество и пожертвования не принимаются.
7. Если правообладатели считают проект неуместным, свяжитесь с нами для изменения или удаления.


## 📜 Лицензия

Проект предназначен только для личного изучения. Коммерческое и незаконное использование запрещено.

Лицензия: [GNU General Public License v2.0 (GPL-2.0)](https://github.com/iAJue/MoeKoeMusic/blob/main/LICENSE)

## 👍 Источники вдохновения

Исходный код API: [MakcRe/KuGouMusicApi](https://github.com/MakcRe/KuGouMusicApi)

- [Apple Music](https://music.apple.com)
- [YouTube Music](https://music.youtube.com)
- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)
- [Kugou Music](https://kugou.com/)

## 🖼️ Скриншоты

![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/2.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/3.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/4.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/5.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/6.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/7.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/8.png)


## 🗓️ Star History

[![Star History Chart](https://api.star-history.com/svg?repos=iAJue/MoeKoeMusic&type=Date)](https://www.star-history.com/#iAJue/MoeKoeMusic&Date)


================================================
FILE: docs/README_tw.md
================================================
> **注意**: 此繁體中文文檔可能更新不及時,最新內容請參考[簡體中文版本](https://github.com/iAJue/MoeKoeMusic/README.md)。
<br />
<p align="center">
<img src="https://github.com/iAJue/MoeKoeMusic/raw/main/images/logo.png "alt="Logo"width="156"height="156">
<h2 align="center"style="font-weight: 600">MoeKoe Music</h2>
  <p align="center">
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/releases/latest"><img src="https://img.shields.io/github/v/release/MoeKoeMusic/MoeKoeMusic?style=flat-square" /></a>
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/stargazers"><img src="https://img.shields.io/github/stars/MoeKoeMusic/MoeKoeMusic?style=flat-square" /></a>
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/releases"><img src="https://img.shields.io/github/downloads/MoeKoeMusic/MoeKoeMusic/total?style=flat-square" /></a>
    <a href="https://github.com/MoeKoeMusic/MoeKoeMusic/blob/main/LICENSE"><img src="https://img.shields.io/github/license/MoeKoeMusic/MoeKoeMusic?style=flat-square" /></a>
    <a href="https://github.com/iAJue"><img src="https://img.shields.io/badge/%F0%9F%8E%89_Create_by_iAJue-with_Love_%E2%9D%A4-pink?style=flat-square" /></a>
  </p>
<p align="center">
一款開源簡潔高顏值的酷狗協力廠商用戶端
<br />
<a href="https://github.com/iAJue/MoeKoeMusic/" target="blank"><strong>🌎 GitHub 倉庫</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="https://github.com/iAJue/MoeKoeMusic/releases" target="blank"><strong>📦️ 下載安裝包</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="https://MoeJue.cn" target="blank"><strong>💬 訪問部落格</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="https://Music.MoeKoe.cn" target="blank"><strong>🏠 項目主頁</strong></a>
</p>
<p align="center">
    <a href="https://github.com/iAJue/MoeKoeMusic/README.md" target="blank"><strong>🇨🇳 简体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_tw.md" target="blank"><strong>🇨🇳 繁体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ja.md" target="blank"><strong>🇯🇵 日本語</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_en.md" target="blank"><strong>🇺🇸 English</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ko.md" target="blank"><strong>🇰🇷 한국어</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
    <a href="https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ru.md" target="blank"><strong>🇷🇺 Русский</strong></a>
    <br />
    <br />
</p>
</p>

![images](https://github.com/iAJue/MoeKoeMusic/raw/main/images/1.png)

## ❤️ 前言

早在10年前後的樣子,那會在用網頁版QQ的時候我就已經開始使用酷狗音樂了(也是十來年的老粉了),所以這些年收藏的歌曲全部都在上面. 後來我也嘗試開始使用網易雲或QQ音樂,也嘗試把酷狗的歌單導入進去,但是效果都不盡人意. 我聽的大多是日漫OP,好多歌曲都沒辦法找到.

兜兜轉轉最後還是回到酷狗,但是在Mac端的酷狗,時常可能會出現不能播放的情况,雖說介面沒什麼功能,但也挺好的. 在網友的安利下,我現在一直是在酷狗的[概念版](https://t1.kugou.com/d2tBza3CSV2)上聽歌,並且是市面上為數不多能免費聽VIP歌曲的音樂播放軟體了,力推.

我在我的個人介紹頁面說我特別喜歡聽歌,尤其是日漫OP. 怎麼證明呢? (之前我網頁版歌單也年久失修了)那就自己開發一個音樂播放機.


## ✨  特性

- ✅  使用Vue.js全家桶開發
- 🔴  酷狗帳號登入(掃碼/手機/帳號登入)
- 📃  支持歌詞顯示
- 📻  每日推薦歌曲
- 🚫🤝  無任何社交功能
- 🔗  官方服務器直連,無任何協力廠商API
- ✔️  每日自動領取VIP,登入就是VIP
- 🎨  主題色切換
- 👋  啟動問候語
- ⚙️  多平臺支持
- 🛠  更多特性開發中

## 📢 Todo List
- [x] 📺  支持MV播放
- [x] 🌚 Light/Dark Mode  自動切換
- [x] 👆  支持Touch Bar
- [x] 🖥️  支持PWA,可在Chrome/Edge裏點擊地址欄右邊的 ➕  安裝到電腦
- [ ] 🎧  支持Mpris
- [x] ⌨️  全域快速鍵
- [x] 🤟  多語言支持
- [x] 📻  案頭歌詞
- [x] ⚙️  系統架構優化
- [x] 🎶  歌曲、歌單/收藏、取消

更新日誌請查看[Commits](https://github.com/iAJue/MoeKoeMusic/commits/main/)

## 📦️ 安裝

### 1. 用戶端安裝

訪問本項目的 [Releases](https://github.com/iAJue/MoeKoeMusic/releases) 頁面下載安裝包。

### 2. WEB端安裝(docker)

* 注意:部署後請開放伺服器對應埠才可使用,或者使用反向代理實現功能變數名稱訪問。

    1. 方式一:快速啟動(推薦)

    ```
    git clone https://github.com/iAJue/MoeKoeMusic.git
    cd MoeKoeMusic
    git submodule update --init --recursive
    docker compose up -d &
    ```

    2. ~~方式二:使用docker-compose一鍵安裝 (映像暫未上傳官方)~~
    
    ```
    docker run -d --name MoeKoeMusic -p 8080:8080 -p 6521:6521 -e PORT=6521 -e platform=lite iajue/moekoe-music:latest
    ```

    3. 方式三:寶塔容器編排

    * 遠端映像,版本可能會落後於官方
    
    ```
    version: '3.3'
    
    services:
      moekoe-music:
        # 映像地址
        image: registry.cn-wulanchabu.aliyuncs.com/youngxj/moekoe-music:latest
        container_name: moekoe-music # 容器名
        restart: unless-stopped # 自動重啟
        build:
          context: .
          dockerfile: Dockerfile
        environment:
          - PORT=6521
          - platform=lite
        ports: # 埠映射
          - "8080:8080"  # 前端服務
          - "6521:6521"  # 介面服務
    
    ```
    
    複製上面的內容,貼上到寶塔面板的容器編排裡面,編排名稱為MoeKoeMusic,點擊部署即可。
### 3. 一鍵部署
[![使用 EdgeOne Pages 部署](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?template=https://github.com/iAJue/moekoemusic&install-command=npm%20install&output-directory=dist&root-directory=.%2F&build-command=npm%20run%20build&env=VITE_APP_API_URL)

需在環境變數(VITE_APP_API_URL)中填寫自己的API地址

## ⚙️  開發

1.尅隆本倉庫

```sh
git clone --recurse-submodules https://github.com/iAJue/MoeKoeMusic.git
```

2.進入目錄並安裝依賴

```sh
cd MoeKoeMusic
npm run install-all
```
3.啟動開發者模式
```sh
npm run dev
```
4.打包項目
```sh
npm run build
```
5.編譯項目
- Windows:
```sh
npm run electron:build:win [默認NSIS安裝包]
```
- Linux:
```sh
npm run electron:build:linux [默認AppImage格式]
```
- macOS:
```sh
npm run electron:build:macos [默認雙架構]
```


更多命令請查看`package.json`檔案`scripts`

## 👷‍♂️  編譯用戶端

如果在Release頁面沒有找到適合你的設備的安裝包的話,你可以根據下麵的步驟來打包自己的用戶端。

1.安裝[Node.js](https://nodejs.org/en/),並確保`Node.js`版本>= 18.0.0。

2.使用`git clone https://github.com/iAJue/MoeKoeMusic.git `尅隆本倉庫到本地。

3.使用`npm install`安裝項目依賴。
4.編譯API服務端
- Windows:
```sh
npm run build:api:win
```
- Linux:
```sh
npm run build:api:linux
```
- macOS:
```sh
npm run build:api:macos
```

5.選擇下列的命令來打包適合的你的安裝包,打包出來的檔案在`/dist_electron`目錄下。 瞭解更多資訊可訪問[electron-builder檔案](https://www.electron.build/cli)


#### 1. 打包macOS平臺
-通用的macOS包(Intel和Apple Silicon雙架構):
```
npm run electron:build -- --mac --universal
```
-僅Intel架構:
```
npm run electron:build -- --mac --x64
```
-僅Apple Silicon架構:
```
npm run electron:build -- --mac --arm64
```


#### 2. 打包Windows平臺

-默認NSIS安裝包(適合大多數Windows用戶):
```
npm run electron:build -- --win
```
-為Windows創建EXE檔案和Squirrel安裝包:
```
npm run electron:build -- --win --ia32 --x64 --arm64 --target squirrel
```
---ia32為32比特Windows架構。
---x64為64比特Windows架構。
---arm64為ARM Windows架構(Surface等設備)。

-為Windows生成可擕式的EXE檔案(免安裝):
```
npm run electron:build -- --win --portable
```
#### 3. 打包Linux平臺
-默認AppImage格式(適用於大多數Linux發行版本):
```
npm run electron:build -- --linux
```
- snap(適用於Ubuntu和支持snap的發行版本):
```
npm run electron:build -- --linux --target snap
```
- deb(適用於Debian/Ubuntu系列):
```
npm run electron:build -- --linux --target deb
```
- rpm(適用於Red Hat/Fedora系列):
```
npm run electron:build -- --linux --target rpm
```

#### 4. 打包所有平臺

如果需要同時生成Windows、macOS和Linux的安裝包,可以使用以下命令:
```
npm run electron:build -- -mwl
```

#### 5. 自定義編譯設定

您可以根據需要添加其他選項來進一步自定義打包,例如指定x64和arm64架構,或選擇不同的目標格式。


## ⭐ 支持項目

如果您覺得這個項目對您有幫助,歡迎給我們一個 Star!您的支持是我們持續改進的動力。

[![GitHub stars](https://img.shields.io/github/stars/iAJue/MoeKoeMusic.svg?style=social&label=Star)](https://github.com/iAJue/MoeKoeMusic)


## ☑️  反饋

如有任何問題或建議,歡迎提交issue或pull request。

## ⚠️ 免責聲明
0. 本程式是酷狗協力廠商用戶端,並非酷狗官方,需要更完善的功能請下載官方用戶端體驗.
1. 本項目僅供學習使用,請尊重版權,請勿利用此項目從事商業行為及非法用途!
2. 使用本項目的過程中可能會產生版權數據。 對於這些版權數據,本項目不擁有它們的所有權。 為了避免侵權,使用者務必在24小時內清除使用本項目的過程中所產生的版權數據。
3. 由於使用本項目產生的包括由於本協定或由於使用或無法使用本項目而引起的任何性質的任何直接、間接、特殊、偶然或結果性損害(包括但不限於因商譽損失、停工、電腦故障或故障引起的損害賠償,或任何及所有其他商業損害或損失)由使用者負責。         
1. 禁止在違反當地法律法規的情况下使用本項目。 對於使用者在明知或不知當地法律法規不允許的情况下使用本項目所造成的任何違法違規行為由使用者承擔,本項目不承擔由此造成的任何直接、間接、特殊、偶然或結果性責任。
            
2. 音樂平臺不易,請尊重版權,支持正版。
3. 本項目僅用於對科技可行性的探索及研究,不接受任何商業(包括但不限於廣告等)合作及捐贈。
4. 如果官方音樂平臺覺得本項目不妥,可聯系本項目更改或移除。
            

## 📜  開源許可

本項目僅供個人學習研究使用,禁止用於商業及非法用途。

基於[MIT license](https://opensource.org/licenses/MIT)許可進行開源。

## 👍 靈感來源

API原始程式碼來自[MakcRe/KuGouMusicApi](https://github.com/MakcRe/KuGouMusicApi)

- [Apple Music](https://music.apple.com)
- [YouTube Music](https://music.youtube.com)
- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)
- [酷狗音樂](https://kugou.com/)

## 🖼️  截圖

![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/2.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/3.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/4.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/5.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/6.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/7.png)
![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/8.png)

## 🗓️ Star 歷史

[![Star History Chart](https://api.star-history.com/svg?repos=iAJue/MoeKoeMusic&type=Date)](https://www.star-history.com/#iAJue/MoeKoeMusic&Date)


================================================
FILE: electron/appServices.js
================================================
import { app, ipcMain, BrowserWindow, screen, Tray, Menu, TouchBar, globalShortcut, dialog, shell, nativeImage } from 'electron';
import path from 'path';
import { spawn } from 'child_process';
import log from 'electron-log';
import Store from 'electron-store';
import { fileURLToPath } from 'url';
import isDev from 'electron-is-dev';
import fs from 'fs';
import { exec } from 'child_process';
import { checkForUpdates } from './services/updater.js';
import { Notification } from 'electron';
import extensionManager from './extensions/extensionManager.js';
import { t } from './language/i18n.js';
import { bindExternalLinkHandler } from './services/externalLinkHandler.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const store = new Store();
const { TouchBarLabel, TouchBarButton, TouchBarGroup, TouchBarSpacer } = TouchBar;
let mainWindow = null;
let apiProcess = null;
let tray = null;

// 创建主窗口
export function createWindow() {
    const savedConfig = store.get('settings');
    const useNativeTitleBar = savedConfig?.nativeTitleBar === 'on' ? true : false;
    const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;

    const windowWidth = Math.min(1200, screenWidth * 0.8);
    const windowHeight = Math.min(938, screenHeight * 0.9);
    const lastWindowState = store.get('windowState') || {};

    let x = lastWindowState.x;
    let y = lastWindowState.y;
    let width = lastWindowState.width || windowWidth;
    let height = lastWindowState.height || windowHeight;

    width = Math.min(width, screenWidth);
    height = Math.min(height, screenHeight);

    const isValidPosition = x !== undefined && y !== undefined &&
        x >= 0 && x <= screenWidth &&
        y >= 0 && y <= screenHeight;

    if (!isValidPosition) {
        x = Math.floor((screenWidth - width) / 2);
        y = Math.floor((screenHeight - height) / 2);
    }

    mainWindow = new BrowserWindow({
        width: width,
        height: height,
        x: x,
        y: y,
        minWidth: 890,
        minHeight: 750,
        show: savedConfig?.startMinimized === 'on' ? false : true,
        frame: useNativeTitleBar,
        titleBarStyle: useNativeTitleBar ? 'default' : 'hiddenInset',
        autoHideMenuBar: true,
        webPreferences: {
            preload: path.join(__dirname, 'preload.cjs'),
            contextIsolation: true,
            nodeIntegration: false,
            sandbox: false,
            webSecurity: false, // 禁用 CORS、同源策略
            allowRunningInsecureContent: true, // 允许混合内容
            zoomFactor: 1.0
        },
        icon: getIconPath('icon.ico')
    });
    bindExternalLinkHandler(mainWindow);

    if (store.get('maximize')) {
        mainWindow.maximize();
    }

    if (isDev) {
        mainWindow.loadURL('http://localhost:8080');
        mainWindow.webContents.openDevTools();
    } else {
        if (savedConfig?.networkMode == 'devnet') { //开发网
            mainWindow.loadURL('http://localhost:8080');
        } else if (savedConfig?.networkMode == 'testnet') { //测试网
            mainWindow.loadURL('https://app.testnet.music.moekoe.cn');
        } else { //主网
            mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));
        }
    }

    mainWindow.webContents.once('dom-ready', () => {
        extensionManager.loadChromeExtensions();
    });

    mainWindow.webContents.on('dom-ready', () => {
        console.log('DOM Ready');
    });

    mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => {
        console.error('Failed to load:', errorCode, errorDescription);
    });

    mainWindow.once('ready-to-show', () => {
        if (savedConfig?.startMinimized === 'on') {
            mainWindow.hide();
        }
    });

    mainWindow.webContents.on('did-finish-load', () => {
        console.log('Page Loaded Successfully');
        mainWindow.webContents.insertCSS('::-webkit-scrollbar { display: none; }');
        if (!store.get('disclaimerAccepted')) {
            mainWindow.webContents.send('show-disclaimer');
        }
        mainWindow.webContents.send('version', app.getVersion());
    });

    mainWindow.on('close', (event) => {
        const savedConfig = store.get('settings');
        if (savedConfig?.minimizeToTray === 'off') {
            app.isQuitting = true;
            app.quit();
        }
        if (!app.isQuitting) {
            event.preventDefault();
            mainWindow.hide();
        }
    });

    if (process.platform === 'win32') {
        setThumbarButtons(mainWindow);
    }

    if (savedConfig?.desktopLyrics === 'on') {
        createLyricsWindow();
    }
    return mainWindow;
}

let lyricsWindow;

export function createLyricsWindow() {
    const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
    const windowWidth = Math.floor(screenWidth * 0.7);
    const windowHeight = 200;

    const savedLyricsPosition = store.get('lyricsWindowPosition') || {};
    const savedLyricsSize = store.get('lyricsWindowSize') || {
        width: windowWidth,
        height: windowHeight
    };

    let x = savedLyricsPosition.x;
    let y = savedLyricsPosition.y;
    let width = savedLyricsSize.width || windowWidth;
    let height = savedLyricsSize.height || windowHeight;

    // 限制窗口尺寸不超过屏幕
    width = Math.min(width, screenWidth);
    height = Math.min(height, screenHeight);

    // 检查位置是否有效
    const isValidPosition = x !== undefined && y !== undefined &&
        x >= 0 && x <= screenWidth &&
        y >= 0 && y <= screenHeight;

    // 如果位置无效,设置默认位置
    if (!isValidPosition) {
        x = Math.floor((screenWidth - width) / 2);
        y = screenHeight - height;
    }

    lyricsWindow = new BrowserWindow({
        width: width,
        height: height,
        x: x,
        y: y,
        minWidth: 800,
        minHeight: 200,
        alwaysOnTop: true,
        frame: false,
        transparent: true,
        resizable: true,
        skipTaskbar: true,
        hasShadow: false,
        webPreferences: {
            preload: path.join(__dirname, 'preload.cjs'),
            contextIsolation: true,
            nodeIntegration: false,
            sandbox: false,
            webSecurity: false, // 禁用 CORS、同源策略
            allowRunningInsecureContent: true, // 允许混合内容
            backgroundThrottling: false,
            zoomFactor: 1.0
        }
    });

    lyricsWindow.on('resize', () => {
        const [width, height] = lyricsWindow.getSize();
        store.set('lyricsWindowSize', { width, height });
    });
    mainWindow.lyricsWindow = lyricsWindow;
    lyricsWindow.on('closed', () => {
        mainWindow.lyricsWindow = null;
    });
    if (isDev) {
        lyricsWindow.loadURL('http://localhost:8080/#/lyrics');
        lyricsWindow.webContents.openDevTools({ mode: 'detach' });
    } else {
        lyricsWindow.loadFile(path.join(__dirname, '../dist/index.html'), {
            hash: 'lyrics'
        });
    }



    // 设置窗口置顶级别
    lyricsWindow.setAlwaysOnTop(true, 'screen-saver');
    lyricsWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });

    // 允许窗口透明
    lyricsWindow.setBackgroundColor('#00000000');
}

export function createMvWindow() {
    const { screenWidth, screenHeight } = screen.getPrimaryDisplay().workAreaSize;
    return new BrowserWindow({
        width: Math.min(screenWidth * 0.8, 1280),
        height: Math.min(screenHeight * 0.8, 720),
        frame: false,
        transparent: true,
        show: false,
        titleBarStyle: 'hiddenInset',
        autoHideMenuBar: true,
        backgroundColor: '#00000000',
        webPreferences: {
            preload: path.join(__dirname, 'preload.cjs'),
            contextIsolation: true,
            nodeIntegration: false,
            sandbox: false,
            webSecurity: false, // 禁用 CORS、同源策略
            allowRunningInsecureContent: true, // 允许混合内容
            zoomFactor: 1.0,
            devTools: isDev
        },
        icon: getIconPath('icon.ico')
    });
}

const getIconPath = (iconName, subPath = '') => path.join(
    isDev ? __dirname + '/../build/icons' : process.resourcesPath + '/icons',
    subPath,
    iconName
);

export function getTray() {
    return tray;
}

// 创建托盘图标及菜单
export function createTray(mainWindow, title = '') {
    if (tray && title) {
        tray.setToolTip(title);
        return tray;
    }

    let trayIconName
    if (process.platform === 'linux') {
        trayIconName = 'linux-icon.png'
    } else if (process.platform === 'darwin') {
        trayIconName = 'tray-icon.png'
    } else {
        trayIconName = 'tray-icon.ico'
    }

    tray = new Tray(getIconPath(trayIconName));
    tray.setToolTip('MoeKoe Music');

    const contextMenu = Menu.buildFromTemplate([
        {
            label: t('project-home'),
            icon: getIconPath('home.png', 'menu'),
            click: () => {
                shell.openExternal('https://Music.MoeKoe.cn');
            }
        },
        {
            label: t('report-bug'),
            icon: getIconPath('bug.png', 'menu'),
            click: () => {
                shell.openExternal('https://github.com/iAJue/MoeKoeMusic/issues');
            }
        },
        {
            label: t('prev-track'),
            icon: getIconPath('prev.png', 'menu'),
            accelerator: 'Alt+CommandOrControl+Left',
            click: () => {
                mainWindow.webContents.send('play-previous-track');
            }
        },
        {
            label: t('pause'),
            accelerator: 'Alt+CommandOrControl+Space',
            icon: getIconPath('play.png', 'menu'),
            click: () => {
                mainWindow.webContents.send('toggle-play-pause');
            }
        },
        {
            label: t('next-track'),
            accelerator: 'Alt+CommandOrControl+Right',
            icon: getIconPath('next.png', 'menu'),
            click: () => {
                mainWindow.webContents.send('play-next-track');
            }
        },
        {
            label: t('check-updates'),
            icon: getIconPath('update.png', 'menu'),
            click: () => {
                checkForUpdates(false);
            }
        },
        {
            label: t('restart-app'),
            icon: getIconPath('restart.png', 'menu'),
            click: () => {
                app.relaunch();
                app.isQuitting = true;
                app.quit();
            }
        },
        {
            label: t('show-hide'),
            accelerator: 'CmdOrCtrl+Shift+S',
            icon: getIconPath('show.png', 'menu'),
            click: () => {
                if (mainWindow) {
                    if (mainWindow.isVisible()) {
                        mainWindow.hide();
                    } else {
                        mainWindow.show();
                    }
                }
            }
        },
        {
            label: t('quit'),
            accelerator: 'CmdOrCtrl+Q',
            icon: getIconPath('quit.png', 'menu'),
            click: () => {
                app.isQuitting = true;
                app.quit();
            }
        }
    ]);

    switch (process.platform) {
        case 'linux':
            tray.setContextMenu(contextMenu);
            break;
        default:
            tray.on('right-click', () => {
                tray.popUpContextMenu(contextMenu);
            });
    }
    tray.on('click', () => {
        if (!mainWindow.isVisible()) {
            mainWindow.show();
        } else if (!mainWindow.isFocused()) {
            mainWindow.show();
            mainWindow.focus();
        } else {
            mainWindow.hide(); //大概率永远不会执行
        }
    });
    tray.on('double-click', () => {
        mainWindow.show();
    });
    return tray;
}

// 创建 TouchBar
export function createTouchBar(mainWindow) {
    const ICON_SIZE = 16;

    let isPlaying = false;

    const iconPath = (iconName) => {
        const originalIcon = nativeImage.createFromPath(
            getIconPath(`${iconName}.png`)
        );

        // 调整图标大小
        return originalIcon.resize({
            width: ICON_SIZE,
            height: ICON_SIZE,
        });
    };

    const prevButton = new TouchBarButton({
        icon: iconPath("prev"),
        iconPosition: "center",
        click: () => {
            mainWindow.webContents.send("play-previous-track");
        },
    });

    const playPauseButton = new TouchBarButton({
        icon: iconPath(isPlaying ? "pause" : "play"),
        iconPosition: "center",
        click: () => {
            isPlaying = !isPlaying;
            playPauseButton.icon = iconPath(isPlaying ? "pause" : "play");
            mainWindow.webContents.send("toggle-play-pause");
        },
    });

    const nextButton = new TouchBarButton({
        icon: iconPath("next"),
        iconPosition: "center",
        click: () => {
            mainWindow.webContents.send("play-next-track");
        },
    });

    // 歌词
    const lyricsLabel = new TouchBarLabel({
        label: t('no-lyrics'),
        textColor: "#FFFFFF",
    });

    const touchBar = new TouchBar({
        items: [
            prevButton,
            new TouchBarSpacer({ size: "small" }),
            playPauseButton,
            new TouchBarSpacer({ size: "small" }),
            nextButton,
            new TouchBarSpacer({ size: "flexible" }),
            lyricsLabel,
            new TouchBarSpacer({ size: "flexible" }),
        ],
    });

    mainWindow.setTouchBar(touchBar);

    // 监听播放状态变化
    ipcMain.on("play-pause-action", (event, playing) => {
        isPlaying = playing;
        playPauseButton.icon = iconPath(isPlaying ? "pause" : "play");
    });

    // 监听歌词更新
    ipcMain.on("update-current-lyrics", (event, currentLyric) => {
        if (currentLyric) {
            lyricsLabel.label = currentLyric;
        }
    });

    return touchBar;
}

// 启动 API 服务器
export function startApiServer() {
    return new Promise((resolve, reject) => {
        let apiPath = '';
        if (isDev) {
            return resolve();
            // apiPath = path.join(__dirname, '../api/app_api');
        } else {
            switch (process.platform) {
                case 'win32':
                    apiPath = path.join(process.resourcesPath, '../api', 'app_win.exe');
                    break;
                case 'darwin':
                    apiPath = path.join(process.resourcesPath, '../api', 'app_macos');
                    break;
                case 'linux':
                    apiPath = path.join(process.resourcesPath, '../api', 'app_linux');
                    break;
                default:
                    reject(new Error(`Unsupported platform: ${process.platform}`));
                    return;
            }
        }

        log.info(`API路径: ${apiPath}`);

        if (!fs.existsSync(apiPath)) {
            const error = new Error(`API可执行文件未找到:${apiPath}`);
            log.error(error.message);
            reject(error);
            return;
        }

        // 启动 API 服务器进程
        const savedConfig = store.get('settings') || {};
        const proxy = savedConfig?.proxy;
        const proxyUrl = savedConfig?.proxyUrl;
        const dataSource = savedConfig?.dataSource || 'concept';

        const Args = [];
        if (dataSource === 'concept') {
            Args.push('--platform=lite');
            log.info('API data source: concept (lite mode)');
        }
        if (proxy === 'on' && proxyUrl) {
            const proxyAddress = String(proxyUrl).trim();
            if (proxyAddress) {
                Args.push(`--proxy=${proxyAddress}`);
                log.info(`API proxy enabled: ${proxyAddress}`);
            }
        }
        Args.push('--port=6521');
        apiProcess = spawn(apiPath, Args, { windowsHide: true });

        apiProcess.stdout.on('data', (data) => {
            log.info(`API输出: ${data}`);
            if (data.toString().includes('running')) {
                console.log('API服务器已启动');
                resolve();
            }
        });

        apiProcess.stderr.on('data', (data) => {
            log.error(`API 错误: ${data}`);
            reject(data);
        });

        apiProcess.on('close', (code) => {
            log.info(`API 关闭,退出码: ${code}`);
        });

        apiProcess.on('error', (error) => {
            log.error('启动 API 失败:', error);
            reject(error);
        });
    });
}

// 停止 API 服务器
export function stopApiServer() {
    if (apiProcess) {
        process.kill(apiProcess.pid, 'SIGKILL');
        apiProcess = null;
    }
}

// 注册快捷键
export function registerShortcut() {
    try {
        const settings = store.get('settings');
        globalShortcut.unregisterAll();
        let clickFunc = () => { app.isQuitting = true; };
        if (process.platform === 'darwin') {
            app.on('before-quit', clickFunc);
        } else {
            clickFunc = () => {
                app.isQuitting = true;
                app.quit();
            };
            if (settings?.shortcuts?.quitApp) {
                globalShortcut.register(settings?.shortcuts?.quitApp, clickFunc);
            } else if (!settings?.shortcuts) {
                globalShortcut.register('CmdOrCtrl+Q', clickFunc);
            }
        }

        clickFunc = () => {
            if (mainWindow) {
                if (mainWindow.isVisible()) {
                    mainWindow.hide();
                } else {
                    mainWindow.show();
                }
            }
        }
        if (settings?.shortcuts?.mainWindow) {
            globalShortcut.register(settings?.shortcuts?.mainWindow, clickFunc);
        } else if (!settings?.shortcuts) {
            globalShortcut.register('CmdOrCtrl+Shift+S', clickFunc);
        }

        clickFunc = () => mainWindow.webContents.send('play-previous-track');
        if (settings?.shortcuts?.prevTrack) {
            globalShortcut.register(settings?.shortcuts?.prevTrack, clickFunc);
        } else if (!settings?.shortcuts) {
            globalShortcut.register('Alt+CommandOrControl+Left', clickFunc);
        }

        clickFunc = () => mainWindow.webContents.send('play-next-track');
        if (settings?.shortcuts?.nextTrack) {
            globalShortcut.register(settings?.shortcuts?.nextTrack, clickFunc);
        } else if (!settings?.shortcuts) {
            globalShortcut.register('Alt+CommandOrControl+Right', clickFunc);
        }

        clickFunc = () => mainWindow.webContents.send('volume-up');
        if (settings?.shortcuts?.volumeUp) {
            globalShortcut.register(settings?.shortcuts?.volumeUp, clickFunc);
        } else if (!settings?.shortcuts) {
            globalShortcut.register('Alt+CommandOrControl+Up', clickFunc);
        }

        clickFunc = () => mainWindow.webContents.send('volume-down');
        if (settings?.shortcuts?.volumeDown) {
            globalShortcut.register(settings?.shortcuts?.volumeDown, clickFunc);
        } else if (!settings?.shortcuts) {
            globalShortcut.register('Alt+CommandOrControl+Down', clickFunc);
        }

        clickFunc = () => mainWindow.webContents.send('toggle-play-pause');
        if (settings?.shortcuts?.playPause) {
            globalShortcut.register(settings?.shortcuts?.playPause, clickFunc);
        } else if (!settings?.shortcuts) {
            globalShortcut.register('Alt+CommandOrControl+Space', clickFunc);
        }

        clickFunc = () => mainWindow.webContents.send('toggle-mute');
        if (settings?.shortcuts?.mute) {
            globalShortcut.register(settings?.shortcuts?.mute, clickFunc);
        } else if (!settings?.shortcuts) {
            globalShortcut.register('Alt+CommandOrControl+M', clickFunc);
        }

        clickFunc = () => mainWindow.webContents.send('toggle-like');
        if (settings?.shortcuts?.like) {
            globalShortcut.register(settings?.shortcuts?.like, clickFunc);
        } else if (!settings?.shortcuts) {
            globalShortcut.register('Alt+CommandOrControl+L', clickFunc);
        }

        clickFunc = () => mainWindow.webContents.send('toggle-mode');
        if (settings?.shortcuts?.mode) {
            globalShortcut.register(settings?.shortcuts?.mode, clickFunc);
        } else if (!settings?.shortcuts) {
            globalShortcut.register('Alt+CommandOrControl+P', clickFunc);
        }

        clickFunc = () => {
            if (mainWindow.lyricsWindow) {
                mainWindow.lyricsWindow.close();
                mainWindow.lyricsWindow = null;
                new Notification({
                    title: t('desktop-lyrics-closed'),
                    body: t('this-time-only'),
                    icon: getIconPath('logo.png')
                }).show();
            } else {
                createLyricsWindow();
            }
        }
        if (settings?.shortcuts?.toggleDesktopLyrics) {
            globalShortcut.register(settings.shortcuts.toggleDesktopLyrics, clickFunc);
        } else if (!settings?.shortcuts) {
            globalShortcut.register('Alt+Ctrl+D', clickFunc);
        }
    } catch {
        dialog.showMessageBox({
            type: 'error',
            title: t('hint'),
            message: t('shortcut-failed'),
            buttons: [t('ok')]
        });
    }
}

// 播放启动问候语
export function playStartupSound() {
    const savedConfig = store.get('settings');
    if (!savedConfig || (savedConfig['greetings'] !== 'on' && savedConfig['greetings'] !== 'null')) {
        return;
    }
    const audioFiles = [
        '/assets/sound/yise-jp.mp3',
        '/assets/sound/qiqi-jp.mp3',
        '/assets/sound/qiqi-zh.mp3'
    ];
    const randomIndex = Math.floor(Math.random() * audioFiles.length);
    const soundPath = isDev
        ? path.join(__dirname, '..', 'public', audioFiles[randomIndex])
        : path.join(process.resourcesPath, 'public', audioFiles[randomIndex]);
    try {
        switch (process.platform) {
            case 'win32':
                const escapedPath = soundPath.replace(/'/g, "''");
                exec(`powershell -c "Add-Type -AssemblyName PresentationCore; $player = New-Object System.Windows.Media.MediaPlayer; $player.Open('${escapedPath}'); $player.Play(); Start-Sleep -s 3; $player.Stop()"`);
                break;
            case 'darwin':
                exec(`afplay "${soundPath}"`);
                break;
            case 'linux':
                exec(`paplay "${soundPath}"`, (error) => {
                    if (error) {
                        exec(`play "${soundPath}"`);
                    }
                });
                break;
        }
    } catch (error) {
        log.error('播放启动问候语失败:', error);
    }
}

// 设置任务栏缩略图工具栏
export function setThumbarButtons(mainWindow, isPlaying = false) {
    const buttons = [
        {
            tooltip: t('prev-track'),
            icon: getIconPath('prev.png'),
            click: () => {
                mainWindow.webContents.send('play-previous-track');
                setThumbarButtons(mainWindow, true);
            }
        },
        {
            tooltip: t('pause'),
            icon: getIconPath('pause.png'),
            click: () => {
                mainWindow.webContents.send('toggle-play-pause');
                setThumbarButtons(mainWindow, false);
            }
        },
        {
            tooltip: t('next-track'),
            icon: getIconPath('next.png'),
            click: () => {
                mainWindow.webContents.send('play-next-track');
                setThumbarButtons(mainWindow, true);
            }
        }
    ];

    if (!isPlaying) {
        buttons[1] = {
            tooltip: t('play'),
            icon: getIconPath('play.png'),
            click: () => {
                mainWindow.webContents.send('toggle-play-pause');
                setThumbarButtons(mainWindow, true);
            }
        };
    }

    mainWindow.setThumbarButtons(buttons);
}

// 处理自定义协议相关
let hash = "";
let listid = "";
let protocolMainWindow = null;

// 注册自定义协议
export function registerProtocolHandler(mainWindow) {
    const PROTOCOL = "moekoe";

    // 保存mainWindow引用
    if (mainWindow) {
        protocolMainWindow = mainWindow;
    }

    // 注册协议
    app.setAsDefaultProtocolClient(PROTOCOL, process.execPath);

    // 处理启动参数
    handleArgv(process.argv);

    // 处理第二个实例的启动参数
    app.on('second-instance', (event, commandLine) => {
        if (protocolMainWindow) {
            if (protocolMainWindow.isMinimized()) protocolMainWindow.restore();
            protocolMainWindow.show();
            protocolMainWindow.focus();
            handleArgv(commandLine);
        }
    });

    // 在macOS平台特别处理open-url事件
    if (process.platform === 'darwin') {
        app.on('open-url', (event, urlStr) => {
            event.preventDefault();
            handleUrl(urlStr);
        });
    }

    return {
        getHash: () => hash,
        handleProtocolArgv: handleArgv
    };
}

// 处理命令行参数
function handleArgv(argv) {
    const PROTOCOL = "moekoe";
    const prefix = `${PROTOCOL}:`;
    const url = argv.find(arg => arg.startsWith(prefix));
    if (url) handleUrl(url);
}

// 处理URL
function handleUrl(url) {
    const urlObj = new URL(url);

    // 提取所有参数并更新全局变量
    hash = urlObj.searchParams.get("hash") || "";
    listid = urlObj.searchParams.get("listid") || "";

    // 根据路径和参数决定发送什么数据到渲染进程
    if (protocolMainWindow && protocolMainWindow.webContents) {
        // 将所有参数打包发送
        protocolMainWindow.webContents.send('url-params', {
            hash,
            listid,
            urlPath: urlObj.pathname.substring(1) // 去掉前导斜杠
        });
    }
}

// 如果有从URL启动的hash参数,在页面加载完成后发送
export function sendHashAfterLoad(mainWindow) {
    if (mainWindow) {
        protocolMainWindow = mainWindow;
    }

    if ((hash || listid) && protocolMainWindow) {
        protocolMainWindow.webContents.on('did-finish-load', () => {
            setTimeout(() => {
                protocolMainWindow.webContents.send('url-params', {
                    hash,
                    listid,
                    urlPath: 'share'
                });
            }, 1000);
        });
    }
}


================================================
FILE: electron/extensions/extensionIPC.js
================================================
import { ipcMain, shell, BrowserWindow, dialog } from 'electron';
import path from 'path';
import fs from 'fs';
import log from 'electron-log';
import { fileURLToPath } from 'url';
import extensionManager from './extensionManager.js';
import { bindExternalLinkHandler } from '../services/externalLinkHandler.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// 获取插件图标数据
function getExtensionIconData(extension, extensionPath) {
    if (extension.manifest?.icons) {
        const icons = extension.manifest.icons;
        const iconSizes = Object.keys(icons);
        if (iconSizes.length > 0) {
            const iconSize = iconSizes[0];
            const iconPath = icons[iconSize];
            const fullIconPath = path.join(extensionPath, iconPath);
            
            try {
                if (fs.existsSync(fullIconPath)) {
                    const iconData = fs.readFileSync(fullIconPath);
                    const ext = path.extname(iconPath).toLowerCase();
                    let mimeType = 'image/png';
                    if (ext === '.jpg' || ext === '.jpeg') {
                        mimeType = 'image/jpeg';
                    }
                    return `data:${mimeType};base64,${iconData.toString('base64')}`;
                }
            } catch (error) {
                log.error('读取插件图标失败:', error);
            }
        }
    }
    return null;
}

/**
 * 注册插件相关的 IPC 处理程序
 */
export function registerExtensionIPC() {
    // 获取插件列表
    ipcMain.handle('get-extensions', () => {
        try {
            const loadedExtensions = extensionManager.getLoadedExtensions();
            const scannedExtensions = extensionManager.scanExtensions();
            
            const extensions = loadedExtensions.map(ext => {
                const scannedExt = scannedExtensions.find(scanned => scanned.name === ext.name);
                let iconData = null;
                const manifestAuthor = ext.manifest?.author ?? scannedExt?.manifest?.author;
                const authorName = typeof manifestAuthor === 'string'
                    ? manifestAuthor
                    : (manifestAuthor?.name || '');
                const authorUrl = typeof manifestAuthor === 'object'
                    ? (manifestAuthor?.url || '')
                    : '';
                
                if (scannedExt?.path) {
                    iconData = getExtensionIconData(ext, scannedExt.path);
                }
                
                return {
                    id: ext.id,
                    pluginId: ext.manifest?.plugin_id || scannedExt?.manifest?.plugin_id || '',
                    name: ext.name,
                    directory: scannedExt?.directory || '',
                    version: ext.version,
                    enabled: true,
                    description: ext.manifest?.description || '',
                    author: authorName,
                    authorUrl: authorUrl,
                    permissions: ext.manifest?.permissions || [],
                    iconData: iconData,
                    moeKoeAdapted: ext.manifest?.moekoe === true || scannedExt?.manifest?.moekoe === true,
                    minversion: ext.manifest?.minversion || scannedExt?.manifest?.minversion || ''
                };
            });
            
            return {
                success: true,
                extensions: extensions
            };
        } catch (error) {
            log.error('获取插件列表失败:', error);
            return {
                success: false,
                error: error.message,
                extensions: []
            };
        }
    });

    // 获取详细插件信息
    ipcMain.handle('get-extensions-detailed', () => {
        try {
            return extensionManager.scanExtensions();
        } catch (error) {
            log.error('获取详细插件信息失败:', error);
            return [];
        }
    });

    // 重新加载插件
    ipcMain.handle('reload-extensions', () => {
        try {
            const result = extensionManager.reloadExtensions();
            return result;
        } catch (error) {
            log.error('重新加载插件失败:', error);
            return { success: false, message: error.message };
        }
    });

    // 打开插件目录
    ipcMain.handle('open-extensions-dir', () => {
        try {
            const extensionsDir = extensionManager.getExtensionsDirectory();
            shell.openPath(extensionsDir);
            return { success: true, path: extensionsDir };
        } catch (error) {
            log.error('打开插件目录失败:', error);
            return { success: false, message: error.message };
        }
    });

    // 打开插件弹窗
    ipcMain.handle('open-extension-popup', (event, extensionId, extensionName) => {
        try {
            
            // 创建新的弹窗窗口
            const popupWindow = new BrowserWindow({
                width: 400,
                height: 600,
                webPreferences: {
                    preload: path.join(__dirname, '../preload.cjs'),
                    nodeIntegration: false,
                    contextIsolation: true,
                    enableRemoteModule: false,
                    sandbox: false,
                    webSecurity: false // 允许加载插件内容
                },
                title: extensionName || '插件弹窗',
                resizable: true,
                minimizable: true,
                maximizable: false,
                alwaysOnTop: false,
                show: false,
                autoHideMenuBar: true, // 隐藏菜单栏
                menuBarVisible: false  // 不显示菜单栏
            });

            // 完全移除菜单栏
            popupWindow.setMenuBarVisibility(false);
            popupWindow.removeMenu();

            bindExternalLinkHandler(
                popupWindow,
                (url) => /^(https?:|mailto:|tel:)/i.test(url)
            );

            // 构建插件弹窗URL
            const popupUrl = `chrome-extension://${extensionId}/popup.html`;
            
            popupWindow.loadURL(popupUrl).then(() => {
                popupWindow.show();
            }).catch((error) => {
                log.error('加载插件弹窗失败:', error);
                popupWindow.close();
            });

            return { success: true, extensionId };
        } catch (error) {
            log.error('打开插件弹窗失败:', error);
            return { success: false, message: error.message };
        }
    });

    // 安装插件
    ipcMain.handle('install-extension', async (event, extensionPath) => {
        try {
            const result = await extensionManager.installExtension(extensionPath);
            return result;
        } catch (error) {
            log.error('手动安装插件失败:', error);
            return { success: false, message: error.message };
        }
    });

    // 卸载插件
    ipcMain.handle('uninstall-extension', (event, extensionId, extensionDir) => {
        try {
            const result = extensionManager.uninstallExtension(extensionId, extensionDir);
            return result;
        } catch (error) {
            log.error('卸载插件失败:', error);
            return { success: false, message: error.message };
        }
    });

    // 验证插件清单
    ipcMain.handle('validate-extension', async (event, extensionPath) => {
        try {
            const manifestPath = path.join(extensionPath, 'manifest.json');
            const validation = extensionManager.validateManifest(manifestPath);
            return validation;
        } catch (error) {
            log.error('验证插件失败:', error);
            return { valid: false, error: error.message };
        }
    });

    // 获取插件目录路径
    ipcMain.handle('get-extensions-directory', () => {
        try {
            return {
                success: true,
                path: extensionManager.getExtensionsDirectory()
            };
        } catch (error) {
            log.error('获取插件目录路径失败:', error);
            return { success: false, message: error.message };
        }
    });

    // 从zip安装插件
    ipcMain.handle('install-plugin-from-zip', async (event, zipPath) => {
        try {
            const result = await extensionManager.installPluginFromZip(zipPath);
            return result;
        } catch (error) {
            log.error('安装插件失败:', error);
            return { success: false, message: error.message };
        }
    });
    
    // 从URL安装插件
    ipcMain.handle('install-plugin-from-url', async (event, payload = {}) => {
        try {
            const result = await extensionManager.installPluginFromUrl(
                payload.downloadUrl,
                payload.extensionId,
                payload.extensionDir
            );
            return result;
        } catch (error) {
            log.error('Failed to install remote plugin:', error);
            return { success: false, message: error.message };
        }
    });
    
    // 显示文件选择对话框
    ipcMain.handle('show-open-dialog', async (event, options) => {
        try {
            const result = await dialog.showOpenDialog({
                ...options,
                properties: [...(options.properties || [])],
                filters: [...(options.filters || [])]
            });

            if (!result.canceled && result.filePaths.length > 0) {
                return { success: true, filePath: result.filePaths[0] };
            }
            return { success: false, message: '未选择文件' };
        } catch (error) {
            log.error('打开文件对话框失败:', error);
            return { success: false, message: error.message };
        }
    });

    // 确保插件目录存在
    ipcMain.handle('ensure-extensions-directory', () => {
        try {
            const path = extensionManager.ensureExtensionsDirectory();
            return { success: true, path };
        } catch (error) {
            log.error('创建插件目录失败:', error);
            return { success: false, message: error.message };
        }
    });

    log.info('插件 IPC 处理程序已注册');
}

/**
 * 注销插件相关的 IPC 处理程序
 */
export function unregisterExtensionIPC() {
    const channels = [
        'get-extensions',
        'get-extensions-detailed',
        'reload-extensions',
        'open-extensions-dir',
        'open-extension-popup',
        'install-extension',
        'uninstall-extension',
        'validate-extension',
        'get-extensions-directory',
        'ensure-extensions-directory',
        'install-plugin-from-zip',
        'install-plugin-from-url',
        'show-open-dialog'
    ];

    channels.forEach(channel => {
        ipcMain.removeHandler(channel);
    });

    log.info('插件 IPC 处理程序已注销');
}

export default {
    registerExtensionIPC,
    unregisterExtensionIPC
};


================================================
FILE: electron/extensions/extensionManager.js
================================================
import { session, app } from 'electron';
import path from 'path';
import fs from 'fs';
import log from 'electron-log';
import { fileURLToPath } from 'url';
import isDev from 'electron-is-dev';
import AdmZip from 'adm-zip';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Chrome 插件管理 - 根据环境选择正确的路径
const EXTENSIONS_DIR = !isDev
    ? path.join(app.getPath('userData'), 'extensions')
    : path.join(__dirname, '../../plugins/extensions');

/**
 * 加载 Chrome 插件
 */
export function loadChromeExtensions() {
    if (!fs.existsSync(EXTENSIONS_DIR)) {
        fs.mkdirSync(EXTENSIONS_DIR, { recursive: true });
        log.info('创建插件目录:', EXTENSIONS_DIR);
    }

    try {
        const extensionDirs = fs.readdirSync(EXTENSIONS_DIR, { withFileTypes: true })
            .filter(dirent => dirent.isDirectory())
            .map(dirent => dirent.name);

        for (const extensionDir of extensionDirs) {
            const extensionPath = path.join(EXTENSIONS_DIR, extensionDir);
            const manifestPath = path.join(extensionPath, 'manifest.json');
            
            if (fs.existsSync(manifestPath)) {
                try {
                    const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
                    
                    // 验证 manifest 格式
                    if (manifest.manifest_version && manifest.name && manifest.version) {
                        session.defaultSession.loadExtension(extensionPath, {
                            allowFileAccess: true
                        }).then((extension) => {
                            log.info(`成功加载插件: ${manifest.name} (${extension.id})`);
                        }).catch((error) => {
                            log.error(`加载插件失败 ${extensionDir}:`, error);
                        });
                    } else {
                        log.warn(`插件 ${extensionDir} 的 manifest.json 格式不正确`);
                    }
                } catch (error) {
                    log.error(`解析插件 ${extensionDir} 的 manifest.json 失败:`, error);
                }
            } else {
                log.warn(`插件目录 ${extensionDir} 缺少 manifest.json 文件`);
            }
        }
    } catch (error) {
        log.error('扫描插件目录失败:', error);
    }
}

/**
 * 卸载所有插件
 */
export function unloadChromeExtensions() {
    try {
        const extensions = session.defaultSession.getAllExtensions();
        extensions.forEach(extension => {
            session.defaultSession.removeExtension(extension.id);
            log.info(`卸载插件: ${extension.name}`);
        });
    } catch (error) {
        log.error('卸载插件失败:', error);
    }
}

/**
 * 获取已加载的插件列表
 */
export function getLoadedExtensions() {
    try {
        return session.defaultSession.getAllExtensions();
    } catch (error) {
        log.error('获取插件列表失败:', error);
        return [];
    }
}

/**
 * 安装单个插件
 * @param {string} extensionPath 插件路径
 */
export async function installExtension(extensionPath) {
    try {
        const extension = await session.defaultSession.loadExtension(extensionPath, {
            allowFileAccess: true
        });
        log.info(`手动安装插件成功: ${extension.name}`);
        return { success: true, extension: { id: extension.id, name: extension.name } };
    } catch (error) {
        log.error('手动安装插件失败:', error);
        return { success: false, message: error.message };
    }
}

/**
 * 卸载单个插件
 * @param {string} extensionId 插件ID
 */
export function uninstallExtension(extensionId, extensionDir = '') {
    try {
        let removedFromSession = false;
        let removedFiles = false;
        let targetDirPath = '';

        targetDirPath = path.join(EXTENSIONS_DIR, path.basename(extensionDir.trim()));
        try {
            session.defaultSession.removeExtension(extensionId);
            removedFromSession = true;
            log.info(`卸载插件会话: ${extensionId}`);
        } catch (error) {
            log.warn(`卸载插件会话失败 ${extensionId}:`, error);
        }

        if (targetDirPath && fs.existsSync(targetDirPath)) {
            fs.rmSync(targetDirPath, { recursive: true, force: true });
            removedFiles = true;
            log.info(`删除插件目录: ${targetDirPath}`);
        }

        if (!removedFromSession && !removedFiles) {
            return { success: false, message: '未找到可卸载的插件会话或目录' };
        }

        return {
            success: true,
            removedFromSession,
            removedFiles,
            path: targetDirPath || ''
        };
    } catch (error) {
        log.error('卸载插件失败:', error);
        return { success: false, message: error.message };
    }
}

/**
 * 重新加载所有插件
 */
export function reloadExtensions() {
    try {
        unloadChromeExtensions();
        loadChromeExtensions();
        return { success: true, message: '插件重新加载成功' };
    } catch (error) {
        log.error('重新加载插件失败:', error);
        return { success: false, message: error.message };
    }
}

/**
 * 获取插件目录路径
 */
export function getExtensionsDirectory() {
    return EXTENSIONS_DIR;
}

/**
 * 检查插件目录是否存在
 */
export function ensureExtensionsDirectory() {
    if (!fs.existsSync(EXTENSIONS_DIR)) {
        fs.mkdirSync(EXTENSIONS_DIR, { recursive: true });
        log.info('创建插件目录:', EXTENSIONS_DIR);
    }
    return EXTENSIONS_DIR;
}

/**
 * 验证插件清单文件
 * @param {string} manifestPath manifest.json 文件路径
 */
export function validateManifest(manifestPath) {
    try {
        if (!fs.existsSync(manifestPath)) {
            return { valid: false, error: 'manifest.json 文件不存在' };
        }

        const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
        
        // 检查必需字段
        const requiredFields = ['manifest_version', 'name', 'version'];
        for (const field of requiredFields) {
            if (!manifest[field]) {
                return { valid: false, error: `缺少必需字段: ${field}` };
            }
        }

        // 检查 manifest 版本
        if (manifest.manifest_version !== 3) {
            return { valid: false, error: '仅支持 Manifest V3 格式' };
        }

        return { valid: true, manifest };
    } catch (error) {
        return { valid: false, error: `解析 manifest.json 失败: ${error.message}` };
    }
}

/**
 * 获取插件详细信息
 * @param {string} extensionDir 插件目录名
 */
export function getExtensionInfo(extensionDir) {
    const extensionPath = path.join(EXTENSIONS_DIR, extensionDir);
    const manifestPath = path.join(extensionPath, 'manifest.json');
    
    const validation = validateManifest(manifestPath);
    if (!validation.valid) {
        return { error: validation.error };
    }

    const manifest = validation.manifest;
    const stats = fs.statSync(extensionPath);
    
    return {
        name: manifest.name,
        version: manifest.version,
        description: manifest.description || '',
        author: manifest.author || '',
        permissions: manifest.permissions || [],
        path: extensionPath,
        size: getDirectorySize(extensionPath),
        lastModified: stats.mtime,
        manifest: manifest
    };
}

/**
 * 获取目录大小
 * @param {string} dirPath 目录路径
 */
function getDirectorySize(dirPath) {
    let totalSize = 0;
    
    try {
        const files = fs.readdirSync(dirPath);
        
        for (const file of files) {
            const filePath = path.join(dirPath, file);
            const stats = fs.statSync(filePath);
            
            if (stats.isDirectory()) {
                totalSize += getDirectorySize(filePath);
            } else {
                totalSize += stats.size;
            }
        }
    } catch (error) {
        log.error('计算目录大小失败:', error);
    }
    
    return totalSize;
}

/**
 * 从zip文件安装插件
 * @param {string} zipPath zip文件路径
 * @returns {Promise<{success: boolean, message: string}>}
 */
export async function installPluginFromZip(zipPath) {
    try {
        const zip = new AdmZip(zipPath);
        const zipEntries = zip.getEntries();

        // 确保插件目录存在
        ensureExtensionsDirectory();

        // 查找包含 manifest.json 的第一级目录
        let pluginEntry = null;
        let manifestEntry = null;
        for (const entry of zipEntries) {
            const parts = entry.entryName.split('/');
            if (parts.length === 2 && parts[1] === 'manifest.json') {
                manifestEntry = entry;
                pluginEntry = parts[0];
                break;
            }
        }

        if (!manifestEntry || !pluginEntry) {
            return { success: false, message: '无效的插件包格式:未找到 manifest.json' };
        }

        // 验证 manifest
        const manifestContent = zip.readAsText(manifestEntry);
        try {
            const manifest = JSON.parse(manifestContent);
            // 直接验证 manifest 对象而不是文件路径
            if (!manifest.manifest_version || !manifest.name || !manifest.version) {
                return { success: false, message: '清单文件缺少必需字段' };
            }
            if (manifest.manifest_version !== 3) {
                return { success: false, message: '仅支持 Manifest V3 格式' };
            }
        } catch (error) {
            return { success: false, message: `manifest.json 解析失败: ${error.message}` };
        }

        // 获取真实的插件目录名(去除可能的 -main 后缀)
        const pluginName = pluginEntry.replace(/-main$/, '');
        const targetDir = path.join(EXTENSIONS_DIR, pluginName);
        
        // 如果目标目录已存在,先删除
        if (fs.existsSync(targetDir)) {
            fs.rmSync(targetDir, { recursive: true, force: true });
        }

        // 创建目标目录
        fs.mkdirSync(targetDir, { recursive: true });

        // 解压所有文件,保持目录结构
        for (const entry of zipEntries) {
            const entryName = entry.entryName;
            // 检查是否属于目标插件目录
            if (entryName.startsWith(pluginEntry + '/')) {
                // 计算相对路径
                const relativePath = entryName.substring(pluginEntry.length + 1);
                if (relativePath) {  // 跳过空路径
                    const targetPath = path.join(targetDir, relativePath);
                    if (entry.isDirectory) {
                        // 创建目录
                        fs.mkdirSync(targetPath, { recursive: true });
                    } else {
                        // 解压文件
                        const targetDirPath = path.dirname(targetPath);
                        fs.mkdirSync(targetDirPath, { recursive: true });
                        fs.writeFileSync(targetPath, entry.getData());
                    }
                }
            }
        }

        // 加载新插件
        const result = await installExtension(targetDir);
        if (!result.success) {
            // 如果加载失败,清理已解压的文件
            if (fs.existsSync(targetDir)) {
                fs.rmSync(targetDir, { recursive: true, force: true });
            }
            return { success: false, message: '插件加载失败:' + result.message };
        }

        return { 
            success: true, 
            message: `插件安装成功`,
            extension: result.extension
        };
    } catch (error) {
        log.error('安装插件失败:', error);
        return { success: false, message: '安装插件失败:' + error.message };
    }
}

/**
 * Download a zip package and install or update a plugin from a remote URL.
 * @param {string} downloadUrl
 * @param {string} extensionId
 * @param {string} extensionDir
 * @returns {Promise<{success: boolean, message: string}>}
 */
export async function installPluginFromUrl(downloadUrl, extensionId = '', extensionDir = '') {
    const tempZipPath = path.join(app.getPath('temp'), `moekoe-plugin-${Date.now()}.zip`);

    try {
        if (!downloadUrl || typeof downloadUrl !== 'string') {
            return { success: false, message: 'Invalid plugin download url' };
        }

        const response = await fetch(downloadUrl);
        if (!response.ok) {
            return {
                success: false,
                message: `Failed to download plugin package: ${response.status} ${response.statusText || ''}`.trim()
            };
        }

        const arrayBuffer = await response.arrayBuffer();
        const zipBuffer = Buffer.from(arrayBuffer);

        if (!isZipBuffer(zipBuffer)) {
            return { success: false, message: '下载内容不是有效的 zip 插件包' };
        }

        fs.writeFileSync(tempZipPath, zipBuffer);

        if (extensionId || extensionDir) {
            const uninstallResult = uninstallExtension(extensionId, extensionDir);
            if (!uninstallResult.success) {
                log.warn('Failed to remove existing plugin before update:', uninstallResult.message);
            }
        }

        return await installPluginFromZip(tempZipPath);
    } catch (error) {
        log.error('Failed to install plugin from url:', error);
        return { success: false, message: error.message };
    } finally {
        if (fs.existsSync(tempZipPath)) {
            fs.rmSync(tempZipPath, { force: true });
        }
    }
}

function isZipBuffer(buffer) {
    return Boolean(buffer) &&
        buffer.length >= 4 &&
        buffer[0] === 0x50 &&
        buffer[1] === 0x4b &&
        (
            (buffer[2] === 0x03 && buffer[3] === 0x04) ||
            (buffer[2] === 0x05 && buffer[3] === 0x06) ||
            (buffer[2] === 0x07 && buffer[3] === 0x08)
        );
}

/**
 * 格式化文件大小
 * @param {number} bytes 字节数
 */
export function formatFileSize(bytes) {
    if (bytes === 0) return '0 B';
    
    const k = 1024;
    const sizes = ['B', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

/**
 * 扫描并获取所有插件信息
 */
export function scanExtensions() {
    ensureExtensionsDirectory();
    
    try {
        const extensionDirs = fs.readdirSync(EXTENSIONS_DIR, { withFileTypes: true })
            .filter(dirent => dirent.isDirectory())
            .map(dirent => dirent.name);

        const extensions = [];
        
        for (const extensionDir of extensionDirs) {
            const info = getExtensionInfo(extensionDir);
            if (!info.error) {
                extensions.push({
                    ...info,
                    directory: extensionDir,
                    installed: isExtensionInstalled(info.name)
                });
            } else {
                log.warn(`插件 ${extensionDir} 信息获取失败:`, info.error);
            }
        }
        
        return extensions;
    } catch (error) {
        log.error('扫描插件失败:', error);
        return [];
    }
}

/**
 * 检查插件是否已安装
 * @param {string} extensionName 插件名称
 */
function isExtensionInstalled(extensionName) {
    try {
        const loadedExtensions = getLoadedExtensions();
        return loadedExtensions.some(ext => ext.name === extensionName);
    } catch (error) {
        return false;
    }
}

// 默认导出所有功能
export default {
    loadChromeExtensions,
    unloadChromeExtensions,
    getLoadedExtensions,
    installExtension,
    uninstallExtension,
    reloadExtensions,
    getExtensionsDirectory,
    ensureExtensionsDirectory,
    validateManifest,
    getExtensionInfo,
    formatFileSize,
    scanExtensions,
    installPluginFromZip,
    installPluginFromUrl
};


================================================
FILE: electron/extensions/extensions.js
================================================
// 插件系统统一入口文件
import extensionManager from './extensionManager.js';
import { registerExtensionIPC, unregisterExtensionIPC } from './extensionIPC.js';
import log from 'electron-log';

/**
 * 初始化插件系统
 */
export function initializeExtensions() {
    try {
        // 确保插件目录存在
        extensionManager.ensureExtensionsDirectory();
        
        // 注册 IPC 处理程序
        registerExtensionIPC();
        
        // 加载插件
        extensionManager.loadChromeExtensions();
        
        return { success: true };
    } catch (error) {
        log.error('插件系统初始化失败:', error);
        return { success: false, error: error.message };
    }
}

/**
 * 清理插件系统
 */
export function cleanupExtensions() {
    try {
        // 卸载所有插件
        extensionManager.unloadChromeExtensions();
        
        // 注销 IPC 处理程序
        unregisterExtensionIPC();
        
        return { success: true };
    } catch (error) {
        log.error('插件系统清理失败:', error);
        return { success: false, error: error.message };
    }
}

/**
 * 重启插件系统
 */
export function restartExtensions() {
    try {
        const cleanupResult = cleanupExtensions();
        if (!cleanupResult.success) {
            return cleanupResult;
        }
        
        const initResult = initializeExtensions();
        return initResult;
    } catch (error) {
        log.error('重启插件系统失败:', error);
        return { success: false, error: error.message };
    }
}

export {
    extensionManager,
    registerExtensionIPC,
    unregisterExtensionIPC
};

export default {
    initializeExtensions,
    cleanupExtensions,
    restartExtensions,
    extensionManager,
    registerExtensionIPC,
    unregisterExtensionIPC
};

================================================
FILE: electron/language/i18n.js
================================================
import Store from 'electron-store';
import { app } from 'electron';

const store = new Store();

const translations = {
  'zh-CN': {
    'project-home': '项目主页',
    'report-bug': '反馈bug',
    'prev-track': '上一首',
    'pause': '暂停',
    'play': '播放',
    'next-track': '下一首',
    'check-updates': '检查更新',
    'restart-app': '重启应用',
    'show-hide': '显示/隐藏',
    'quit': '退出程序',
    'no-lyrics': '暂无歌词',
    'desktop-lyrics-closed': '桌面歌词已关闭',
    'this-time-only': '仅本次生效',
    'now-playing': '正在播放:',
    'error': '错误',
    'init-error': '初始化应用时发生错误。',
    'api-error': 'API 服务启动失败,请检查!',
    'ok': '确定',
    'shortcut-failed': '快捷键注册失败,请重新尝试',
    'hint': '提示',
    'update-timeout': '更新服务器连接超时,请检查网络',
    'update-failed': '更新检查失败,请重试',
    'new-version': '发现新版本',
    'new-version-msg': '发现新版本 {version}\n\n{notes}',
    'no-release-notes': '暂无更新说明',
    'update-now': '立即更新',
    'later': '稍后提醒',
    'update-hint': '更新提示',
    'already-latest': '当前已是最新版本',
    'update-ready': '更新就绪',
    'update-ready-msg': '新版本已下载完成,立即安装?',
    'install-now': '现在安装',
    'install-later': '稍后安装',
    'non-windows-update': '非 Windows 平台暂不支持在线更新,请前往官网或应用市场下载最新版本。'
  },
  'zh-TW': {
    'project-home': '專案首頁',
    'report-bug': '回報問題',
    'prev-track': '上一首',
    'pause': '暫停',
    'play': '播放',
    'next-track': '下一首',
    'check-updates': '檢查更新',
    'restart-app': '重新啟動',
    'show-hide': '顯示/隱藏',
    'quit': '結束程式',
    'no-lyrics': '暫無歌詞',
    'desktop-lyrics-closed': '桌面歌詞已關閉',
    'this-time-only': '僅本次生效',
    'now-playing': '正在播放:',
    'error': '錯誤',
    'init-error': '初始化應用時發生錯誤。',
    'api-error': 'API 服務啟動失敗,請檢查!',
    'ok': '確定',
    'shortcut-failed': '快捷鍵註冊失敗,請重新嘗試',
    'hint': '提示',
    'update-timeout': '更新伺服器連接逾時,請檢查網絡',
    'update-failed': '更新檢查失敗,請重試',
    'new-version': '發現新版本',
    'new-version-msg': '發現新版本 {version}\n\n{notes}',
    'no-release-notes': '暫無更新說明',
    'update-now': '立即更新',
    'later': '稍後提醒',
    'update-hint': '更新提示',
    'already-latest': '當前已是最新版本',
    'update-ready': '更新就緒',
    'update-ready-msg': '新版本已下載完成,立即安裝?',
    'install-now': '現在安裝',
    'install-later': '稍後安裝',
    'non-windows-update': '非 Windows 平台暫不支持線上更新,請前往官網或應用市場下載最新版本。'
  },
  'en': {
    'project-home': 'Project Homepage',
    'report-bug': 'Report Bug',
    'prev-track': 'Previous',
    'pause': 'Pause',
    'play': 'Play',
    'next-track': 'Next',
    'check-updates': 'Check Updates',
    'restart-app': 'Restart App',
    'show-hide': 'Show/Hide',
    'quit': 'Quit',
    'no-lyrics': 'No Lyrics',
    'desktop-lyrics-closed': 'Desktop Lyrics Closed',
    'this-time-only': 'This session only',
    'now-playing': 'Now Playing: ',
    'error': 'Error',
    'init-error': 'Error initializing app.',
    'api-error': 'API service failed to start!',
    'ok': 'OK',
    'shortcut-failed': 'Shortcut registration failed',
    'hint': 'Notice',
    'update-timeout': 'Update server connection timeout',
    'update-failed': 'Update check failed',
    'new-version': 'New Version Available',
    'new-version-msg': 'New version {version} available\n\n{notes}',
    'no-release-notes': 'No release notes',
    'update-now': 'Update Now',
    'later': 'Later',
    'update-hint': 'Update',
    'already-latest': 'Already up to date',
    'update-ready': 'Update Ready',
    'update-ready-msg': 'Update downloaded. Install now?',
    'install-now': 'Install Now',
    'install-later': 'Later',
    'non-windows-update': 'Auto-update is only available on Windows. Please download from the official website.'
  },
  'ru': {
    'project-home': 'Страница проекта',
    'report-bug': 'Сообщить об ошибке',
    'prev-track': 'Предыдущий',
    'pause': 'Пауза',
    'play': 'Воспроизвести',
    'next-track': 'Следующий',
    'check-updates': 'Проверить обновления',
    'restart-app': 'Перезапустить',
    'show-hide': 'Показать/Скрыть',
    'quit': 'Выход',
    'no-lyrics': 'Нет текста',
    'desktop-lyrics-closed': 'Текст на рабочем столе скрыт',
    'this-time-only': 'Только для этого сеанса',
    'now-playing': 'Сейчас играет: ',
    'error': 'Ошибка',
    'init-error': 'Ошибка инициализации приложения.',
    'api-error': 'Не удалось запустить API!',
    'ok': 'ОК',
    'shortcut-failed': 'Не удалось зарегистрировать горячие клавиши',
    'hint': 'Уведомление',
    'update-timeout': 'Таймаут подключения к серверу обновлений',
    'update-failed': 'Ошибка проверки обновлений',
    'new-version': 'Доступна новая версия',
    'new-version-msg': 'Доступна версия {version}\n\n{notes}',
    'no-release-notes': 'Нет описания изменений',
    'update-now': 'Обновить',
    'later': 'Позже',
    'update-hint': 'Обновление',
    'already-latest': 'Установлена последняя версия',
    'update-ready': 'Обновление готово',
    'update-ready-msg': 'Обновление загружено. Установить сейчас?',
    'install-now': 'Установить',
    'install-later': 'Позже',
    'non-windows-update': 'Автообновление доступно только на Windows. Скачайте с официального сайта.'
  },
  'ja': {
    'project-home': 'プロジェクトホーム',
    'report-bug': 'バグ報告',
    'prev-track': '前の曲',
    'pause': '一時停止',
    'play': '再生',
    'next-track': '次の曲',
    'check-updates': '更新を確認',
    'restart-app': '再起動',
    'show-hide': '表示/非表示',
    'quit': '終了',
    'no-lyrics': '歌詞なし',
    'desktop-lyrics-closed': 'デスクトップ歌詞を閉じました',
    'this-time-only': '今回のみ有効',
    'now-playing': '再生中:',
    'error': 'エラー',
    'init-error': 'アプリの初期化エラー。',
    'api-error': 'APIサービスの起動に失敗しました!',
    'ok': 'OK',
    'shortcut-failed': 'ショートカット登録に失敗しました',
    'hint': '通知',
    'update-timeout': '更新サーバー接続タイムアウト',
    'update-failed': '更新確認に失敗しました',
    'new-version': '新しいバージョンが利用可能',
    'new-version-msg': 'バージョン {version} が利用可能です\n\n{notes}',
    'no-release-notes': 'リリースノートなし',
    'update-now': '今すぐ更新',
    'later': '後で',
    'update-hint': '更新',
    'already-latest': '最新バージョンです',
    'update-ready': '更新準備完了',
    'update-ready-msg': '更新がダウンロードされました。今すぐインストールしますか?',
    'install-now': '今すぐインストール',
    'install-later': '後で',
    'non-windows-update': '自動更新はWindowsでのみ利用可能です。公式サイトからダウンロードしてください。'
  },
  'ko': {
    'project-home': '프로젝트 홈',
    'report-bug': '버그 신고',
    'prev-track': '이전',
    'pause': '일시정지',
    'play': '재생',
    'next-track': '다음',
    'check-updates': '업데이트 확인',
    'restart-app': '재시작',
    'show-hide': '표시/숨기기',
    'quit': '종료',
    'no-lyrics': '가사 없음',
    'desktop-lyrics-closed': '바탕화면 가사 닫힘',
    'this-time-only': '이번 세션만',
    'now-playing': '재생 중: ',
    'error': '오류',
    'init-error': '앱 초기화 오류.',
    'api-error': 'API 서비스 시작 실패!',
    'ok': '확인',
    'shortcut-failed': '단축키 등록 실패',
    'hint': '알림',
    'update-timeout': '업데이트 서버 연결 시간 초과',
    'update-failed': '업데이트 확인 실패',
    'new-version': '새 버전 사용 가능',
    'new-version-msg': '버전 {version} 사용 가능\n\n{notes}',
    'no-release-notes': '릴리스 노트 없음',
    'update-now': '지금 업데이트',
    'later': '나중에',
    'update-hint': '업데이트',
    'already-latest': '최신 버전입니다',
    'update-ready': '업데이트 준비 완료',
    'update-ready-msg': '업데이트가 다운로드되었습니다. 지금 설치하시겠습니까?',
    'install-now': '지금 설치',
    'install-later': '나중에',
    'non-windows-update': '자동 업데이트는 Windows에서만 사용 가능합니다. 공식 웹사이트에서 다운로드하세요.'
  }
};

function getLocale() {
  // First check user settings
  const settings = store.get('settings');
  if (settings?.language) {
    return settings.language;
  }
  // Otherwise use system language
  const systemLang = app.getLocale();
  if (systemLang.startsWith('zh')) {
    return systemLang === 'zh-TW' || systemLang === 'zh-HK' ? 'zh-TW' : 'zh-CN';
  }
  if (systemLang.startsWith('ru')) return 'ru';
  if (systemLang.startsWith('ja')) return 'ja';
  if (systemLang.startsWith('ko')) return 'ko';
  if (systemLang.startsWith('en')) return 'en';
  return 'zh-CN'; // fallback
}

export function t(key) {
  const locale = getLocale();
  return translations[locale]?.[key] || translations['zh-CN']?.[key] || key;
}

export default { t };


================================================
FILE: electron/main.js
================================================
import { app, ipcMain, globalShortcut, dialog, Notification, shell, session, powerSaveBlocker, nativeImage } from 'electron';
import {
    createWindow, createTray, createTouchBar, startApiServer,
    stopApiServer, registerShortcut,
    playStartupSound, createLyricsWindow, setThumbarButtons,
    registerProtocolHandler, sendHashAfterLoad, getTray, createMvWindow
} from './appServices.js';
import { initializeExtensions, cleanupExtensions } from './extensions/extensions.js';
import { setupAutoUpdater } from './services/updater.js';
import apiService from './services/apiService.js';
import statusBarLyricsService from './services/statusBarLyricsService.js';
import Store from 'electron-store';
import path from 'path';
import { fileURLToPath } from 'url';
import { t } from './language/i18n.js';

let mainWindow = null;
let blockerId = null;
const store = new Store();
const __dirname = path.dirname(fileURLToPath(import.meta.url));

const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
    app.quit();
    process.exit(0);
} else {
    let protocolHandler;
    app.on('second-instance', (event, commandLine) => {
        if (!protocolHandler) {
            protocolHandler = registerProtocolHandler(null);
        }
        if (mainWindow) {
            if (mainWindow.isMinimized()) mainWindow.restore();
            mainWindow.show();
            mainWindow.focus();
        }
        protocolHandler.handleProtocolArgv(commandLine);
    });
}

app.on('ready', () => {
    startApiServer().then(() => {
        try {
            mainWindow = createWindow();
            createTray(mainWindow);

            // 初始化状态栏歌词服务
            statusBarLyricsService.init(mainWindow, store, getTray, createTray);

            if (process.platform === "darwin" && store.get('settings')?.touchBar == 'on') createTouchBar(mainWindow);
            playStartupSound();
            registerShortcut();
            setupAutoUpdater(mainWindow);
            apiService.init(mainWindow);
            registerProtocolHandler(mainWindow);
            sendHashAfterLoad(mainWindow);
            initializeExtensions();
        } catch (error) {
            console.log('初始化应用时发生错误:', error);
            createTray(null);
            dialog.showMessageBox({
                type: 'error',
                title: t('error'),
                message: t('init-error'),
                buttons: [t('ok')]
            }).then(result => {
                if (result.response === 0) {
                    app.isQuitting = true;
                    app.quit();
                }
            });
        }
    }).catch((error) => {
        console.log('API 服务启动失败:', error);
        createTray(null);
        dialog.showMessageBox({
            type: 'error',
            title: t('error'),
            message: t('api-error'),
            buttons: [t('ok')]
        }).then(result => {
            if (result.response === 0) {
                app.isQuitting = true;
                app.quit();
            }
            return;
        });
    });
});

const settings = store.get('settings');
if (settings?.gpuAcceleration === 'on') {
    app.disableHardwareAcceleration();
    app.commandLine.appendSwitch('enable-transparent-visuals');
    app.commandLine.appendSwitch('disable-gpu-compositing');
}

if (settings?.preventAppSuspension === 'on') {
    blockerId = powerSaveBlocker.start('prevent-display-sleep');
}

if (settings?.highDpi === 'on') {
    app.commandLine.appendSwitch('high-dpi-support', '1');
    app.commandLine.appendSwitch('force-device-scale-factor', settings?.dpiScale || '1');
}

if (settings?.apiMode === 'on') {
    apiService.start();
}

// 即将退出
app.on('before-quit', () => {
    if (mainWindow && !mainWindow.isMaximized()) {
        const windowBounds = mainWindow.getBounds();
        store.set('windowState', windowBounds);
    }
    if (blockerId !== null) {
        powerSaveBlocker.stop(blockerId);
    }

    // 清理状态栏歌词服务
    statusBarLyricsService.cleanup();

    stopApiServer();
    apiService.stop();
    cleanupExtensions();
});
// 关闭所有窗口
app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        app.isQuitting = true;
        app.quit(); // 非 macOS 系统上关闭所有窗口后退出应用
    }
});
// 图标被点击
app.on('activate', () => {
    if (mainWindow && !mainWindow.isVisible()) {
        mainWindow.show();
    } else if (!mainWindow) {
        mainWindow = createWindow();
    }
});

// 处理未捕获的异常
process.on('uncaughtException', (error) => {
    console.error('Unhandled Exception:', error);
});

// 监听渲染进程发送的免责声明结果
ipcMain.on('disclaimer-response', (event, accepted) => {
    if (accepted) {
        store.set('disclaimerAccepted', true);
    } else {
        app.quit();
    }
});

ipcMain.on('window-control', (event, action) => {
    switch (action) {
        case 'close':
            if (store.get('settings')?.minimizeToTray === 'off') {
                app.isQuitting = true;
                app.quit();
            } else {
                mainWindow.close();
            }
            break;
        case 'minimize':
            mainWindow.minimize();
            break;
        case 'maximize':
            if (mainWindow.isMaximized()) {
                mainWindow.unmaximize();
                store.set('maximize', false);
            } else {
                mainWindow.maximize();
                store.set('maximize', true);
            }
            break;
    }
});

app.on('will-quit', () => {
    globalShortcut.unregisterAll();
});
ipcMain.on('save-settings', (event, settings) => {
    store.set('settings', settings);
    if (['on', 'off'].includes(settings?.autoStart)) {
        app.setLoginItemSettings({
            openAtLogin: settings?.autoStart === 'on',
            path: app.getPath('exe'),
        });
    }
});
ipcMain.on('clear-settings', (event) => {
    store.clear();
    session.defaultSession.clearCache();
    session.defaultSession.clearStorageData();
    const userDataPath = app.getPath('userData');
    shell.openPath(userDataPath);
});
ipcMain.on('custom-shortcut', (event) => {
    registerShortcut();
});

ipcMain.on('lyrics-data', (event, lyricsData) => {
    const lyricsWindow = mainWindow?.lyricsWindow;
    if (lyricsWindow) {
        lyricsWindow.webContents.send('lyrics-data', lyricsData);
    }

    // 状态栏歌词功能服务处理(仅支持Mac系统)
    if (process.platform === 'darwin') {
        statusBarLyricsService.handleLyricsData(lyricsData);
    }
});

ipcMain.on('server-lyrics', (event, lyricsData) => {
    apiService.updateLyrics(lyricsData);
});

// 监听桌面歌词操作
ipcMain.on('desktop-lyrics-action', (event, action) => {
    switch (action) {
        case 'previous-song':
            mainWindow.webContents.send('play-previous-track');
            break;
        case 'next-song':
            mainWindow.webContents.send('play-next-track');
            break;
        case 'toggle-play':
            mainWindow.webContents.send('toggle-play-pause');
            break;
        case 'close-lyrics':
            const lyricsWindow = mainWindow.lyricsWindow;
            if (lyricsWindow) {
                lyricsWindow.close();
                new Notification({
                    title: t('desktop-lyrics-closed'),
                    body: t('this-time-only'),
                    icon: path.join(__dirname, '../build/icons/logo.png')
                }).show();
                mainWindow.lyricsWindow = null;
            }
            break;
        case 'display-lyrics':
            if (!mainWindow.lyricsWindow) createLyricsWindow();
            break;
    }
});

ipcMain.on('set-ignore-mouse-events', (event, ignore) => {
    const lyricsWindow = mainWindow.lyricsWindow;
    if (lyricsWindow) {
        lyricsWindow.setIgnoreMouseEvents(ignore, { forward: true });
    }
});

ipcMain.on('window-drag', (event, { mouseX, mouseY }) => {
    const lyricsWindow = mainWindow.lyricsWindow;
    if (!lyricsWindow) return
    lyricsWindow.setPosition(mouseX, mouseY)
    store.set('lyricsWindowPosition', { x: mouseX, y: mouseY });
})

ipcMain.on('play-pause-action', (event, playing, currentTime) => {
    const lyricsWindow = mainWindow.lyricsWindow;
    if (lyricsWindow) {
        lyricsWindow.webContents.send('playing-status', playing);
    }
    apiService.updatePlayerState({ isPlaying: playing, currentTime: currentTime });
    setThumbarButtons(mainWindow, playing);
})

ipcMain.on('open-url', (event, url) => {
    shell.openExternal(url);
})

ipcMain.on('set-tray-title', (event, title) => {
    createTray(mainWindow, t('now-playing') + title);
    mainWindow.setTitle(title);
})


ipcMain.handle('open-mv-window', (e, url) => {
    return (async () => {
        const mvWindow = createMvWindow();
        try {
            await mvWindow.loadURL(url);
            mvWindow.show();
            return true;
        } catch (error) {
            console.error('[open-mv-window] loadURL failed:', url, error);
            try {
                mvWindow.close();
            } catch {}
            throw error;
        }
    })();
});


================================================
FILE: electron/preload.cjs
================================================
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electron', {
    ipcRenderer: {
        send: (channel, ...args) => ipcRenderer.send(channel, ...args),
        on: (channel, listener) => ipcRenderer.on(channel, listener),
        once: (channel, listener) => ipcRenderer.once(channel, listener),
        removeListener: (channel, listener) => ipcRenderer.removeListener(channel, listener),
        removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel)
    },
    platform: process.platform
});

// 添加插件管理 API
contextBridge.exposeInMainWorld('electronAPI', {
    // 插件管理
    getExtensions: () => ipcRenderer.invoke('get-extensions'),
    getExtensionsDetailed: () => ipcRenderer.invoke('get-extensions-detailed'),
    reloadExtensions: () => ipcRenderer.invoke('reload-extensions'),
    openExtensionsDir: () => ipcRenderer.invoke('open-extensions-dir'),
    openExtensionPopup: (extensionId, extensionName) => ipcRenderer.invoke('open-extension-popup', extensionId, extensionName),
    installExtension: (extensionPath) => ipcRenderer.invoke('install-extension', extensionPath),
    uninstallExtension: (extensionId, extensionDir) => ipcRenderer.invoke('uninstall-extension', extensionId, extensionDir),
    validateExtension: (extensionPath) => ipcRenderer.invoke('validate-extension', extensionPath),
    getExtensionsDirectory: () => ipcRenderer.invoke('get-extensions-directory'),
    ensureExtensionsDirectory: () => ipcRenderer.invoke('ensure-extensions-directory'),
    installPluginFromZip: (zipPath) => ipcRenderer.invoke('install-plugin-from-zip', zipPath),
    installPluginFromUrl: (downloadUrl, extensionId = '', extensionDir = '') => ipcRenderer.invoke('install-plugin-from-url', {
        downloadUrl,
        extensionId,
        extensionDir,
    }),
    showOpenDialog: (options) => ipcRenderer.invoke('show-open-dialog', options),
    openMvWindow: (url) => ipcRenderer.invoke('open-mv-window', url),
});


================================================
FILE: electron/services/apiService.js
================================================
import { WebSocketServer } from 'ws';

class ApiService {
    constructor() {
        this.wsServer = null;
        this.clients = new Set();
        this.currentLyrics = null;
        this.isPlaying = false;
        this.currentTime = 0;
        this.mainWindow = null;
    }
    init(mainWindow) {
        this.mainWindow = mainWindow;
    }
    // 启动 WebSocket 服务器
    start() {
        if (this.wsServer) return; 
        this.wsServer = new WebSocketServer({ port: 6520 });
        this.wsServer.on('connection', (ws) => {
            this.clients.add(ws);
            //发送欢迎信息
            ws.send(JSON.stringify({
                type: 'welcome',
                data: '感谢接入MoeKoe Music,文档地址:https://music.moekoe.cn/'
            }));

            // 发送当前歌词
            if (this.currentLyrics) {
                ws.send(JSON.stringify({
                    type: 'lyrics',
                    data: this.currentLyrics
                }));
            }

            // 发送当前播放状态
            ws.send(JSON.stringify({
                type: 'playerState',
                data: {
                    isPlaying: this.isPlaying,
                    currentTime: this.currentTime
                }
            }));

            // 处理来自客户端的消息
            ws.on('message', (message) => {
                try {
                    const data = JSON.parse(message);
                    console.log(data);
                    if (data.type === 'control') {
                        this.handleControlCommand(data.data);
                    }
                } catch (e) {
                    console.error('无效的 WebSocket 消息', e);
                }
            });

            ws.on('close', () => {
                this.clients.delete(ws);
                console.log('WebSocket 客户端已断开连接');
            });
        });

        console.log('WebSocket server running at ws://127.0.0.1:6520');
    }

    stop() {
        if (this.wsServer) {
            for (const client of this.clients) {
                client.close();
            }
            this.clients.clear();
            this.wsServer.close();
            this.wsServer = null;
            console.log('WebSocket 服务器已停止');
        }
    }
    
    // 广播到所有客户端
    broadcastToClients(data) {
        if(!this.wsServer) return;
        const message = JSON.stringify(data);
        for (const client of this.clients) {
            if (client.readyState === 1) {
                client.send(message);
            }
        }
    }

    handleControlCommand(data) {
        if (!this.mainWindow) return;
        switch (data.command) {
            case 'toggle': // 切换播放状态
                this.mainWindow.webContents.send('toggle-play-pause');
                break;
            case 'next': // 下一首
                this.mainWindow.webContents.send('play-next-track');
                break;
            case 'prev': // 上一首
                this.mainWindow.webContents.send('play-previous-track');
                break;
        }
    }
    
    // 更新歌词数据
    updateLyrics(lyricsData) {
        this.currentLyrics = lyricsData;
        this.broadcastToClients({
            type: 'lyrics',
            data: lyricsData
        });
    }
    
    // 更新播放状态
    updatePlayerState(state) {
        this.isPlaying = state.isPlaying;
        this.currentTime = state.currentTime;
        this.broadcastToClients({
            type: 'playerState',
            data: state
        });
    }
}

const apiService = new ApiService();
export default apiService; 

================================================
FILE: electron/services/externalLinkHandler.js
================================================
import { shell } from 'electron';

export function shouldOpenExternally(targetUrl, currentUrl = '') {
    try {
        const target = new URL(targetUrl);
        if (target.protocol === 'mailto:' || target.protocol === 'tel:') {
            return true;
        }
        if (target.protocol !== 'http:' && target.protocol !== 'https:') {
            return false;
        }

        if (!currentUrl) {
            return true;
        }

        const current = new URL(currentUrl);
        return target.origin !== current.origin;
    } catch {
        return false;
    }
}

export function bindExternalLinkHandler(win, openExternalPredicate = shouldOpenExternally) {
    const { webContents } = win;

    webContents.setWindowOpenHandler(({ url }) => {
        if (openExternalPredicate(url, webContents.getURL())) {
            shell.openExternal(url);
        }
        return { action: 'deny' };
    });

    webContents.on('will-navigate', (event, url) => {
        if (openExternalPredicate(url, webContents.getURL())) {
            event.preventDefault();
            shell.openExternal(url);
        }
    });
}



================================================
FILE: electron/services/statusBarLyricsService.js
================================================
import { ipcMain, nativeImage } from 'electron';

class StatusBarLyricsService {
    constructor() {
        this.mainWindow = null;
        this.store = null;
        this.tray = null;
        this.clearLyricsTimeout = null;
        this.lastStatusBarLyric = '';
        this.lastTrayUpdateTime = 0;
        this.lastTrayImageHash = '';
        this.TRAY_UPDATE_THROTTLE = 30; // 30ms 节流
        this.getTrayCallback = null;
        this.createTrayCallback = null;
    }

    init(mainWindow, store, getTrayCallback, createTrayCallback) {
        this.mainWindow = mainWindow;
        this.store = store;
        this.getTrayCallback = getTrayCallback;
        this.createTrayCallback = createTrayCallback;

        if (process.platform === 'darwin') {
            this.registerListeners();
            this.initializeOnStartup();  // 自动初始化
        }
    }

    // 判断状态栏歌词是否开启
    isStatusBarLyricsEnabled() {
        if (process.platform !== 'darwin') return false;

        const settings = this.store.get('settings') || {};
        return settings.statusBarLyrics === 'on';
    }

    registerListeners() {
        // 监听渲染进程生成的图片并更新 Tray
        ipcMain.on('update-statusbar-image', (event, dataUrl) => {
            this.handleUpdateImage(dataUrl);
        });
    }

    // 处理歌词数据 (供 main.js 调用)
    handleLyricsData(lyricsData) {
        if (!this.isStatusBarLyricsEnabled()) {
            this.handleDisabledState();
            return;
        }

        const currentLyric = lyricsData?.currentLyric || '';

        if (currentLyric) {
            // 有歌词:清除防抖定时器,立即更新
            if (this.clearLyricsTimeout) {
                clearTimeout(this.clearLyricsTimeout);
                this.clearLyricsTimeout = null;
            }

            if (currentLyric !== this.lastStatusBarLyric) {
                if (this.mainWindow?.webContents) {
                    this.mainWindow.webContents.send('generate-statusbar-image', currentLyric);
                }
                this.lastStatusBarLyric = currentLyric;
            }
        } else {
            // 无歌词 (间奏):启动 5秒 防抖
            if (!this.clearLyricsTimeout && this.lastStatusBarLyric !== '') {
                this.clearLyricsTimeout = setTimeout(() => {
                    // 再次检查设置,确保这段时间没被关闭
                    if (this.isStatusBarLyricsEnabled()) {
                        if (this.mainWindow?.webContents) {
                            this.mainWindow.webContents.send('generate-statusbar-image', ''); // 发送空字符触发占位符
                        }
                        this.lastStatusBarLyric = '';
                    }
                    this.clearLyricsTimeout = null;
                }, 5000);
            }
        }
    }

    // 处理功能关闭时的状态清理
    handleDisabledState() {
        if (this.lastStatusBarLyric !== '') {
            if (this.clearLyricsTimeout) {
                clearTimeout(this.clearLyricsTimeout);
                this.clearLyricsTimeout = null;
            }

            const tray = this.getTrayCallback ? this.getTrayCallback() : null;
            if (tray && !tray.isDestroyed()) {
                tray.setTitle(''); // 清除文字
                tray.setImage(nativeImage.createEmpty()); // 清除图片

                // 如果需要恢复原始 Tray 图标,这里可能需要重新调用 createTray
                if (this.createTrayCallback) {
                    this.createTrayCallback(this.mainWindow);
                }

                this.lastStatusBarLyric = '';
            }
        }
    }

    // 处理图片更新 (更新 Tray)
    handleUpdateImage(dataUrl) {
        // 节流
        const now = Date.now();
        if (now - this.lastTrayUpdateTime < this.TRAY_UPDATE_THROTTLE) return;

        // Tray 检查
        const tray = this.getTrayCallback ? this.getTrayCallback() : null;
        if (!tray || tray.isDestroyed()) return;

        if (!dataUrl) return;

        // 哈希去重
        const imageHash = dataUrl.slice(-100);
        if (imageHash === this.lastTrayImageHash) return;

        this.lastTrayUpdateTime = now;
        this.lastTrayImageHash = imageHash;

        try {
            const base64Data = dataUrl.replace(/^data:image\/png;base64,/, "");
            const buffer = Buffer.from(base64Data, 'base64');
            const image = nativeImage.createEmpty();

            // 逻辑尺寸 200x22, 实际 Buffer 400x44 (@2x)
            image.addRepresentation({
                scaleFactor: 2.0,
                width: 200,
                height: 22,
                buffer: buffer
            });

            image.setTemplateImage(true);

            if (tray && !tray.isDestroyed()) {
                tray.setImage(image);
                tray.setTitle(''); // 确保不显示文字
            }
        } catch (e) {
            console.error('[StatusBarService] Failed to set tray image:', e);
        }
    }

    // 应用启动时初始化状态栏歌词(私有方法)
    initializeOnStartup() {
        if (!this.isStatusBarLyricsEnabled()) {
            return;
        }

        // 等待窗口准备好后触发渲染
        this.mainWindow.webContents.once('did-finish-load', () => {
            setTimeout(() => {
                if (this.mainWindow?.webContents) {
                    console.log('[StatusBarLyricsService] 启动时主动触发状态栏歌词渲染');
                    this.mainWindow.webContents.send('generate-statusbar-image', '');
                }
            }, 1000);
        });
    }

    // 清理资源(应用退出时调用)
    cleanup() {
        // 清理定时器
        if (this.clearLyricsTimeout) {
            clearTimeout(this.clearLyricsTimeout);
            this.clearLyricsTimeout = null;
        }

        // 清理 Tray(防止退出后闪烁)
        const tray = this.getTrayCallback ? this.getTrayCallback() : null;
        if (tray && !tray.isDestroyed()) {
            try {
                tray.setImage(nativeImage.createEmpty());
                tray.setTitle('');
                tray.destroy();
            } catch (e) {
                console.error('[StatusBarLyricsService] Error cleaning up tray:', e);
            }
        }
    }
}

export default new StatusBarLyricsService();


============
Download .txt
gitextract_l9rjb4a1/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   ├── discussion.yml
│   │   └── feature_request.yml
│   ├── PULL_REQUEST_TEMPLATE/
│   │   ├── bug_fix.md
│   │   ├── docs_update.md
│   │   └── feature_request.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── SECURITY.md
│   └── workflows/
│       ├── AiIssueCheck.yml
│       ├── AutoCloseIssues.yml
│       ├── release.yml
│       └── version.yml
├── .gitignore
├── .gitmodules
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── build/
│   ├── icons/
│   │   └── icon.icns
│   ├── installer.nsh
│   └── license.txt
├── docker-compose.yml
├── docs/
│   ├── README_en.md
│   ├── README_ja.md
│   ├── README_ko.md
│   ├── README_ru.md
│   └── README_tw.md
├── electron/
│   ├── appServices.js
│   ├── extensions/
│   │   ├── extensionIPC.js
│   │   ├── extensionManager.js
│   │   └── extensions.js
│   ├── language/
│   │   └── i18n.js
│   ├── main.js
│   ├── preload.cjs
│   └── services/
│       ├── apiService.js
│       ├── externalLinkHandler.js
│       ├── statusBarLyricsService.js
│       └── updater.js
├── index.html
├── nginx.conf
├── package.json
└── src/
    ├── App.vue
    ├── assets/
    │   ├── style/
    │   │   └── PlayerControl.css
    │   └── themes/
    │       └── dark.css
    ├── components/
    │   ├── AlbumGrid.vue
    │   ├── ArtistGrid.vue
    │   ├── BirthdayEasterEgg.vue
    │   ├── ContextMenu.vue
    │   ├── CustomModal.vue
    │   ├── Disclaimer.vue
    │   ├── ExtensionManager.vue
    │   ├── Header.vue
    │   ├── MessageNotification.vue
    │   ├── PlayerControl.vue
    │   ├── PlaylistGrid.vue
    │   ├── PlaylistSelectModal.vue
    │   ├── QueueList.vue
    │   ├── StatusBarLyrics.vue
    │   ├── TitleBar.vue
    │   └── player/
    │       ├── AudioController.js
    │       ├── Helpers.js
    │       ├── LyricsHandler.js
    │       ├── MediaSession.js
    │       ├── PlaybackMode.js
    │       ├── ProgressBar.js
    │       ├── SongQueue.js
    │       └── index.js
    ├── language/
    │   ├── en.json
    │   ├── ja.json
    │   ├── ko.json
    │   ├── ru.json
    │   ├── zh-CN.json
    │   └── zh-TW.json
    ├── layouts/
    │   └── HomeLayout.vue
    ├── main.js
    ├── plugins/
    │   ├── MessagePlugin.js
    │   └── ModalPlugin.js
    ├── router/
    │   └── router.js
    ├── stores/
    │   ├── musicQueue.js
    │   └── store.js
    ├── utils/
    │   ├── apiBaseUrl.js
    │   ├── i18n.js
    │   ├── request.js
    │   └── utils.js
    └── views/
        ├── CloudDrive.vue
        ├── Discover.vue
        ├── Home.vue
        ├── Library.vue
        ├── LocalMusic.vue
        ├── Login.vue
        ├── Lyrics.vue
        ├── PlaylistDetail.vue
        ├── Ranking.vue
        ├── Search.vue
        ├── Settings.vue
        └── VideoPlayer.vue
Download .txt
SYMBOL INDEX (94 symbols across 23 files)

FILE: electron/appServices.js
  function createWindow (line 23) | function createWindow() {
  function createLyricsWindow (line 141) | function createLyricsWindow() {
  function createMvWindow (line 224) | function createMvWindow() {
  function getTray (line 255) | function getTray() {
  function createTray (line 260) | function createTray(mainWindow, title = '') {
  function createTouchBar (line 384) | function createTouchBar(mainWindow) {
  function startApiServer (line 465) | function startApiServer() {
  function stopApiServer (line 543) | function stopApiServer() {
  function registerShortcut (line 551) | function registerShortcut() {
  function playStartupSound (line 670) | function playStartupSound() {
  function setThumbarButtons (line 707) | function setThumbarButtons(mainWindow, isPlaying = false) {
  function registerProtocolHandler (line 755) | function registerProtocolHandler(mainWindow) {
  function handleArgv (line 794) | function handleArgv(argv) {
  function handleUrl (line 802) | function handleUrl(url) {
  function sendHashAfterLoad (line 821) | function sendHashAfterLoad(mainWindow) {

FILE: electron/extensions/extensionIPC.js
  function getExtensionIconData (line 12) | function getExtensionIconData(extension, extensionPath) {
  function registerExtensionIPC (line 42) | function registerExtensionIPC() {
  function unregisterExtensionIPC (line 289) | function unregisterExtensionIPC() {

FILE: electron/extensions/extensionManager.js
  constant EXTENSIONS_DIR (line 12) | const EXTENSIONS_DIR = !isDev
  function loadChromeExtensions (line 19) | function loadChromeExtensions() {
  function unloadChromeExtensions (line 65) | function unloadChromeExtensions() {
  function getLoadedExtensions (line 80) | function getLoadedExtensions() {
  function installExtension (line 93) | async function installExtension(extensionPath) {
  function uninstallExtension (line 110) | function uninstallExtension(extensionId, extensionDir = '') {
  function reloadExtensions (line 150) | function reloadExtensions() {
  function getExtensionsDirectory (line 164) | function getExtensionsDirectory() {
  function ensureExtensionsDirectory (line 171) | function ensureExtensionsDirectory() {
  function validateManifest (line 183) | function validateManifest(manifestPath) {
  function getExtensionInfo (line 214) | function getExtensionInfo(extensionDir) {
  function getDirectorySize (line 243) | function getDirectorySize(dirPath) {
  function installPluginFromZip (line 271) | async function installPluginFromZip(zipPath) {
  function installPluginFromUrl (line 372) | async function installPluginFromUrl(downloadUrl, extensionId = '', exten...
  function isZipBuffer (line 415) | function isZipBuffer(buffer) {
  function formatFileSize (line 431) | function formatFileSize(bytes) {
  function scanExtensions (line 444) | function scanExtensions() {
  function isExtensionInstalled (line 478) | function isExtensionInstalled(extensionName) {

FILE: electron/extensions/extensions.js
  function initializeExtensions (line 9) | function initializeExtensions() {
  function cleanupExtensions (line 30) | function cleanupExtensions() {
  function restartExtensions (line 48) | function restartExtensions() {

FILE: electron/language/i18n.js
  function getLocale (line 225) | function getLocale() {
  function t (line 243) | function t(key) {

FILE: electron/services/apiService.js
  class ApiService (line 3) | class ApiService {
    method constructor (line 4) | constructor() {
    method init (line 12) | init(mainWindow) {
    method start (line 16) | start() {
    method stop (line 66) | stop() {
    method broadcastToClients (line 79) | broadcastToClients(data) {
    method handleControlCommand (line 89) | handleControlCommand(data) {
    method updateLyrics (line 105) | updateLyrics(lyricsData) {
    method updatePlayerState (line 114) | updatePlayerState(state) {

FILE: electron/services/externalLinkHandler.js
  function shouldOpenExternally (line 3) | function shouldOpenExternally(targetUrl, currentUrl = '') {
  function bindExternalLinkHandler (line 24) | function bindExternalLinkHandler(win, openExternalPredicate = shouldOpen...

FILE: electron/services/statusBarLyricsService.js
  class StatusBarLyricsService (line 3) | class StatusBarLyricsService {
    method constructor (line 4) | constructor() {
    method init (line 17) | init(mainWindow, store, getTrayCallback, createTrayCallback) {
    method isStatusBarLyricsEnabled (line 30) | isStatusBarLyricsEnabled() {
    method registerListeners (line 37) | registerListeners() {
    method handleLyricsData (line 45) | handleLyricsData(lyricsData) {
    method handleDisabledState (line 84) | handleDisabledState() {
    method handleUpdateImage (line 107) | handleUpdateImage(dataUrl) {
    method initializeOnStartup (line 150) | initializeOnStartup() {
    method cleanup (line 167) | cleanup() {

FILE: electron/services/updater.js
  method get (line 12) | get() {
  function setupAutoUpdater (line 17) | function setupAutoUpdater(mainWindow) {
  function checkForUpdates (line 86) | function checkForUpdates(silent = false) {

FILE: src/components/player/AudioController.js
  function useAudioController (line 3) | function useAudioController({ onSongEnd, updateCurrentTime }) {

FILE: src/components/player/Helpers.js
  function useHelpers (line 6) | function useHelpers(t) {

FILE: src/components/player/LyricsHandler.js
  function useLyricsHandler (line 4) | function useLyricsHandler(t) {

FILE: src/components/player/MediaSession.js
  function useMediaSession (line 1) | function useMediaSession() {

FILE: src/components/player/PlaybackMode.js
  function usePlaybackMode (line 3) | function usePlaybackMode(t, audio) {

FILE: src/components/player/ProgressBar.js
  function useProgressBar (line 4) | function useProgressBar(audio, resetLyricsHighlight) {

FILE: src/components/player/SongQueue.js
  constant QUALITY_LEVELS (line 5) | const QUALITY_LEVELS = ['128', '320', 'flac', 'high', 'viper_atmos', 'vi...
  constant QUALITY_LABELS (line 6) | const QUALITY_LABELS = {
  function useSongQueue (line 73) | function useSongQueue(t, musicQueueStore, queueList = null) {

FILE: src/main.js
  method onNeedRefresh (line 33) | onNeedRefresh() {
  method onOfflineReady (line 36) | onOfflineReady() {

FILE: src/plugins/MessagePlugin.js
  method install (line 5) | install() {

FILE: src/plugins/ModalPlugin.js
  method install (line 5) | install() {

FILE: src/router/router.js
  method scrollBehavior (line 43) | scrollBehavior(to, from, savedPosition) {

FILE: src/stores/musicQueue.js
  method addSong (line 9) | addSong(song) {
  method setQueue (line 13) | setQueue(newQueue) {
  method getQueue (line 17) | getQueue() {
  method removeSong (line 21) | removeSong(index) {
  method clearQueue (line 25) | clearQueue() {

FILE: src/stores/store.js
  method fetchConfig (line 18) | fetchConfig(key) {
  method setData (line 23) | async setData(data) {
  method clearData (line 27) | clearData() {
  method initDevice (line 30) | async initDevice() {

FILE: src/utils/apiBaseUrl.js
  constant DEFAULT_API_BASE_URL (line 1) | const DEFAULT_API_BASE_URL =
  function normalizeApiBaseUrl (line 4) | function normalizeApiBaseUrl(input) {
  function validateApiBaseUrl (line 9) | function validateApiBaseUrl(input) {
  function getApiBaseUrl (line 27) | function getApiBaseUrl() {
  function joinApiUrl (line 38) | function joinApiUrl(baseUrl, path = '/') {
  function testApiBaseUrl (line 44) | async function testApiBaseUrl(baseUrl, options = {}) {
Condensed preview — 98 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,048K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 1502,
    "preview": "name: \"🐞 Bug报告\"\ndescription: 创建一个报告来帮助我们改进产品\ntitle: '[Bug]: '\nlabels: ['bug']\nbody:\n  - type: markdown\n    attributes:\n "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 471,
    "preview": "blank_issues_enabled: false\ncontact_links:\n  - name: 🌈 社区交流\n    url: https://github.com/iAJue/MoeKoeMusic/discussions\n  "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/discussion.yml",
    "chars": 1680,
    "preview": "name: \"💬 讨论问题\"\ndescription: 提出一个需要讨论的话题或问题\ntitle: '[讨论]: '\nlabels: ['question']\nbody:\n  - type: markdown\n    attributes:"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "chars": 1363,
    "preview": "name: \"✨ 新特性请求\"\ndescription: 为项目提出一个新想法或建议\ntitle: '[新需求]: '\nlabels: ['enhancement']\nbody:\n  - type: markdown\n    attribu"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE/bug_fix.md",
    "chars": 516,
    "preview": "---\nname: 🐞 Bug Fix | 修复 Bug\nabout: 修复现有的问题或异常\ntitle: 'fix: 修复 [问题名称]'\nlabels: bug\n---\n\n## 🐛 修复了什么?What is fixed?\n\n请简要说明"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE/docs_update.md",
    "chars": 547,
    "preview": "---\nname: 📝 Docs Update | 文档更新\nabout: 提交文档相关的修改\ntitle: 'docs: 更新 [文档主题]'\nlabels: documentation\n---\n\n## 📄 更新内容 Descriptio"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE/feature_request.md",
    "chars": 540,
    "preview": "---\nname: ✨ New Feature | 新功能\nabout: 实现一个新功能或对现有功能进行增强\ntitle: 'feat: 实现 [功能名称]'\nlabels: enhancement\n---\n\n## 🌟 实现了什么?What"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "chars": 638,
    "preview": "## ✨ 变更类型 Type of Change\n\n- [ ] Bug 修复 (fix)\n- [ ] 新功能 (feat)\n- [ ] 文档更新 (docs)\n- [ ] 样式调整 (style)\n- [ ] 重构 (refactor)\n-"
  },
  {
    "path": ".github/SECURITY.md",
    "chars": 1416,
    "preview": "# Security Policy\n\n## Supported Versions\n\nWe actively maintain security updates for the following versions:\n\n| Version |"
  },
  {
    "path": ".github/workflows/AiIssueCheck.yml",
    "chars": 4447,
    "preview": "name: AI Issue Checker\n\non:\n  issues:\n    types: [opened, edited] \n\npermissions:\n  issues: write\n  pull-requests: write\n"
  },
  {
    "path": ".github/workflows/AutoCloseIssues.yml",
    "chars": 2147,
    "preview": "name: Auto Close Invalid Issues\n\non:\n  schedule:\n    - cron: \"0 3 * * *\"   # 每天 UTC 03:00 运行一次\n  workflow_dispatch:\n\nper"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 5121,
    "preview": "name: Release\n\non:\n  push:\n    branches:\n      - main\n  workflow_dispatch:\n\npermissions:\n  contents: write\n\njobs:\n  # 创建"
  },
  {
    "path": ".github/workflows/version.yml",
    "chars": 1547,
    "preview": "name: Version Check\n\non:\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njobs:\n  # 计算版本号\n  check-version:\n    if: g"
  },
  {
    "path": ".gitignore",
    "chars": 407,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n.history\n\nnode_modules"
  },
  {
    "path": ".gitmodules",
    "chars": 77,
    "preview": "[submodule \"api\"]\n\tpath = api\n\turl = https://github.com/MakcRe/KuGouMusicApi\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 969,
    "preview": "# 贡献者公约\n\n## 我们的承诺\n\n为了营造一个开放和友好的环境,作为贡献者和维护者,我们承诺让每个人都能参与我们的项目和社区,享受无骚扰的体验,无论年龄、体型、残疾、种族、性别特征、性别认同和表达、经验水平、教育、社会经济地位、国籍、个"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 4453,
    "preview": "# 贡献指南\n\n感谢您考虑为我们的项目做出贡献!无论是报告bug、提出新功能建议,还是提交代码,您的参与都将帮助我们改进这个项目。\n\n## 目录\n\n- [行为准则](#行为准则)\n- [如何贡献](#如何贡献)\n  - [报告Bug](#报"
  },
  {
    "path": "Dockerfile",
    "chars": 1585,
    "preview": "# Stage 1: Build Frontend\nFROM node:20-alpine AS frontend-builder\nWORKDIR /app\nCOPY package*.json ./\n# Remove electron a"
  },
  {
    "path": "LICENSE",
    "chars": 18091,
    "preview": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 2, June 1991\n\n Copyright (C) 1989, 1991 Fr"
  },
  {
    "path": "README.md",
    "chars": 9186,
    "preview": "<br />\n<p align=\"center\">\n    <img src=\"https://github.com/iAJue/MoeKoeMusic/raw/main/images/logo.png\" alt=\"Logo\" width="
  },
  {
    "path": "build/installer.nsh",
    "chars": 725,
    "preview": "!include \"MUI.nsh\"\n!define MUI_FINISHPAGE_LINK_LOCATION \"https://MoeJue.cn\"\n!define MUI_FINISHPAGE_LINK \"访问作者(阿珏酱)主页\"\n!d"
  },
  {
    "path": "build/license.txt",
    "chars": 849,
    "preview": "MoeKoe Music Software License Agreement\n\n1. Terms of Use\nThis software is released under the GPL-2.0 license. You are fr"
  },
  {
    "path": "docker-compose.yml",
    "chars": 305,
    "preview": "version: '3.3'\n\nservices:\n  moekoe-music:\n    container_name: moekoe-music # 容器名\n    restart: unless-stopped # 自动重启\n    "
  },
  {
    "path": "docs/README_en.md",
    "chars": 13496,
    "preview": "> **Note**: This English document may not be updated in a timely manner. For the latest content, please refer to the [Si"
  },
  {
    "path": "docs/README_ja.md",
    "chars": 10013,
    "preview": "> **注意**: この日本語ドキュメントはタイムリーに更新されない場合があります。最新の内容については[簡体字中国語版](https://github.com/iAJue/MoeKoeMusic/README.md)をご参照ください。\n<"
  },
  {
    "path": "docs/README_ko.md",
    "chars": 9838,
    "preview": "> **주의**: 이 한국어 문서는 업데이트가 지연될 수 있으니, 최신 내용은 [중국어 간체 버전](https://github.com/iAJue/MoeKoeMusic/README.md)을 참고하세요.\n<br />\n<"
  },
  {
    "path": "docs/README_ru.md",
    "chars": 11373,
    "preview": "> **Note**: Этот документ на русском языке может не быть актуальным вовремя. Для актуального материала, пожалуйста, обра"
  },
  {
    "path": "docs/README_tw.md",
    "chars": 8723,
    "preview": "> **注意**: 此繁體中文文檔可能更新不及時,最新內容請參考[簡體中文版本](https://github.com/iAJue/MoeKoeMusic/README.md)。\n<br />\n<p align=\"center\">\n<img"
  },
  {
    "path": "electron/appServices.js",
    "chars": 26148,
    "preview": "import { app, ipcMain, BrowserWindow, screen, Tray, Menu, TouchBar, globalShortcut, dialog, shell, nativeImage } from 'e"
  },
  {
    "path": "electron/extensions/extensionIPC.js",
    "chars": 10537,
    "preview": "import { ipcMain, shell, BrowserWindow, dialog } from 'electron';\nimport path from 'path';\nimport fs from 'fs';\nimport l"
  },
  {
    "path": "electron/extensions/extensionManager.js",
    "chars": 15030,
    "preview": "import { session, app } from 'electron';\nimport path from 'path';\nimport fs from 'fs';\nimport log from 'electron-log';\ni"
  },
  {
    "path": "electron/extensions/extensions.js",
    "chars": 1674,
    "preview": "// 插件系统统一入口文件\nimport extensionManager from './extensionManager.js';\nimport { registerExtensionIPC, unregisterExtensionIP"
  },
  {
    "path": "electron/language/i18n.js",
    "chars": 7904,
    "preview": "import Store from 'electron-store';\nimport { app } from 'electron';\n\nconst store = new Store();\n\nconst translations = {\n"
  },
  {
    "path": "electron/main.js",
    "chars": 9034,
    "preview": "import { app, ipcMain, globalShortcut, dialog, Notification, shell, session, powerSaveBlocker, nativeImage } from 'elect"
  },
  {
    "path": "electron/preload.cjs",
    "chars": 1989,
    "preview": "const { contextBridge, ipcRenderer } = require('electron');\n\ncontextBridge.exposeInMainWorld('electron', {\n    ipcRender"
  },
  {
    "path": "electron/services/apiService.js",
    "chars": 3500,
    "preview": "import { WebSocketServer } from 'ws';\n\nclass ApiService {\n    constructor() {\n        this.wsServer = null;\n        this"
  },
  {
    "path": "electron/services/externalLinkHandler.js",
    "chars": 1125,
    "preview": "import { shell } from 'electron';\n\nexport function shouldOpenExternally(targetUrl, currentUrl = '') {\n    try {\n        "
  },
  {
    "path": "electron/services/statusBarLyricsService.js",
    "chars": 5971,
    "preview": "import { ipcMain, nativeImage } from 'electron';\n\nclass StatusBarLyricsService {\n    constructor() {\n        this.mainWi"
  },
  {
    "path": "electron/services/updater.js",
    "chars": 3394,
    "preview": "import { app, dialog } from 'electron';\nimport electronUpdater from 'electron-updater';\nconst { autoUpdater } = electron"
  },
  {
    "path": "index.html",
    "chars": 1959,
    "preview": "<!-- public/index.html -->\n<!DOCTYPE html>\n<html lang=\"zh\">\n\n<head>\n    <meta charset=\"UTF-8\">\n    <link rel=\"manifest\" "
  },
  {
    "path": "nginx.conf",
    "chars": 696,
    "preview": "worker_processes 1;\n\nevents {\n    worker_connections 1024;\n}\n\nhttp {\n    include mime.types;\n    default_type applicatio"
  },
  {
    "path": "package.json",
    "chars": 4953,
    "preview": "{\n  \"name\": \"moekoemusic\",\n  \"version\": \"1.6.1\",\n  \"homepage\": \"https://github.com/iAJue/MoeKoeMusic\",\n  \"main\": \"electr"
  },
  {
    "path": "src/App.vue",
    "chars": 1442,
    "preview": "<template>\n    <div id=\"app\">\n        <TitleBar v-if=\"showTitleBar && !isLyricsRoute\" />\n        <RouterView />\n        "
  },
  {
    "path": "src/assets/style/PlayerControl.css",
    "chars": 15905,
    "preview": "#lyrics-container {\n    height: 75vh;\n    overflow: hidden;\n    display: flex;\n    justify-content: center;\n    padding:"
  },
  {
    "path": "src/assets/themes/dark.css",
    "chars": 13044,
    "preview": "/* 暗黑模式样式 */\nhtml.dark {\n    background-color: #121212;\n    color: #e1e1e1;\n    filter: brightness(0.8);\n    mix-blend-m"
  },
  {
    "path": "src/components/AlbumGrid.vue",
    "chars": 4120,
    "preview": "<template>\n  <div class=\"album-grid\">\n    <div v-for=\"(album, index) in albums\" :key=\"index\" class=\"album-card\" @click=\""
  },
  {
    "path": "src/components/ArtistGrid.vue",
    "chars": 3427,
    "preview": "<template>\n  <div class=\"artist-grid\">\n    <div v-for=\"(artist, index) in artists\" :key=\"index\" class=\"artist-card\" @cli"
  },
  {
    "path": "src/components/BirthdayEasterEgg.vue",
    "chars": 7894,
    "preview": "<template>\n    <span v-if=\"isBirthdayToday\" class=\"birthday-badge\">\n        <i class=\"fas fa-birthday-cake\"></i>\n       "
  },
  {
    "path": "src/components/ContextMenu.vue",
    "chars": 6661,
    "preview": "<template>\n    <div v-if=\"showContextMenu\" :style=\"{ top: `${menuPosition.y}px`, left: `${menuPosition.x}px` }\"\n        "
  },
  {
    "path": "src/components/CustomModal.vue",
    "chars": 5425,
    "preview": "<template>\n    <div>\n        <!-- Alert 模态框 -->\n        <div v-if=\"showAlert\" class=\"modal-overlay\">\n            <div cl"
  },
  {
    "path": "src/components/Disclaimer.vue",
    "chars": 3364,
    "preview": "<template>\n    <div v-if=\"showModal\" class=\"modal-overlay\">\n        <div class=\"modal-content\">\n            <h2>{{ $t('y"
  },
  {
    "path": "src/components/ExtensionManager.vue",
    "chars": 32888,
    "preview": "<template>\n    <template v-if=\"isElectron()\">\n        <div class=\"extensions-toolbar\">\n            <div class=\"extension"
  },
  {
    "path": "src/components/Header.vue",
    "chars": 13709,
    "preview": "<template>\n    <header>\n        <nav class=\"navigation\">\n            <div class=\"navigation\">\n                <button cl"
  },
  {
    "path": "src/components/MessageNotification.vue",
    "chars": 5425,
    "preview": "<template>\n    <transition-group name=\"message-fade\" tag=\"div\" class=\"message-container\" v-show=\"messages.length\">\n     "
  },
  {
    "path": "src/components/PlayerControl.vue",
    "chars": 59624,
    "preview": "<template>\n    <div class=\"player-container\">\n        <div class=\"progress-bar\" @mousedown=\"onProgressDragStart\" @click="
  },
  {
    "path": "src/components/PlaylistGrid.vue",
    "chars": 4763,
    "preview": "<template>\n  <div class=\"playlist-grid\">\n    <div v-for=\"(playlist, index) in playlists\" :key=\"index\" class=\"playlist-ca"
  },
  {
    "path": "src/components/PlaylistSelectModal.vue",
    "chars": 4385,
    "preview": "<template>\n    <transition name=\"fade\">\n        <div v-if=\"isOpen\" class=\"modal\">\n            <div class=\"modal-content\""
  },
  {
    "path": "src/components/QueueList.vue",
    "chars": 5618,
    "preview": "<template>\n    <transition name=\"fade\">\n        <div v-if=\"showQueue\" class=\"queue-popup\">\n            <div class=\"queue"
  },
  {
    "path": "src/components/StatusBarLyrics.vue",
    "chars": 7589,
    "preview": "<template>\n    <!-- 离屏 Canvas 用于生成顶部状态栏StatusBar图片 (逻辑宽 200pt * 2 = 400px, 高 22pt * 2 = 44px) -->\n    <canvas ref=\"canva"
  },
  {
    "path": "src/components/TitleBar.vue",
    "chars": 2929,
    "preview": "<template>\n  <div class=\"titlebar\">\n    <div class=\"window-controls\" v-if=\"isElectron && !isMac && $route.name !== 'Vide"
  },
  {
    "path": "src/components/player/AudioController.js",
    "chars": 13194,
    "preview": "import { ref } from 'vue';\n\nexport default function useAudioController({ onSongEnd, updateCurrentTime }) {\n    const aud"
  },
  {
    "path": "src/components/player/Helpers.js",
    "chars": 2889,
    "preview": "import { ref } from 'vue';\nimport { get } from '../../utils/request';\nimport { MoeAuthStore } from '../../stores/store';"
  },
  {
    "path": "src/components/player/LyricsHandler.js",
    "chars": 13007,
    "preview": "import { ref, nextTick } from 'vue';\nimport { get } from '../../utils/request';\n\nexport default function useLyricsHandle"
  },
  {
    "path": "src/components/player/MediaSession.js",
    "chars": 3152,
    "preview": "export default function useMediaSession() {\n  // 初始化媒体会话\n  const initMediaSession = (handlers) => {\n    if (!(\"mediaSess"
  },
  {
    "path": "src/components/player/PlaybackMode.js",
    "chars": 1491,
    "preview": "import { ref, computed } from 'vue';\n\nexport default function usePlaybackMode(t, audio) {\n  const playbackModes = ref([\n"
  },
  {
    "path": "src/components/player/ProgressBar.js",
    "chars": 5982,
    "preview": "import { ref } from 'vue';\nimport { get } from '../../utils/request';\n\nexport default function useProgressBar(audio, res"
  },
  {
    "path": "src/components/player/SongQueue.js",
    "chars": 22511,
    "preview": "import { ref } from 'vue';\nimport { get } from '../../utils/request';\nimport { MoeAuthStore } from '../../stores/store';"
  },
  {
    "path": "src/components/player/index.js",
    "chars": 468,
    "preview": "// 导出所有组件模块\nimport useAudioController from './AudioController';\nimport useLyricsHandler from './LyricsHandler';\nimport u"
  },
  {
    "path": "src/language/en.json",
    "chars": 18355,
    "preview": "{\n  \"tui-jian\": \"Recommendations\",\n  \"tui-jian-ge-qu\": \"Recommended Tracks\",\n  \"tui-jian-ge-dan\": \"Recommended Playlists"
  },
  {
    "path": "src/language/ja.json",
    "chars": 16215,
    "preview": "{\n  \"tui-jian\": \"推薦する\",\n  \"tui-jian-ge-qu\": \"おすすめの曲\",\n  \"tui-jian-ge-dan\": \"おすすめのプレイリスト\",\n  \"fa-xian\": \"発見する\",\n  \"da\": \""
  },
  {
    "path": "src/language/ko.json",
    "chars": 16002,
    "preview": "{\n  \"tui-jian\": \"추천하다\",\n  \"tui-jian-ge-qu\": \"추천곡\",\n  \"tui-jian-ge-dan\": \"추천 재생목록\",\n  \"fa-xian\": \"발견하다\",\n  \"da\": \"큰\",\n  \""
  },
  {
    "path": "src/language/ru.json",
    "chars": 19340,
    "preview": "{\n  \"tui-jian\": \"Рекомендации\",\n  \"tui-jian-ge-qu\": \"Рекомендуемые треки\",\n  \"tui-jian-ge-dan\": \"Рекомендуемые плейлисты"
  },
  {
    "path": "src/language/zh-CN.json",
    "chars": 14717,
    "preview": "{\n  \"tui-jian\": \"推荐\",\n  \"tui-jian-ge-qu\": \"推荐歌曲\",\n  \"tui-jian-ge-dan\": \"推荐歌单\",\n  \"fa-xian\": \"发现\",\n  \"yu-yan\": \"语言\",\n  \"z"
  },
  {
    "path": "src/language/zh-TW.json",
    "chars": 14144,
    "preview": "{\n  \"tui-jian\": \"推薦\",\n  \"tui-jian-ge-qu\": \"推薦歌曲\",\n  \"tui-jian-ge-dan\": \"推薦歌單\",\n  \"fa-xian\": \"發現\",\n  \"da\": \"大\",\n  \"da-kai"
  },
  {
    "path": "src/layouts/HomeLayout.vue",
    "chars": 3217,
    "preview": "<template>\n    <Header />\n    <main>\n        <div v-if=\"!isOnline\" class=\"network-status\">\n            网络连接已断开\n        <"
  },
  {
    "path": "src/main.js",
    "chars": 1452,
    "preview": "import { createApp } from 'vue';\nimport { createPinia } from 'pinia';\nimport piniaPersistedstate from 'pinia-plugin-pers"
  },
  {
    "path": "src/plugins/MessagePlugin.js",
    "chars": 1592,
    "preview": "import { createApp, ref } from 'vue';\nimport MessageNotification from '@/components/MessageNotification.vue';\n\nexport de"
  },
  {
    "path": "src/plugins/ModalPlugin.js",
    "chars": 1358,
    "preview": "import { createApp, ref } from 'vue';\nimport CustomModal from '@/components/CustomModal.vue';\n\nexport default {\n    inst"
  },
  {
    "path": "src/router/router.js",
    "chars": 3088,
    "preview": "import { createRouter, createWebHashHistory } from 'vue-router';\nimport HomeLayout from '@/layouts/HomeLayout.vue';\nimpo"
  },
  {
    "path": "src/stores/musicQueue.js",
    "chars": 839,
    "preview": "import { defineStore } from 'pinia';\n\nexport const useMusicQueueStore = defineStore('MusicQueue', {\n    state: () => ({\n"
  },
  {
    "path": "src/stores/store.js",
    "chars": 1737,
    "preview": "import { defineStore } from 'pinia';\nimport axios from 'axios';\nimport { getApiBaseUrl } from '../utils/apiBaseUrl';\n\n//"
  },
  {
    "path": "src/utils/apiBaseUrl.js",
    "chars": 2378,
    "preview": "export const DEFAULT_API_BASE_URL =\n    import.meta.env.VITE_APP_API_URL || 'http://127.0.0.1:6521';\n\nexport function no"
  },
  {
    "path": "src/utils/i18n.js",
    "chars": 917,
    "preview": "import { createI18n } from 'vue-i18n';\nimport en from '../language/en.json';\nimport ja from '../language/ja.json';\nimpor"
  },
  {
    "path": "src/utils/request.js",
    "chars": 4994,
    "preview": "// src/services/request.js\nimport axios from 'axios';\nimport { MoeAuthStore } from '../stores/store';\nimport { getApiBas"
  },
  {
    "path": "src/utils/utils.js",
    "chars": 6678,
    "preview": "import i18n from '@/utils/i18n';\n\nexport const applyColorTheme = (theme) => {\n    let colors;\n    if (theme === 'blue') "
  },
  {
    "path": "src/views/CloudDrive.vue",
    "chars": 30195,
    "preview": "<template>\n    <div class=\"detail-page\">\n        <!-- 头部信息区域 -->\n        <div class=\"header\">\n            <img class=\"co"
  },
  {
    "path": "src/views/Discover.vue",
    "chars": 7058,
    "preview": "<template>\n    <div class=\"discover-page\">\n        <h2 class=\"section-title\">{{ $t('fa-xian') }}</h2>\n        \n        <"
  },
  {
    "path": "src/views/Home.vue",
    "chars": 19463,
    "preview": "<template>\n    <div class=\"container\">\n        <h2 class=\"section-title\">{{ $t('tui-jian') }}</h2>\n        <div class=\"r"
  },
  {
    "path": "src/views/Library.vue",
    "chars": 24293,
    "preview": "<template>\n    <div class=\"library-page\">\n        <div class=\"profile-section\">\n            <div class=\"profile-header\" "
  },
  {
    "path": "src/views/LocalMusic.vue",
    "chars": 35404,
    "preview": "<template>\n    <div class=\"detail-page\">\n        <div class=\"header\">\n            <img class=\"cover-art\" :src=\"`./assets"
  },
  {
    "path": "src/views/Login.vue",
    "chars": 27504,
    "preview": "<template>\n    <div class=\"login-page\">\n        <div class=\"login-container\">\n            <img src=\"https://www.kugou.co"
  },
  {
    "path": "src/views/Lyrics.vue",
    "chars": 25928,
    "preview": "<template>\n    <div class=\"lyrics-container\" :class=\"{ 'locked': isLocked, 'hovering': isHovering && !isLocked }\">\n     "
  },
  {
    "path": "src/views/PlaylistDetail.vue",
    "chars": 55538,
    "preview": "<template>\n    <div class=\"detail-page\">\n        <!-- 头部信息区域 -->\n        <div class=\"header\">\n            <img class=\"co"
  },
  {
    "path": "src/views/Ranking.vue",
    "chars": 16279,
    "preview": "<template>\n    <div class=\"ranking-container\">\n        <!-- 添加榜单选择区域 -->\n        <div class=\"rank-selector\">\n           "
  },
  {
    "path": "src/views/Search.vue",
    "chars": 14219,
    "preview": "<template>\n    <div class=\"search-page\">\n        <div class=\"search-results\">\n            <h2 class=\"section-title\">{{ $"
  },
  {
    "path": "src/views/Settings.vue",
    "chars": 59332,
    "preview": "<template>\n    <div class=\"settings-page\">\n        <div class=\"settings-sidebar\">\n            <div v-for=\"(section, sect"
  },
  {
    "path": "src/views/VideoPlayer.vue",
    "chars": 5413,
    "preview": "<template>\n    <div class=\"video-player-page\">\n        <div class=\"video-container\">\n            <div class=\"video-heade"
  }
]

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

About this extraction

This page contains the full source code of the iAJue/MoeKoeMusic GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 98 files (893.1 KB), approximately 262.5k tokens, and a symbol index with 94 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!