Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
163 commits
Select commit Hold shift + click to select a range
f9fb2e1
设计插件系统
power721 Apr 5, 2026
5fa90a4
新增插件SDK
power721 Apr 5, 2026
0314b64
修复插件SDK契约
power721 Apr 5, 2026
f488171
收紧清单校验与SDK类型契约
power721 Apr 5, 2026
7fb4d98
实现插件运行时
power721 Apr 5, 2026
fcc61a4
修复禁用外部插件预加载
power721 Apr 5, 2026
2622679
完善插件加载与安装容错
power721 Apr 5, 2026
9f6ce63
修复插件相对导入缓存污染
power721 Apr 5, 2026
979f69c
修复插件相对导入审计误判
power721 Apr 5, 2026
eaec8e4
拆分安装校验与运行时实例化
power721 Apr 5, 2026
d63a0f8
修复插件安装校验与加载回滚
power721 Apr 5, 2026
e29963a
修复插件启用状态与安装事务性
power721 Apr 5, 2026
34272c7
修复插件状态存储容错与内置禁用
power721 Apr 5, 2026
9356570
忽略插件安装临时目录
power721 Apr 5, 2026
28be49e
接入插件宿主桥接
power721 Apr 5, 2026
c2d5155
新增插件管理页
power721 Apr 5, 2026
382fad1
支持插件侧边栏页面
power721 Apr 5, 2026
ca9cf1d
迁移LRCLIB插件
power721 Apr 5, 2026
e45c5bd
创建QQ音乐插件包
power721 Apr 5, 2026
57d8511
迁移QQ音乐宿主接线
power721 Apr 5, 2026
4b81d0e
添加插件打包脚本
power721 Apr 5, 2026
1fca94a
切换QQ源到插件提供
power721 Apr 5, 2026
9e04444
移除宿主在线音乐入口
power721 Apr 5, 2026
d8a1e52
移除宿主QQ服务注入
power721 Apr 5, 2026
090d29a
切换宿主QQ配置到插件命名空间
power721 Apr 5, 2026
1e5d5a4
切换在线服务QQ配置读取
power721 Apr 5, 2026
9462c32
解耦QQ客户端与在线服务
power721 Apr 5, 2026
ae9716f
清理宿主QQ源实现
power721 Apr 5, 2026
a060580
切换在线视图QQ配置读取
power721 Apr 5, 2026
666268f
切换QQ凭据到插件安全存储
power721 Apr 5, 2026
6f09c2b
移除QQ专用配置接口
power721 Apr 5, 2026
2416e71
让QQ插件通过导入审计
power721 Apr 5, 2026
24c7a1e
抽离通用音质工具
power721 Apr 5, 2026
e7ba241
保留插件安装器后缀
power721 Apr 5, 2026
26eb5e1
通过插件注册表解析QQ封面
power721 Apr 5, 2026
883b07d
增加内置插件加载冒烟测试
power721 Apr 5, 2026
9e9e826
验证QQ插件打包安装
power721 Apr 5, 2026
915b8e7
完善QQ插件设置页
power721 Apr 5, 2026
e4571f0
增加QQ插件登录入口
power721 Apr 5, 2026
0b318cc
添加QQ插件扫码客户端
power721 Apr 5, 2026
4e4606e
连接QQ插件本地登录对话框
power721 Apr 5, 2026
178d092
完善QQ插件凭据管理
power721 Apr 5, 2026
973db02
更新音质工具测试引用
power721 Apr 5, 2026
3a53125
增强内置插件集成校验
power721 Apr 5, 2026
48a7611
通过插件注册表解析QQ歌词
power721 Apr 5, 2026
48c8847
收窄宿主对话框导出面
power721 Apr 5, 2026
e7e7a7a
移除宿主QQ登录对话框
power721 Apr 5, 2026
3e0e41e
迁移QQ云服务到插件目录
power721 Apr 6, 2026
b5f4091
删除旧QQ歌词模块
power721 Apr 6, 2026
aaadc4e
完善QQ插件在线页
power721 Apr 6, 2026
7c75bba
补充QQ插件页迁移设计
power721 Apr 6, 2026
7bb9c93
完善插件系统
power721 Apr 7, 2026
9ec3de4
修复登录状态
power721 Apr 7, 2026
c93415b
补充插件管理页开关设计
power721 Apr 7, 2026
c1fb5d8
修复下拉框
power721 Apr 7, 2026
9cfbe1e
补充Kugou歌词插件设计
power721 Apr 7, 2026
0d73757
新增酷狗歌词插件骨架
power721 Apr 7, 2026
b36c8b2
插件管理
power721 Apr 7, 2026
cb77691
移除宿主内置酷狗歌词源
power721 Apr 7, 2026
667f3f5
补充酷狗歌词插件测试
power721 Apr 7, 2026
84543bd
修复QQ音乐歌词下载
power721 Apr 7, 2026
b3fc7ac
Merge branch 'feature/kugou-lyrics-plugin' into feature/plugin-system
power721 Apr 7, 2026
631242b
ITunesCoverPlugin
power721 Apr 7, 2026
c7ca57e
统一主题控件样式设计
power721 Apr 7, 2026
e2a9f9d
LastFmCoverPlugin
power721 Apr 7, 2026
949fae6
扩展统一主题基础层设计
power721 Apr 7, 2026
6afa3fc
LastFmCoverPlugin
power721 Apr 7, 2026
84d33b7
新增网易插件拆分设计
power721 Apr 7, 2026
b6df0aa
统一主题基础样式接口
power721 Apr 7, 2026
cbda742
收敛宿主共享组件主题样式
power721 Apr 7, 2026
4d95f83
统一宿主与插件基础控件主题入口
power721 Apr 7, 2026
20bb805
Merge branch 'feature/unified-foundation-theme-styles' into feature/p…
power721 Apr 7, 2026
e89bfd8
plan
power721 Apr 7, 2026
ca731ab
网易云插件
power721 Apr 7, 2026
6836541
Merge branch 'feature/netease-plugin-split' into feature/plugin-system
power721 Apr 7, 2026
0c6f875
修复歌词下载
power721 Apr 7, 2026
f614d18
修复样式
power721 Apr 7, 2026
20ea8dd
修复样式
power721 Apr 7, 2026
ab74cc0
修复样式
power721 Apr 7, 2026
d135a88
修复样式
power721 Apr 7, 2026
6b5be88
修复样式
power721 Apr 7, 2026
a116330
清理QQ插件独立发布边界
power721 Apr 7, 2026
73d0405
Merge branch 'feature/qqmusic-externalization-cleanup' into feature/p…
power721 Apr 7, 2026
43c5c38
doc
power721 Apr 7, 2026
24c2ce1
Merge branch 'master' into feature/plugin-system
power721 Apr 7, 2026
a72af60
修复应用UI回调签名
power721 Apr 8, 2026
2c17580
修复SingleFlight完成竞态
power721 Apr 8, 2026
7086ee9
修复播放索引竞态
power721 Apr 8, 2026
c4e5cfe
修复下一曲播放竞态
power721 Apr 8, 2026
2c5a92c
修复下载后播放锁竞争
power721 Apr 8, 2026
d0bf026
修复睡眠定时器零音量淡出
power721 Apr 8, 2026
4c97c68
修复元数据路径异常回退
power721 Apr 8, 2026
a71f7a9
修复
power721 Apr 8, 2026
c4039b8
优化插件系统
power721 Apr 8, 2026
72394c8
修复插件主题桥初始化兜底
power721 Apr 8, 2026
8a04dbe
修复插件状态存储并发写入
power721 Apr 8, 2026
671db79
修复文件整理回滚错误上报
power721 Apr 8, 2026
ffe38a9
修复酷狗歌词候选缺字段
power721 Apr 8, 2026
a2dee27
修复网易歌词缺失字段
power721 Apr 8, 2026
55534c7
添加QQ音乐Provider收口设计
power721 Apr 8, 2026
069ab76
修复Qt后端对象父级绑定
power721 Apr 8, 2026
2f78eef
修复HTTP共享客户端退出清理
power721 Apr 8, 2026
315d2bc
修复配置密钥存储空值兜底
power721 Apr 8, 2026
0a4b6ae
修复空流派ID冲突
power721 Apr 8, 2026
6d2bb6a
修复播放项反序列化类型
power721 Apr 8, 2026
6134e30
修复LRCLIB响应类型校验
power721 Apr 8, 2026
20a6542
添加QQ音乐Provider歌词入口
power721 Apr 8, 2026
8224f90
添加QQ音乐Provider封面入口
power721 Apr 8, 2026
3be99b5
替换歌词面板过时菜单调用
power721 Apr 8, 2026
f115de6
减少在线歌曲批量入队查找
power721 Apr 8, 2026
f7a8c26
统一QQ音乐歌词封面来源入口
power721 Apr 8, 2026
995ec6b
修复在线provider_id透传
power721 Apr 8, 2026
68eadba
修复在线队列provider占位值
power721 Apr 8, 2026
47e3d9e
迁移旧QQ在线provider数据
power721 Apr 8, 2026
2468cc5
修复QQ音乐下载
power721 Apr 8, 2026
5abc8e4
整理QQ插件重构设计
power721 Apr 8, 2026
56cac63
修复退出时热键清理
power721 Apr 8, 2026
32dc518
修复播放队列弹窗释放
power721 Apr 8, 2026
e4a2047
修复播放列表删除事务回滚
power721 Apr 8, 2026
7f72ea2
修复图片缓存清理迭代竞争
power721 Apr 8, 2026
4815e5e
修复MPRIS服务引用竞态
power721 Apr 8, 2026
963abb1
修复国际化状态并发访问
power721 Apr 8, 2026
9b7cbf6
细化QQ插件重构计划
power721 Apr 8, 2026
6d60d2c
修复数据库跨线程连接清理
power721 Apr 8, 2026
9862a2d
提取QQ音乐媒体辅助函数
power721 Apr 8, 2026
1a2d655
修复云账户硬删除副作用
power721 Apr 8, 2026
543491c
统一QQ音乐搜索结果归一化
power721 Apr 8, 2026
d5b2223
修复迷你播放器封面线程退出
power721 Apr 8, 2026
e81c079
收敛QQ音乐卡片组装逻辑
power721 Apr 8, 2026
aa4df75
收敛QQ音乐Provider与Client职责
power721 Apr 8, 2026
8a6687e
收敛QQ音乐服务层格式化逻辑
power721 Apr 8, 2026
455999f
优化网易云插件
power721 Apr 8, 2026
7ffab93
优化QQ音乐插件结构
power721 Apr 8, 2026
ad596e9
优化歌词下载
power721 Apr 8, 2026
0065ce3
Merge branch 'qqmusic-refactor-inline' into feature/plugin-system
power721 Apr 8, 2026
4c966b8
编写基础优化设计
power721 Apr 8, 2026
e51d1ec
优化扫码登录
power721 Apr 8, 2026
5277171
缓存聚合实体ID
power721 Apr 8, 2026
960492b
优化专辑查询封面聚合
power721 Apr 8, 2026
965d886
优化歌手查询封面聚合
power721 Apr 8, 2026
7f25d1f
移除流派随机封面查询
power721 Apr 8, 2026
84844a2
优化本地歌词读取路径
power721 Apr 8, 2026
b6e51ae
限制数据库写队列大小
power721 Apr 8, 2026
89938d6
为HTTP客户端增加重试
power721 Apr 8, 2026
a6ee334
节流下载进度回调
power721 Apr 8, 2026
4ec4b4a
改进图片缓存原子写入
power721 Apr 8, 2026
d86811e
限制图片缓存容量
power721 Apr 8, 2026
98c8f63
修复歌词下载
power721 Apr 8, 2026
b4d3b74
修复百度删除接口令牌提取
power721 Apr 8, 2026
6ef599e
兼容轻量本地播放轨道对象
power721 Apr 8, 2026
b88fc33
为线程运行检查补充isValid守卫
power721 Apr 8, 2026
3ac5dc2
为QQ音乐桥接补充安全主题回退
power721 Apr 8, 2026
af33852
让插件SDK测试按需构建产物
power721 Apr 8, 2026
3828536
补充云盘线程守卫导入
power721 Apr 8, 2026
3360399
修复歌词搜索
power721 Apr 8, 2026
b025192
修复插件解压穿越
power721 Apr 8, 2026
846a0db
修复坏插件阻断发现
power721 Apr 8, 2026
01f5b03
修复在线缓存删除
power721 Apr 8, 2026
d1ff4d4
修复云盘空目录缓存
power721 Apr 8, 2026
422bd12
修复在线收藏源冲突
power721 Apr 8, 2026
00b95cd
修复在线批量查歌串源
power721 Apr 8, 2026
5f4a3d1
修复mpv退出崩溃
power721 Apr 8, 2026
498b32b
修复批量查歌兼容回归
power721 Apr 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,15 @@ Dependencies are managed by `pyproject.toml` and installed using uv:
4. Browse cloud folders and click music files to start playback
5. Music will automatically download to the local cache directory

### QQ Music Login

1. Go to Settings -> QQ Music Configuration
2. Click the "Scan to Login" button
3. Select login method: QQ or WeChat
4. Scan the QR code with mobile QQ or WeChat
5. Confirm login on your phone
6. Credentials will be automatically saved after successful login
### QQ Music Plugin

1. Open `Settings -> Plugins`
2. Install the QQ Music plugin zip or enable the external QQ Music plugin if it is already installed
3. Open the plugin's own settings tab and click the login button
4. Select login method: QQ or WeChat
5. Scan the QR code with mobile QQ or WeChat
6. Confirm login on your phone
7. Credentials will be saved in the plugin's own settings

### Playback Control

Expand Down
5 changes: 4 additions & 1 deletion app/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def set_main_window(self, window):
"""Set main window."""
self._main_window = window

def _dispatch_to_ui(fn, *args, **kwargs):
def _dispatch_to_ui(self, fn, *args, **kwargs):
QTimer.singleShot(0, lambda: fn(*args, **kwargs))

def run(self) -> int:
Expand Down Expand Up @@ -136,6 +136,9 @@ def quit(self):
# Stop MPRIS D-Bus service
self._bootstrap.stop_mpris()

from system import hotkeys
hotkeys.cleanup()

# Stop cache cleaner service
cache_cleaner = self._bootstrap.cache_cleaner_service
if cache_cleaner:
Expand Down
210 changes: 129 additions & 81 deletions app/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
Bootstrap - Dependency injection container.
"""

import importlib
import logging
import subprocess
import sys
import threading
from pathlib import Path
from typing import TYPE_CHECKING, Optional

from infrastructure import HttpClient
Expand All @@ -28,18 +32,95 @@
from services.playback import PlaybackService, QueueService
from system.config import ConfigManager
from system.event_bus import EventBus
from system.plugins.host_services import BootstrapPluginContextFactory
from system.plugins.manager import PluginManager
from system.plugins.state_store import PluginStateStore

if TYPE_CHECKING:
from services.lyrics.qqmusic_lyrics import QQMusicClient
from services.online import OnlineDownloadService, OnlineMusicService
from services.online.cache_cleaner_service import CacheCleanerService
from services.download.online_download_gateway import OnlineDownloadGateway
from services.download.cache_cleaner_service import CacheCleanerService
from services.playback.sleep_timer_service import SleepTimerService
from system.mpris import MPRISController
from system.theme import ThemeManager

logger = logging.getLogger(__name__)


def _can_import_linux_mpris_runtime() -> tuple[bool, Optional[str]]:
try:
import dbus
import dbus.mainloop.glib
import dbus.service
from gi.repository import GLib

_ = (dbus.mainloop.glib, dbus.service, GLib)
return True, None
except ImportError as exc:
return False, str(exc)


def _discover_linux_python_module_roots() -> list[str]:
python_bin = Path("/usr/bin/python3")
if not python_bin.exists():
return []

try:
result = subprocess.run(
[
str(python_bin),
"-c",
(
"import importlib, os\n"
"roots = []\n"
"for name in ('dbus', 'gi'):\n"
" try:\n"
" module = importlib.import_module(name)\n"
" except Exception:\n"
" continue\n"
" path = getattr(module, '__file__', None)\n"
" if not path:\n"
" continue\n"
" root = os.path.dirname(os.path.dirname(path))\n"
" if root not in roots:\n"
" roots.append(root)\n"
"print('\\n'.join(roots))\n"
),
],
check=True,
capture_output=True,
text=True,
)
except (OSError, subprocess.CalledProcessError):
return []

roots = []
for line in result.stdout.splitlines():
root = line.strip()
if root and root not in roots:
roots.append(root)
return roots


def _ensure_linux_mpris_runtime() -> tuple[bool, Optional[str]]:
if sys.platform != "linux":
return True, None

ready, reason = _can_import_linux_mpris_runtime()
if ready:
return True, None

added = False
for root in reversed(_discover_linux_python_module_roots()):
if root and root not in sys.path:
sys.path.insert(0, root)
added = True

if added:
importlib.invalidate_caches()

return _can_import_linux_mpris_runtime()


class Bootstrap:
"""
Dependency injection container.
Expand Down Expand Up @@ -85,16 +166,15 @@ def __init__(self, db_path: str = "Harmony.db"):
self._cloud_file_service: Optional[CloudFileService] = None
self._cover_service: Optional[CoverService] = None
self._file_org_service: Optional["FileOrganizationService"] = None
self._online_music_service: Optional["OnlineMusicService"] = None
self._online_download_service: Optional["OnlineDownloadService"] = None

# QQ Music client
self._qqmusic_client: Optional["QQMusicClient"] = None
self._online_download_service: Optional["OnlineDownloadGateway"] = None

# Services
self._cache_cleaner_service: Optional["CacheCleanerService"] = None
self._sleep_timer_service: Optional["SleepTimerService"] = None
self._mpris_controller: Optional["MPRISController"] = None
self._mpris_disabled_reason: Optional[str] = None
self._plugin_manager: Optional[PluginManager] = None
self._plugins_loaded = False

@classmethod
def instance(cls, db_path: str = "Harmony.db") -> "Bootstrap":
Expand Down Expand Up @@ -341,83 +421,51 @@ def file_org_service(self) -> FileOrganizationService:
)
return self._file_org_service

# ===== QQ Music =====

@property
def qqmusic_client(self) -> "QQMusicClient":
"""Get QQ Music client."""
if self._qqmusic_client is None:
from services.lyrics.qqmusic_lyrics import QQMusicClient
self._qqmusic_client = QQMusicClient()
return self._qqmusic_client

def refresh_qqmusic_client(self):
"""Refresh QQ Music client and online music services (call after login)."""
from services.lyrics.qqmusic_lyrics import QQMusicClient

# Refresh lyrics client
self._qqmusic_client = QQMusicClient()

# Reset online music service to pick up new credentials
self._online_music_service = None
self._online_download_service = None

logger.info("QQ Music client and online music services refreshed")
return self._qqmusic_client

def refresh_online_music_service(self) -> "OnlineMusicService":
"""Force refresh of online music service with current credentials."""
self._online_music_service = None
def plugin_manager(self) -> PluginManager:
"""Get plugin manager."""
if self._plugin_manager is None:
logger.info("[Bootstrap] Initializing plugin manager")
self._plugin_manager = PluginManager(
builtin_root=Path("plugins/builtin"),
external_root=Path("data/plugins/external"),
state_store=PluginStateStore(Path("data/plugins/state.json")),
context_factory=BootstrapPluginContextFactory(
self,
storage_root=Path("data/plugins/storage"),
),
)
if not self._plugins_loaded:
logger.info("[Bootstrap] Loading enabled plugins")
self._plugin_manager.load_enabled_plugins()
self._plugins_loaded = True
logger.info("[Bootstrap] Plugin loading finished")
return self._plugin_manager

def refresh_online_download_service(self) -> "OnlineDownloadGateway":
"""Force refresh of host online download gateway."""
self._online_download_service = None
return self.online_music_service
return self.online_download_service

# ===== Online Music =====

@property
def online_music_service(self) -> "OnlineMusicService":
"""Get online music service."""
if self._online_music_service is None:
from services.online import OnlineMusicService
from services.cloud.qqmusic.qqmusic_service import QQMusicService

# Try to create QQMusicService if credential is available
qqmusic = None
if self.config:
# Use get_qqmusic_credential() to get full credential including refresh_token
credential = self.config.get_qqmusic_credential()
if credential and credential.get('musicid') and credential.get('musickey'):
try:
qqmusic = QQMusicService(credential)
logger.info(f"QQMusicService initialized for OnlineMusicService, "
f"musicid={credential.get('musicid')}, "
f"has_refresh_key={bool(credential.get('refresh_key'))}, "
f"has_refresh_token={bool(credential.get('refresh_token'))}")
except Exception as e:
logger.debug(f"Failed to initialize QQMusicService: {e}")

self._online_music_service = OnlineMusicService(
config_manager=self.config,
qqmusic_service=qqmusic
)
return self._online_music_service

@property
def online_download_service(self) -> "OnlineDownloadService":
"""Get online download service."""
def online_download_service(self) -> "OnlineDownloadGateway":
"""Get host online download gateway."""
if self._online_download_service is None:
from services.online import OnlineDownloadService
self._online_download_service = OnlineDownloadService(
from services.download.online_download_gateway import OnlineDownloadGateway
self._online_download_service = OnlineDownloadGateway(
config_manager=self.config,
qqmusic_service=None,
online_music_service=self.online_music_service
plugin_manager=lambda: self._plugin_manager,
event_bus=self.event_bus,
)
return self._online_download_service

@property
def cache_cleaner_service(self) -> "CacheCleanerService":
"""Get cache cleaner service."""
if self._cache_cleaner_service is None:
from services.online.cache_cleaner_service import CacheCleanerService
from services.download.cache_cleaner_service import CacheCleanerService
self._cache_cleaner_service = CacheCleanerService(
config_manager=self.config,
download_service=self.online_download_service,
Expand All @@ -441,18 +489,18 @@ def sleep_timer_service(self) -> "SleepTimerService":
def mpris_controller(self) -> "MPRISController":
"""Get MPRIS D-Bus controller (Linux only)."""
if self._mpris_controller is None:
import sys
if sys.platform == "linux":
ready = False
try:
import dbus
import dbus.mainloop.glib
import dbus.service
from gi.repository import GLib
_ = (dbus.mainloop.glib, dbus.service, GLib)
ready = True
except ImportError:
pass
if self._mpris_disabled_reason is not None:
return None
ready, reason = _ensure_linux_mpris_runtime()
if not ready:
self._mpris_disabled_reason = reason or "unknown import error"
logger.warning(
"MPRIS disabled: missing Linux D-Bus runtime (%s). "
"Install the optional 'linux' dependencies and ensure system "
"PyGObject bindings are available to the application.",
self._mpris_disabled_reason,
)

if ready:
from system.mpris import MPRISController
Expand Down
7 changes: 0 additions & 7 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,13 +391,6 @@ def collect_hidden_imports(audio_backend_bundle: str = AUDIO_BACKEND_ALL) -> lis
except Exception as e:
print(f"Warning: Could not collect mutagen submodules: {e}")

# QQ音乐 API
try:
hiddenimports += collect_submodules("qqmusic_api")
print("Collected submodules for: qqmusic_api")
except Exception as e:
print(f"Warning: Could not collect qqmusic_api submodules: {e}")

# 其他依赖
for package in ["PIL", "qrcode", "bs4", "lxml"]:
try:
Expand Down
Loading
Loading