Patagonia Pulse 是一个 local-first 的 macOS 本地音乐播放器。它直接读取用户选择的本地音乐文件夹,提取元数据并使用 AVFoundation 播放;不需要账号、服务器、网络服务或 Apple Developer 付费订阅。
Patagonia Pulse is a local-first macOS music player. It scans user-selected local files, reads metadata with AVFoundation, and plays audio locally without accounts, servers, network services, or a paid Apple Developer subscription.
这个项目的核心目标不是做一个在线音乐平台,而是把本地音乐库的几个基本动作做好:
- 选择一个音乐目录并递归扫描;
- 稳定读取标题、艺术家、专辑、时长和封面;
- 在歌曲、艺术家、专辑、收藏和最近播放之间快速浏览;
- 播放、暂停、切歌、循环、随机和拖动进度;
- 在本机保存收藏、播放列表、最近播放和文件访问权限。
应用不会上传、复制或移动用户的音频文件。当前没有 CloudKit、iCloud、APNs、账号同步、在线流媒体或第三方数据服务。
| 领域 | 当前支持 |
|---|---|
| 播放 | 播放/暂停、停止、上一首、下一首、进度拖动、音量、Up Next 队列 |
| 播放模式 | 关闭循环、列表循环、单曲循环、随机播放 |
| 音乐库 | 选择文件夹、递归扫描、容错读取、重新扫描、SQLite 增量元数据索引、分页查询 |
| 音乐来源 | 同时管理多个本地音乐文件夹,可在侧栏移除来源 |
| 文件状态 | 保留丢失文件记录,显示旧路径,并通过添加定位文件夹自动尝试恢复稳定 ID |
| 拖拽导入 | 拖入文件夹会替换当前库;拖入音频文件会追加到当前库 |
| 元数据 | 标题、艺术家、专辑、时长、封面;缺失字段使用 fallback 文本 |
| 浏览 | 歌曲、艺术家、专辑、最近播放、收藏 |
| 推荐 | 本地“为你推荐”:播放行为、收藏/播放列表、艺术家/专辑相似度、连续播放关系、多样性重排和“少推荐”反馈 |
| 搜索 | 按歌曲名、艺术家或专辑实时过滤 |
| 收藏 | 星标收藏,使用 UserDefaults 本地保存 |
| 播放列表 | 创建、重命名、删除、添加/移除歌曲、导入 M3U/M3U8、导出 M3U8/JSON、导入 JSON |
| 智能播放列表 | 最近常听、播放最多、从未播放、重新发现、收藏;支持创建、重命名和删除规则 |
| 播放队列 | 查看接下来播放、下一首播放、加入队尾、立即播放、移除、清空、拖拽排序 |
| 访问权限 | 文件夹和拖入文件使用 security-scoped bookmark 记住本地访问权限 |
| 大库优化 | 封面按需加载、512px 缩略图、有上限的 NSCache、每 64 首批量更新扫描结果、不变文件复用元数据 |
| 快捷键 | ⌘O 打开文件夹、⇧⌘R 重新扫描、空格播放/暂停、左右方向键切歌 |
扫描器目前识别以下扩展名:
aac、aif、aiff、alac、caf、flac、m4a、m4b、mp3、mp4、wav
扩展名被识别不代表所有编码都一定能播放,最终能力取决于当前 macOS 的 AVFoundation/AVAudioPlayer 支持。如果文件损坏或编码不兼容,应用会保留 fallback 曲目信息,并在侧栏统计元数据读取失败数量。
前置条件:
- macOS 14.0 或更高版本;
- Xcode,建议使用支持 macOS 14 SDK 的版本;
- 不需要 Apple Developer 付费订阅。
打开项目:
git clone https://github.com/TheRealMilesLee/MusicPlayer-macOS.git
cd MusicPlayer-macOS
open MusicPlayer/PatagoniaPulse.xcodeproj在 Xcode 中选择 MusicPlayer scheme 和 My Mac,然后运行。scheme 名称是 MusicPlayer,生成的 app 名称是 PatagoniaPulse.app。
命令行 Debug 构建:
xcodebuild \
-project MusicPlayer/PatagoniaPulse.xcodeproj \
-scheme MusicPlayer \
-configuration Debug \
-destination 'platform=macOS' \
build项目默认使用本地 ad-hoc 签名。如果只想验证编译、不需要运行签名后的 app,可以追加:
CODE_SIGNING_ALLOWED=NO例如:
xcodebuild \
-project MusicPlayer/PatagoniaPulse.xcodeproj \
-scheme MusicPlayer \
-configuration Debug \
-destination 'platform=macOS' \
CODE_SIGNING_ALLOWED=NO \
buildRelease 构建示例:
xcodebuild \
-project MusicPlayer/PatagoniaPulse.xcodeproj \
-scheme MusicPlayer \
-configuration Release \
-destination 'platform=macOS' \
-derivedDataPath /tmp/patagonia-pulse-release-derived \
build生成的 app 位于:
/tmp/patagonia-pulse-release-derived/Build/Products/Release/PatagoniaPulse.app
本项目当前只依赖本地构建和 ad-hoc 签名,因此个人开发和本机运行不需要付费订阅。没有付费订阅时,以下事情不在项目能力范围内:
- App Store 发布;
- Developer ID 分发和正式 notarization 流程;
- CloudKit、iCloud 容器和 APNs 等开发者服务;
- 通过 Apple 的分发渠道向其他用户提供经过正式签名的安装包。
这不影响本地编译、播放、文件访问 bookmark、UserDefaults 持久化或本地播放列表。
有两种导入方式:
- 点击工具栏或菜单中的“打开音乐文件夹”,选择一个目录。应用会递归扫描该目录中的支持格式。
- 从 Finder 拖拽文件或文件夹到应用窗口。
拖拽行为是刻意区分的:
- 拖入文件夹:替换当前音乐库,并记住新的文件夹 bookmark;
- 拖入一个或多个音频文件:追加到当前音乐库,并记住文件级 bookmark;
- 点击“添加音乐文件夹”:追加一个音乐根目录,不清空现有来源和单文件导入;
- 在侧栏“音乐来源”中移除文件夹:只从应用索引移除该来源,不删除原始音乐文件;
- 重新扫描当前文件夹:文件夹内的歌曲重新读取,之前追加的单文件会尝试保留;
- 重新选择或拖入新的文件夹:切换到新文件夹,并清除之前的单文件导入记录。
应用不会复制音频文件。播放列表保存的是稳定曲目 ID,不是音频副本;M3U/M3U8 交换格式仍然保存当前文件路径。
曲目 ID 现在由本地 SQLite 索引维护:旧版本记录会把原来的标准化路径迁移为稳定 ID,新扫描的歌曲使用本地稳定标识。文件移动或重命名后,重新扫描会优先使用 macOS 文件资源标识;无法取得资源标识时,只有文件大小、标题、艺术家、专辑和时长都匹配且候选唯一,才会自动恢复原有收藏、最近播放和播放列表关联。
如果当前来源中的文件被删除、移动或暂时不可访问,重新扫描不会立即删除 SQLite 记录,而是把它们放到侧栏的“丢失文件(N)”页面。页面保留标题、旧路径和基础元数据,方便确认发生了什么。
要重新定位文件:
- 打开“丢失文件”页面,确认旧路径和歌曲信息;
- 点击“添加定位文件夹”,选择包含已移动歌曲的新文件夹;
- 应用扫描新文件夹,并按文件资源标识或唯一元数据候选恢复稳定 ID;
- 恢复成功后,收藏、最近播放、普通播放列表和推荐行为会继续关联到新路径。
多个候选无法区分时,应用会保留为丢失状态,不会静默把收藏关联到错误文件。移除来源、用“打开音乐文件夹”替换来源集合,或文件夹 bookmark 解析成全新的根路径时,旧来源记录可能被清理,当前不保证整个文件夹搬家后的自动恢复。
- 双击歌曲播放;
- 播放栏提供上一首、播放/暂停、下一首、循环、随机、停止、音量和进度控制;
- “艺术家”和“专辑”视图用于分类浏览;
- “最近播放”最多保留本机最近播放记录的前 100 首;
- “收藏”通过歌曲行末的星标或右键菜单操作;
- 在歌曲上右键可以添加到已有播放列表;
- 播放列表支持创建、重命名、删除和移除歌曲。
在播放列表页面右上角的“播放列表文件”菜单中,可以导出当前列表为 M3U8 或 Patagonia Pulse JSON,也可以导入 M3U、M3U8 或 JSON 文件。M3U/M3U8 主要用于和其他播放器交换;JSON 会额外保存播放列表 UUID、曲目顺序和基础元数据。导入时找不到的文件会在结果提示中列出数量,不会静默删除原始文件。
搜索框会根据当前视图过滤歌曲名、艺术家和专辑。歌曲、收藏、艺术家和专辑主视图使用 SQLite 分页查询,每页最多加载 200 首;播放队列仍然来自触发播放时所在的当前曲目集合。
“为你推荐”中的歌曲可以通过右键菜单选择“少推荐这首歌”或“少推荐这个艺术家”。这只是软惩罚,不会永久隐藏歌曲;再次选择“恢复推荐”即可撤销。智能播放列表保存的是规则,不是歌曲副本,会随着本机播放记录和收藏变化自动更新。
Patagonia Pulse 是本地应用,不主动发起网络请求。当前本地保存的数据包括:
| 数据 | 存储方式 | 用途 |
|---|---|---|
| 音乐文件夹 bookmarks | UserDefaults 中的 PatagoniaPulse.libraryFolderBookmarks |
重启后恢复多个文件夹来源访问 |
| 拖入文件 bookmark | UserDefaults | 重启后恢复单文件访问 |
| 最近播放曲目 ID | UserDefaults | 恢复最近播放列表 |
| 收藏曲目 ID | UserDefaults | 恢复收藏 |
| 播放列表及曲目 ID | JSON 编码后写入 UserDefaults | 恢复本地播放列表 |
| 本地播放行为 | UserDefaults 中的 PatagoniaPulse.listeningHistory.v1 |
保存播放次数、完成/跳过/中断、最近播放时间和短期连续播放关系 |
| 推荐反馈 | UserDefaults 中的 PatagoniaPulse.recommendationFeedback.v1 |
保存歌曲和艺术家的本地“少推荐”软惩罚 |
| 智能播放列表 | JSON 编码后写入 UserDefaults | 保存规则、名称和结果数量限制,打开页面时动态计算 |
| 曲目元数据索引 | 应用 Application Support 目录中的 Library.sqlite |
保存 stable_id、当前路径、文件资源标识、is_missing 和元数据,用于复用、查询和移动后重新定位 |
| 封面缩略图 | 进程内 NSCache | 只为当前界面显示,受大小上限约束,不持久化 |
项目启用了 App Sandbox,并配置了只读的用户选择文件、音乐、下载、影片和图片目录访问。音频文件不会被修改。选择新的文件夹时,应用也不会删除旧文件,只会切换当前索引和本地访问记录。
大音乐库最容易出现的问题不是音频文件大小本身,而是元数据、封面和 UI 状态被无界地留在内存中。当前实现采取了这些措施:
- 扫描阶段不再把原始封面二进制放进
MusicTrack; - 封面只在视图需要时读取,并先转换为最大 512px 的缩略图;
- 图片缓存最多 64 张、成本上限 64MB,并允许系统在内存压力下回收;
- 扫描结果每 64 首批量发布一次,减少 SwiftUI 重算;
- 扫描前比较标准化路径、文件大小和修改时间,未变化文件直接复用本地 SQLite 索引;
- 文件夹扫描使用事务更新索引,成功后把当前根目录未命中的记录标记为
is_missing = 1,中断时回滚索引更新; - 丢失记录不会参与普通歌曲、收藏、艺术家和专辑查询,但会保留在“丢失文件”页面等待重新定位;
- 新路径命中唯一候选后会复用
stable_id并清理旧路径记录,候选不唯一时不会自动猜测; - 单个损坏文件不会中断整次扫描。
主歌曲库、收藏、艺术家和专辑页面已经改为直接查询 SQLite,并使用固定大小的分页结果,避免把完整结果集交给 SwiftUI 表格。扫描和推荐目前仍保留一份轻量的 MusicTrack 元数据快照,播放队列、最近播放、普通播放列表和智能列表继续使用这份快照;后续可以再把这些次级场景改为按需查询。
MusicPlayer/
├── PatagoniaPulse.xcodeproj/
│ └── xcshareddata/xcschemes/MusicPlayer.xcscheme
└── PatagoniaPulse/
├── PatagoniaPulseApp.swift # App 入口、菜单和快捷键
├── PatagoniaPulse.entitlements # App Sandbox 入口
├── ModelAndFunctions/
│ ├── MusicLibrary.swift # 扫描、权限、来源、持久化、筛选
│ ├── LibraryIndexStore.swift # SQLite 索引、搜索、分页和增量扫描缓存
│ ├── MusicTrack.swift # 曲目模型、封面缓存和时长格式化
│ ├── MusicViewModel.swift # 播放模式、侧栏目的地、播放列表模型
│ ├── AudioPlayManager.swift # AVAudioPlayer 播放队列和状态
│ ├── FileLoadingManager.swift # NSOpenPanel/NSSavePanel 文件选择
│ ├── PlaylistFileCodec.swift # M3U8/JSON 播放列表编解码
│ ├── ListeningHistory.swift # 本地播放事件、行为统计和推荐反馈
│ ├── RecommendationEngine.swift # 混合打分和多样性重排
│ └── DoubleClick.swift # SwiftUI 双击手势辅助
└── View/
├── ContentView.swift # 主布局、侧栏、播放栏、封面
├── LocalPlaylistsView.swift # 歌曲表格和本地播放列表
├── QueueView.swift # Up Next 播放队列
├── RecommendationView.swift # 本地推荐列表
├── ArtistView.swift # 艺术家浏览
├── AlbumView.swift # 专辑浏览
├── RecentView.swift # 最近播放
└── MissingTracksView.swift # 丢失文件和重新定位入口
更完整的架构说明见 ARCHITECTURE.md。
当前仓库没有 XCTest target,也没有 CI workflow。每次涉及核心代码的修改至少应执行:
git diff --check
xcodebuild \
-project MusicPlayer/PatagoniaPulse.xcodeproj \
-scheme MusicPlayer \
-configuration Debug \
-destination 'platform=macOS' \
build核心模型和播放器也可以用 swiftc -typecheck 做独立检查。手动验证清单见 DEVELOPMENT.md。
稳定身份和 SQLite 迁移可以在没有完整 Xcode 的环境中运行回归 harness:
zsh scripts/run-library-index-harness.sh该 harness 使用临时 SQLite 数据库,不读取或修改真实音乐库。它覆盖旧库迁移、丢失记录、跨路径稳定 ID、资源标识优先、非唯一候选拒绝匹配和普通查询过滤。
如果机器只有 /Library/Developer/CommandLineTools 而没有完整的 Xcode.app,xcodebuild 无法完成 macOS app 构建;这不影响上述核心模型检查和 SQLite harness。要验证 SwiftUI 界面及真实 app 运行,仍需安装完整 Xcode,但不需要加入付费 Apple Developer 计划。
当前已验证的构建配置:macOS deployment target 14.0、Swift 5 language mode、MusicPlayer scheme、Apple Silicon Debug 构建、ad-hoc 签名。
- 多音乐文件夹来源已经支持,丢失文件页面和基于稳定 ID 的重新定位已经支持,但暂时没有独立的“离线来源”状态或重新授权流程;
- 主歌曲库、收藏、艺术家和专辑页面已经使用 SQLite 分页查询;最近播放、普通播放列表和智能列表仍是进程内动态结果;
- 播放列表可以导入/导出 M3U、M3U8 和 JSON,但还不能在 UI 中拖拽排序;
- 推荐仍只使用本地播放行为、收藏/播放列表、艺术家和专辑等信号,尚未使用音频指纹、歌词或在线画像;“少推荐”反馈目前是有上限的软惩罚,不是永久屏蔽;
- 旧版本路径 ID 会在 SQLite 迁移时保留为稳定 ID;新路径只有在资源标识匹配或元数据候选唯一时才会自动恢复,内容改变、候选重复或来源集合被替换时可能仍需人工处理;
- 当前没有独立的 source bookmark 稳定身份;整个音乐文件夹搬家后,如果 bookmark 解析为新的根路径,旧来源记录可能已被清理;
- 不会自动监视 Finder 中的文件变化,需要手动重新扫描;
- 没有内置歌词、均衡器、淡入淡出、无缝播放或系统 Now Playing 集成;
- 没有在线搜索、流媒体、账号同步、Last.fm scrobbling 或云端备份;
- 仓库根目录的
Patagonia Pulse.dmg是历史遗留文件,当前不能作为有效发布包验证。请优先从源码构建; - 当前没有正式签名、notarization 或 App Store 发布流程。
- 架构说明 / Architecture
- 开发、构建与手动验证 / Development
- 限制、数据和故障排查 / Limitations
- 路线图与功能决策 / Roadmap
- MIT License
- 为曲目增加稳定 ID、文件资源标识和
is_missing状态;旧版 SQLite 记录会自动迁移; - 重新扫描时保留未命中的记录,新增“丢失文件”页面展示旧路径和元数据;
- 添加定位文件夹后,优先按文件资源标识、其次按唯一元数据候选恢复收藏、最近播放和播放列表关联;
- 非唯一候选不会自动猜测,避免把本地行为关联到错误文件;
- 增加可在无完整 Xcode 环境下运行的 SQLite 迁移与重定位回归 harness。
- 增加 SQLite 查询驱动的歌曲、收藏、艺术家和专辑浏览;
- 支持固定大小分页、关键词搜索、排序和数据库聚合,降低大曲库 UI 内存压力;
- 增加歌曲/艺术家级别的“少推荐”和“恢复推荐”反馈,反馈只保存在本机并使用软惩罚;
- 增加可创建、重命名、删除的智能播放列表,支持最近常听、播放最多、从未播放、重新发现和收藏规则;
- 增加“为你推荐”入口;
- 记录开始、完成、跳过和中断等本地播放事件;
- 使用行为、内容、连续播放关系、显式偏好和探索位混合打分,并通过艺术家/专辑多样性重排;
- 推荐数据只写入本机,不需要账号、网络或 Apple Developer 付费服务。
- 支持从 M3U/M3U8 导入播放列表;
- 支持导出 M3U8 和 Patagonia Pulse JSON 播放列表;
- 导入时报告缺失文件和不支持格式,不静默吞掉整张播放列表。
- 支持多个本地音乐文件夹来源;
- 使用 bookmark 数组恢复多个来源,并兼容旧版本的单文件夹 bookmark;
- 支持在侧栏移除来源,且不会删除用户原始音乐文件。
- 增加可见的 Up Next 播放队列;
- 支持下一首播放、加入队尾、立即播放、移除、清空和拖拽排序;
- 随机模式只把队列作为候选池,不伪装成固定的下一首顺序。
- 增加本地 SQLite 曲目元数据索引;
- 通过文件大小和修改时间跳过未变化文件的重复元数据读取;
- 文件夹扫描使用事务更新索引,支持成功清理失效记录和中断回滚。
- 修复大音乐库扫描时封面原始数据无界驻留的问题;
- 增加按需封面加载、512px 缩略图和有上限的
NSCache; - 扫描结果改为批量发布,降低 SwiftUI 重算和内存峰值;
- 使用现代
NSItemProviderAPI 读取 Finder 拖拽 URL,避免新 SDK 弃用警告; - 完整同步当前构建、权限、持久化和 Apple Developer 订阅边界文档。
- 修正拖入单文件会替换整个音乐库的问题,改为追加导入;
- 增加单文件 security-scoped bookmark;
- 清理 macOS 14 的
onChangeAPI 警告。
- 增加收藏和本地播放列表;
- 增加 Finder 拖拽导入;
- 播放列表、收藏和最近播放使用本地持久化。
- 重整音乐库和播放器核心;
- 递归扫描音频文件并记住上次选择的文件夹;
- 增加艺术家、专辑和最近播放浏览;
- 修复时间显示、无封面崩溃、路径错位和失焦退出。
本项目使用 MIT License,详见 LICENSE。