Music Metadata Import Snapshots - log5r/simple-media-player GitHub Wiki

Music のメタデータ補完と編集値の優先順位

Issue #26 を修正する PR #41 の記録。2026-10-07、main の dd3f121 を基点に調査し、4112b1a で修正した。macOS の情報編集に Music の古い値が混入する問題を防ぎ、取り込みで行う Music 照合の取得回数を抑えた。

編集画面で古い値が復元される原因

以前の編集処理は、ライブラリ項目とファイルの MP4 タグを読んだ後に Music の値を重ねていた。アプリ管理下のコピーのパスは Music の原本と一致しないため、ソート用の曲名・アーティスト・アルバムと長さによる照合へ進む。その結果、このアプリでジャンルを変更したりコメントを消したりしても、Music の古い値がドラフトへ入り、一括編集でそのまま保存される場合があった。

修正後は Music 照合を取り込み時だけに限定する。情報編集と一括編集は、ライブラリ項目とアプリ管理下のファイルの値を読む。取り込み時に Music の表示用メタデータを MP4・AVFoundation の値より優先する順序は維持した。この修正は、ほかのタグ格納場所や歌詞の読み直しによる復元を扱う Issue #36 とは範囲が異なる。

取り込み操作ごとの情報取得

取り込み操作ごとに照合用のセッションを作り、最初の対象音声でだけ Music ライブラリ情報を取得する。固定 AppleScript の properties of every file track of library playlist 1 でレコードをまとめて読み、Swift の値へ復号した後、ファイルの場所とソート用曲名の索引を作る。以後の曲は手元の索引で照合する。元ファイルの場所を優先し、一致しない場合はソート用曲名、必要なアーティスト・アルバム、1 秒未満の長さの差を確認する。重複する候補の最初の曲を採用する順序も維持した。

セッションは取得中の Task を共有するため、並行した要求も一度の取得を待つ。未起動時の空結果と読み取り失敗もその取り込み内では保持する。次の取り込みでは新しいセッションを作り、Music の変更を読めるようにする。キャンセル済みの要求は取得を開始せず、待機中にキャンセルされた要求にも結果を返して適用しない。他の要求が共有している取得そのものは取り消さない。

AppleScript の生成・実行、レコード復号、索引作成は、専用の直列 DispatchQueue で実行する。MainActor と協調スレッドプールは取得完了を非同期に待つ。コンパイル済みスクリプトを読み取りワーカーが再利用し、Music のメタデータのキャッシュは取り込み操作をまたいで保持しない。

macOS の起動中アプリ一覧を確認し、Music が未起動ならスクリプトを実行しない。スクリプトにも実行直前の起動状態の確認を入れ、通常の取り込みによる Music の起動を避ける。オートメーション権限の拒否やタイムアウトでは埋め込み値へ戻り、同じ取り込みで繰り返し問い合わせない。iPhone と iPad では Music の AppleScript 連携を使わず、従来どおり埋め込み値を使う。

外部形式の確認

Music.app に同梱された com.apple.Music.sdef で、各プロパティの 4 バイトコードと長さの単位(秒)を確認した。レコードは NSAppleEventDescriptor から復号し、ファイルの場所は File URL へ変換する。テストでは Music を操作せず、using terms from application "Music" によるローカルレコードを生成して、実際の AppleScript の返却形式を確認した。真偽値には typeBoolean に加えて typeTrue / typeFalse が返るため、三つを処理する。missing value、空白だけの値、0 の年・番号は値がない状態を維持する。

参照: NSAppleScript、NSAppleEventDescriptor、AppleScript のアプリケーションオブジェクトと running プロパティ。

修正前後の検証

条件 確認方法 結果
編集後に Music の値が復元される AAC 音声を生成して M4A タグを保存。取り込みでテスト用 Music 値を補完し、ジャンルを変更・コメントを削除。新しい ModelContext で編集を開き直して一括編集 以前の編集時上書き処理を同じテスト用の照合元で戻すと、ジャンル・コメント・ファイルへの書き戻しを含む 8 箇所の期待が失敗。最終実装は成功
取得回数が曲数に比例する 1,000 件の並行照合と次の取り込みに相当する新セッション 同一セッションの情報取得は 1 回、次のセッションで合計 2 回。テスト用取得元を使用
読めないライブラリの再試行 失敗・空結果の取得元へ各 1,000 件を照合 取得はそれぞれ 1 回
場所とソート用情報の照合 正規化した場所、候補重複、アーティスト・アルバムの不一致、長さの境界・非有限値 場所の一致を優先し、条件を満たす最初の候補を採用
Music 未起動 起動状態を偽にした実際の読み取りワーカー スクリプト実行へ進まず空結果を取得
要求のキャンセル 取得前のキャンセルと、継続を保留して取得中にキャンセル キャンセルされた要求は値を受け取らず、別要求は共有取得の結果を利用
AppleScript の返却形式 ローカルで生成したレコードと異常な返却値 ファイルの場所・全編集項目・true/false・空値を復号し、異常なリスト要素は読み取り失敗扱い
既存処理の回帰 MP4/ID3 のタグ、編集キャンセル、取り込み重複判定・マイグレーション・境界条件 macOS の関連単体テスト 56 件が成功

Xcode 27.1 Beta / Swift 6 を使用。macOS の関連単体テスト 56 件と iOS Simulator 向けビルドが成功し、iPhone 18 Pro(iOS 27.0)の新しい関連単体テスト 7 件も成功した。画面構成の変更がないため、UI テストは実施していない。

./scripts/lint.sh は成功し、既存警告 43 件が残る。ベースラインを除いた全診断は変更前 115 件から変更後 114 件へ減り、ファイル・ルール・重大度ごとに追加はなかった。ベースライン変更は、短縮した既存テストファイルの長さの記録(2903→2895)だけで、新しい違反は追加していない。

レビューで指摘された応答待ち時間

レビューコメント は、全曲のプロパティ取得に設定した 30 秒の制限が、大規模または応答の遅い Music ライブラリの補完を打ち切る点を指摘した。コードでは取得失敗をその取り込みセッション全体で保持するため、最初の取得が時間切れになると、後続の MP4 音声も Music の値を補完できない。この指摘は妥当と判断した。

追加修正では with timeout of 30 seconds を削除し、AppleScript 標準の応答待ち時間に戻した。Apple の仕様 は既定の待ち時間を 2 分と定める。時間切れは送信先の処理を取り消す機能ではないため、失敗時に再試行を重ねる方式は採用しなかった。取得処理は引き続き専用ワーカーで実行し、MainActor は非同期に待つ。

Xcode 27.1 Beta / Swift 6 で macOS のビルドと Music 照合・編集・MP4 の関連単体テスト 17 件が成功し、lint も成功した(既存警告 43 件、ベースライン変更なし)。修正は macOS 限定の AppleScript 文字列だけに行い、iPhone/iPad の処理は変更していない。30 秒を超える実際の Music ライブラリの応答は再現していないため、テスト成功を大規模ライブラリの性能検証済みとはしない。標準の待ち時間を超える取得まで完了を保証する変更でもない。

未計測の条件

実際の大規模 Music ライブラリからの情報取得時間、最大メモリ使用量、オートメーションの許可・拒否ダイアログ、および実機の操作は確認していない。全 file track のプロパティを一度取得するため、取得時のコストは Music ライブラリの規模に依存する。1,000 件のテストはセッション共有と照合回数の検証であり、実際の 1,000 曲の取り込み時間を測った結果ではない。

Music の終了と起動状態の確認が同時に起きる場合の挙動も手動では未確認。取得した情報はその取り込み操作で固定し、取り込みの途中に Music で行った編集を即時に反映する設計にはしていない。

Actual Music response record types: investigation on 2026-10-15

At application revision f524b38, importing a supplied M4A through the macOS development app reproduced the generic warning that Music metadata could not be read. The audio file was imported using embedded metadata.

The production AppleScript successfully returned a list of file-track property records. Their descriptor type was cFlT, which the installed Music scripting dictionary defines as file track. Each element was a record descriptor (isRecordDescriptor == true) and its name property was readable. However, snapshot(from:) required descriptorType == typeAERecord (reco), so it rejected these valid records and returned nil. readSynchronously() converted that nil to the generic localized access failure. This failure occurs before per-file matching and does not mean the imported audio is corrupt or that no matching Music song exists.

A standalone diagnostic compiled unchanged copies of the production provider and its value/localization dependencies. It sent the exact production AppleScript to running Music, without modifying the library. The response failed the production snapshot reader. Converting its record-like elements to typeAERecord, then passing that same response to the unchanged reader, succeeded. Diagnostics logged types, counts, and acceptance booleans without publishing song metadata.

The existing tests create literal AppleScript records with Music terminology. Those ordinary records do not reproduce the descriptor type of Music's actual property response. A future fix should accept record-like descriptors (or normalize them to typeAERecord) and add regression coverage for cFlT typed records, while continuing to reject non-record list elements. Apple's isRecordDescriptor tests whether a descriptor is record-like; coerce(toDescriptorType:) can normalize its type.

This was a cause investigation, not an application fix. Successful decoding after normalization does not establish that the supplied copy matches a Music song under the separate path/sort-name/duration rules. No post-fix UI import or application build/test run was performed. The diagnosis is macOS-specific; the iPhone/iPad Music picker uses a different path.