
記録ステータス:
・方針確定済み、
は
。大きい機能なので一旦この本文に全
と
計画を
し、
日ここから
する。
時点 = origin/main の c7a0d04(2026-06-14)。作業ブランチ feat/53/manual-metadata-correction(worktree 保持中)。
Motivation
The AI-based title/artist extraction (#51) works well in most cases, but occasionally returns incorrect results. Once a wrong result is cached, it persists until the cache is manually cleared. Users need a way to correct misidentified songs without developer intervention.
Proposal
Provide a mechanism for users to override AI-extracted metadata when it is incorrect. Possible approaches:
- Keyboard shortcut or menu to cycle through alternative candidates
- A way to clear the cached AI result for the current song and retry
- Manual title/artist override via
or config
Related
確定スコープ
- 案
(候補サイクル
)+ 案
(キャッシュクリア再試行)+ 案
(手動オーバーライド)すべてを
する。
- 手動オーバーライドの

はキャッシュ直書き方式。 ただし
述の is_override
列で
する。
- 案
は NSStatusItem(メニューバー常駐) で
する(グローバルホット
は採らない)。


A. メタデータ解決パイプライン
解決の
順位(Sources/MetadataRepository/MetadataRepositoryImpl.swift):
- LLM cache(
ai_metadata_cache)
- LLM DataSource(
LLMMetadataDataSourceImpl)
- MusicBrainz cache(
musicbrainz_cache)
- MusicBrainz DataSource
- Regex DataSource(キャッシュなし)
:
- LLM は常に単一候補しか返さない(
[Track] だが要素は最大
)。複数候補を返すのは MusicBrainz(最大
件 × 各
バリエーション)と Regex。
- 候補配列は呼び出し元まで渡るが、キャッシュへの write は常に
.first のみ。
MetadataUseCaseImpl(Sources/MetadataUseCase/MetadataUseCaseImpl.swift)には resolve()(先頭のみ)と resolveCandidates()(全候補) の両方が公開済み。→ 候補サイクル
はこの resolveCandidates() をそのまま使える。
- LLM 抽出(
Sources/MetadataDataSource/LLMMetadataDataSourceImpl.swift)は configDataSource.load()?.config.ai から AIEndpoint を取り、MetadataExtractionPrompt を組んで OpenAICompatible(Papyrus)で chat completion を叩き、{title, artist} にデコードする。
は MediaRemote の生 track.title / track.artist 文字列。
B. キャッシュとストア
- プロトコル
MetadataDataStore(Sources/Domain/DataStore/MetadataDataStore.swift)は read(title:artist:) と write(title:artist:value:) のみ。delete / clear は
しない。
write は onConflict: .replace(UPSERT 相当)なので上書きは
。
- キャッシュ
は両ストアとも (raw_title, raw_artist) の複合ユニーク
。すなわち MediaRemote が
する生の曲名・アーティスト文字列のペア。ファイル
や内部 ID は使っていない。
- テーブル定義(
Sources/SQLiteDataStore/DatabaseManager.swift のマイグレーション):
- v3
ai_metadata_cache — (raw_title, raw_artist) → (resolved_title, resolved_artist)(GRDBLLMMetadataDataStore)
- v2
musicbrainz_cache — (query_title, query_artist) → (resolved_title, resolved_artist, duration, musicbrainz_id)(GRDBMetadataDataStore)
- SQLite 実体は
~/.cache/lyra/database 単一ファイル(XDG_CACHE_HOME 尊重。Folder+DefaultCache.swift / DatabaseManager.swift)。歌詞・Wallpaper キャッシュも
。
C. オーバーレイの
ハンドリング
AppWindow(Sources/Views/Overlay/AppWindow.swift)は styleMask: .borderless + ignoresMouseEvents = true + window level = desktopWindow + 1(通常ウィンドウより下)+ collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle]。canBecomeKey/canBecomeMain は未オーバーライド(→ false)。
- アプリの activationPolicy は
.accessory(Sources/App/ForegroundApplication.swift)。
- → オーバーレイウィンドウ自体は

もクリックも受け取れない(クリックは下のウィンドウへ透過する
的
)。
- グローバルモニタは
RipplePresenter(Sources/Presenters/Wallpaper/RipplePresenter.swift)の NSEvent.addGlobalMonitorForEvents(matching: .mouseMoved) が唯一。
ボードモニタは未使用。
な
: 初期
で「.accessory だと NSStatusItem を持てない」と
したが、これは誤り。.accessory(LSUIElement 相当)アプリでもメニューバー常駐アイコン NSStatusItem は所有できる(メニューバーに独自のアプリメニューが出ないだけで、ステータスバー項目は別物)。これが案
の
手段になる。
D. CLI / config / daemon
サブコマンド(Sources/CLI/Commands/RootCommand.swift): start / stop / restart / service / completion / version / daemon / healthcheck / config / track / benchmark。cache 操作系コマンドは無い。
lyra track -r(TrackCommand / TrackHandlerImpl)がワンショットでメタデータ解決を走らせる経路。
AIConfig(Sources/Entity/Config/AIConfig.swift)は endpoint / model / apiKey のみ。override 用フィールド無し。
- daemon と CLI は別プロセスで IPC が一切無い(XPC / Darwin notify / Unix socket / NSDistributedNotification すべて未
)。両者は SQLite キャッシュ経由でのみ間接
する。
- daemon 側
TrackInteractorImpl(Sources/TrackInteractor/TrackInteractorImpl.swift)は observeNowPlaying() を購読し removeDuplicates(by: sameTrack) で
トラックのイベントを無視する。→ CLI でキャッシュを書き換えても「今まさに再生中の曲」は
再描画されない(次曲 / 同曲再生し直し / lyra restart が
)。
方針の
— なぜ NSStatusItem か
NSStatusItem のメニューは daemon プロセス内で動く。したがって Presenter / Interactor に直接アクセスでき、ユーザーが候補を選んだ瞬間に新しい TrackUpdate を TrackInteractor から直接流して再描画できる(removeDuplicates(by: sameTrack) を迂回する経路を
本足すだけ)。
つまり CLI 経由では IPC が無くて不
だった「再生中の曲への
」が、
(in-process)経由なら IPC 無しで
する。 これが案
を採る最大の
であり、グローバルホット
(.keyDown グローバルモニタ / CGEventTap は Accessibility
が
で侵襲的)を避ける
。
メニュー
案(案
)
[lyra アイコン]
├─ 現在: <resolved title> — <resolved artist> (表示のみ・無効化)
├─ 候補を選ぶ ▸
│ ├─ ✓ <candidate 1>
│ ├─ <candidate 2>
│ └─ <candidate 3> … (MetadataUseCase.resolveCandidates の結果)
├─ 手動で入力… (title/artist 入力パネル)
├─ ─────────────
└─ キャッシュをクリアして再解決 (案2)
- 「候補を選ぶ」「手動で
」= 案
(オーバーライドをキャッシュへ直書き + 
)
- 「キャッシュをクリアして再解決」= 案

計画(.claude/rules/module-checklist.md
)
- キャッシュ
(案
) — MetadataDataStore プロトコルに delete(title:artist:) を
し、GRDBLLMMetadataDataStore / GRDBMetadataDataStore に
。
- オーバーライド

— ai_metadata_cache に is_override INTEGER DEFAULT 0 列をマイグレーション
。手動値は is_override = 1 で write し、案
のクリアは is_override = 0 の行だけ
する。これで「キャッシュ直書き」のまま、再解決クリアがユーザーの手動
を巻き込まない(直書き方式の
を
本で補う)。
- 候補取得 —
MetadataUseCase.resolveCandidates(track:) を流用。
- メニューバー
(案
) — Views/App 層に NSStatusItem 所有者を
(VIPER に合わせ Presenter + Interactor
、AppRouter で wireframe 配線)。

— オーバーライド適用後に TrackInteractor が現在トラックを再 emit する経路を
(removeDuplicates 迂回)。
- CLI(補助) —
Handler を
して lyra metadata clear / lyra metadata set --title --artist を
(
を使わない運用・スクリプト向け。CLI 経由は従来どおり次曲 or lyra restart で
)。
残
(
時に詰める)
- 手動
パネルの activation — .accessory のままテキスト
にフォーカスを当てられるか。
なら
中だけ
的に .regular へ昇格し、閉じたら戻す。
- 候補の鮮度 — メニューを開いた時点で
resolveCandidates を都度叩くか、直近
をキャッシュするか。
- CLI のコマンド形 —
metadata を独立サブコマンドにするか track の
拡張にするか。
- MusicBrainz 側のオーバーライド —
is_override
は ai_metadata_cache のみ。MusicBrainz cache 由来の誤りも上書き
にするか(解決順で LLM cache が先に当たるため、LLM 側に override を書けば
カバーできる見込み)。
の起点(再開時メモ)
- ブランチ:
feat/53/manual-metadata-correction(worktree .claude/worktrees/feat-53-metadata-correction、origin/main 起点)
- まず手を付けるべきは

(delete
)→ 
(is_override マイグレーション) の
層から。
(
)はその
。
- 主要ファイル:
Sources/Domain/DataStore/MetadataDataStore.swift / Sources/SQLiteDataStore/{GRDBLLMMetadataDataStore,GRDBMetadataDataStore,DatabaseManager}.swift / Sources/MetadataRepository/MetadataRepositoryImpl.swift / Sources/MetadataUseCase/MetadataUseCaseImpl.swift / Sources/TrackInteractor/TrackInteractorImpl.swift / Sources/App/ForegroundApplication.swift / Sources/AppRouter/(NSStatusItem 配線先)
Motivation
The AI-based title/artist extraction (#51) works well in most cases, but occasionally returns incorrect results. Once a wrong result is cached, it persists until the cache is manually cleared. Users need a way to correct misidentified songs without developer intervention.
Proposal
Provide a mechanism for users to override AI-extracted metadata when it is incorrect. Possible approaches:
Related
確定スコープ
is_overrideNSStatusItem(メニューバー常駐) でA. メタデータ解決パイプライン
解決の
順位(
Sources/MetadataRepository/MetadataRepositoryImpl.swift):ai_metadata_cache)LLMMetadataDataSourceImpl)musicbrainz_cache)[Track]だが要素は最大.firstのみ。MetadataUseCaseImpl(Sources/MetadataUseCase/MetadataUseCaseImpl.swift)にはresolve()(先頭のみ)とresolveCandidates()(全候補) の両方が公開済み。→ 候補サイクルresolveCandidates()をそのまま使える。Sources/MetadataDataSource/LLMMetadataDataSourceImpl.swift)はconfigDataSource.load()?.config.aiからAIEndpointを取り、MetadataExtractionPromptを組んでOpenAICompatible(Papyrus)で chat completion を叩き、{title, artist}にデコードする。track.title/track.artist文字列。B. キャッシュとストア
MetadataDataStore(Sources/Domain/DataStore/MetadataDataStore.swift)はread(title:artist:)とwrite(title:artist:value:)のみ。delete/clearはwriteはonConflict: .replace(UPSERT 相当)なので上書きは(raw_title, raw_artist)の複合ユニークSources/SQLiteDataStore/DatabaseManager.swiftのマイグレーション):ai_metadata_cache—(raw_title, raw_artist)→(resolved_title, resolved_artist)(GRDBLLMMetadataDataStore)musicbrainz_cache—(query_title, query_artist)→(resolved_title, resolved_artist, duration, musicbrainz_id)(GRDBMetadataDataStore)~/.cache/lyra/database単一ファイル(XDG_CACHE_HOME尊重。Folder+DefaultCache.swift/DatabaseManager.swift)。歌詞・Wallpaper キャッシュもC. オーバーレイの
ハンドリング
AppWindow(Sources/Views/Overlay/AppWindow.swift)はstyleMask: .borderless+ignoresMouseEvents = true+ window level =desktopWindow + 1(通常ウィンドウより下)+collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle]。canBecomeKey/canBecomeMainは未オーバーライド(→false)。.accessory(Sources/App/ForegroundApplication.swift)。RipplePresenter(Sources/Presenters/Wallpaper/RipplePresenter.swift)のNSEvent.addGlobalMonitorForEvents(matching: .mouseMoved)が唯一。.accessoryだとNSStatusItemを持てない」と.accessory(LSUIElement 相当)アプリでもメニューバー常駐アイコンNSStatusItemは所有できる(メニューバーに独自のアプリメニューが出ないだけで、ステータスバー項目は別物)。これが案D. CLI / config / daemon
Sources/CLI/Commands/RootCommand.swift):start/stop/restart/service/completion/version/daemon/healthcheck/config/track/benchmark。cache 操作系コマンドは無い。lyra track -r(TrackCommand/TrackHandlerImpl)がワンショットでメタデータ解決を走らせる経路。AIConfig(Sources/Entity/Config/AIConfig.swift)はendpoint/model/apiKeyのみ。override 用フィールド無し。TrackInteractorImpl(Sources/TrackInteractor/TrackInteractorImpl.swift)はobserveNowPlaying()を購読しremoveDuplicates(by: sameTrack)でlyra restartが方針の
— なぜ
NSStatusItemかNSStatusItemのメニューは daemon プロセス内で動く。したがって Presenter / Interactor に直接アクセスでき、ユーザーが候補を選んだ瞬間に新しいTrackUpdateをTrackInteractorから直接流して再描画できる(removeDuplicates(by: sameTrack)を迂回する経路をつまり CLI 経由では IPC が無くて不
だった「再生中の曲への
」が、
(in-process)経由なら IPC 無しで
する。 これが案
を採る最大の
であり、グローバルホット
(
が
で侵襲的)を避ける
。
.keyDownグローバルモニタ /CGEventTapは Accessibilityメニュー
案(案
)
.claude/rules/module-checklist.mdMetadataDataStoreプロトコルにdelete(title:artist:)をGRDBLLMMetadataDataStore/GRDBMetadataDataStoreにai_metadata_cacheにis_override INTEGER DEFAULT 0列をマイグレーションis_override = 1で write し、案is_override = 0の行だけMetadataUseCase.resolveCandidates(track:)を流用。NSStatusItem所有者をAppRouterで wireframe 配線)。TrackInteractorが現在トラックを再 emit する経路をremoveDuplicates迂回)。lyra metadata clear/lyra metadata set --title --artistをlyra restartで残
(
時に詰める)
.accessoryのままテキスト.regularへ昇格し、閉じたら戻す。resolveCandidatesを都度叩くか、直近metadataを独立サブコマンドにするかtrackのis_overrideai_metadata_cacheのみ。MusicBrainz cache 由来の誤りも上書きfeat/53/manual-metadata-correction(worktree.claude/worktrees/feat-53-metadata-correction、origin/main起点)deleteis_overrideマイグレーション) のSources/Domain/DataStore/MetadataDataStore.swift/Sources/SQLiteDataStore/{GRDBLLMMetadataDataStore,GRDBMetadataDataStore,DatabaseManager}.swift/Sources/MetadataRepository/MetadataRepositoryImpl.swift/Sources/MetadataUseCase/MetadataUseCaseImpl.swift/Sources/TrackInteractor/TrackInteractorImpl.swift/Sources/App/ForegroundApplication.swift/Sources/AppRouter/(NSStatusItem 配線先)