Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ce01b67
メタデータプラグインの応答から版を読む
AkashiSN Oct 18, 2026
4badb5e
サイドカーに判定の材料と版・時刻を残す
AkashiSN Oct 18, 2026
279696a
ジョブ種別 inbox_rejudge を足す
AkashiSN Oct 18, 2026
818d602
YouTube のテストの土台を common へ移す
AkashiSN Oct 18, 2026
3ff461a
Inbox の件を判定し直す本体
AkashiSN Oct 18, 2026
fdcb56a
再判定の移動を実際の置き場所で決める
AkashiSN Oct 18, 2026
ad679fb
再判定のジョブを登録する
AkashiSN Oct 18, 2026
921fa07
再判定の API を足す
AkashiSN Oct 18, 2026
ab363bb
Inbox の再判定ボタンと判定の版の表示
AkashiSN Oct 18, 2026
48220c0
再判定の仕様と判断(D-114)
AkashiSN Oct 18, 2026
95fcb08
SPEC の Inbox 画面の節に再判定を足す
AkashiSN Oct 18, 2026
62ec911
サイドカーの読み書きを ytdl と再判定で直列にする
AkashiSN Oct 18, 2026
fd184e1
再判定が途中で止まっても走査を投入し、作った親を fsync する
AkashiSN Oct 18, 2026
262a61f
tag_changes_for の重複した説明を 1 つにする
AkashiSN Oct 18, 2026
925b011
GET /api/inbox に YouTube 機能の有効を載せる
AkashiSN Oct 18, 2026
ed624b3
再判定の着地を一覧の変化で決め、実行中は他の操作を止める
AkashiSN Oct 18, 2026
748dda0
再判定で移動の残りの項を消し、書く前に件を確かめ直す
AkashiSN Oct 18, 2026
d4b3a01
再判定の間は件の承認・却下・再開を 409 rejudging で断る
AkashiSN Oct 18, 2026
a731922
再判定の二度押しを止め、着地の控えを件ごとに持つ
AkashiSN Oct 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions db/migrations/0005_inbox_rejudge.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
-- 再判定(D-114)。jobs は CHECK の列挙に inbox_rejudge を足すため作り直す
-- (foreign_keys=OFF で適用。migrations.rs の FOREIGN_KEYS_OFF)

CREATE TABLE jobs_new (
id INTEGER PRIMARY KEY,
type TEXT NOT NULL
CHECK (type IN ('scan','rip','verify','rg','transcode','tagwrite','rename',
'normalize','thumbnail','flaccheck','inbox','ytdl','gc','backup','hirescheck',
'playlist_sync','source_hash','device_scan','device_sync','device_verify',
'rgwrite','inbox_rejudge')),
dedup_key TEXT,
payload TEXT NOT NULL CHECK (json_valid(payload)),
state TEXT NOT NULL DEFAULT 'queued'
CHECK (state IN ('queued','running','done','failed','cancelled')),
edit_batch_id INTEGER REFERENCES edit_batches(id) ON DELETE SET NULL,
priority INTEGER NOT NULL DEFAULT 0,
run_after INTEGER,
progress REAL CHECK (progress IS NULL OR (progress >= 0.0 AND progress <= 1.0)),
total INTEGER CHECK (total IS NULL OR total >= 0),
done INTEGER CHECK (done IS NULL OR done >= 0),
attempts INTEGER NOT NULL DEFAULT 0 CHECK (attempts >= 0),
max_attempts INTEGER NOT NULL DEFAULT 5 CHECK (max_attempts >= 1),
last_error TEXT,
cancel_requested_at INTEGER,
created_at INTEGER NOT NULL,
started_at INTEGER,
finished_at INTEGER,
note TEXT
) STRICT;

INSERT INTO jobs_new SELECT * FROM jobs;
DROP TABLE jobs;
ALTER TABLE jobs_new RENAME TO jobs;

CREATE UNIQUE INDEX idx_jobs_dedup_active ON jobs(dedup_key)
WHERE dedup_key IS NOT NULL AND state IN ('queued','running');
CREATE INDEX idx_jobs_queue ON jobs(state, run_after, priority DESC, created_at)
WHERE state IN ('queued','running');
CREATE INDEX idx_jobs_finished ON jobs(finished_at) WHERE finished_at IS NOT NULL;
CREATE INDEX idx_jobs_batch ON jobs(edit_batch_id) WHERE edit_batch_id IS NOT NULL;
70 changes: 70 additions & 0 deletions docs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4876,3 +4876,73 @@ mp3 13 曲が「書き込み結果が意図と一致しない(この形式で
**却下**: 候補を下書きの初期値として入れる(確認せずに承認すると誤判定が書かれる)。CD の取り込みの画面にも同じ欄を置く
(CD も Inbox の承認を通るので二重になる)。部分一致(キーの途中)で絞る(`TRACK` で `TOTALTRACKS` まで出て、狙いの候補が
埋もれる)。

---

## D-114 Inbox の YouTube 取り込みを再判定し、判定したプラグインの版を記録する

**背景**(2026-10-18): YouTube の曲名・アーティストはメタデータプラグイン(`spindle-ytmusic-meta`)がタイトルから決める。
ルールに穴があると、どれにも合わず `Inbox/youtube/_unmatched/<channel>/` に動画タイトルのまま置かれるか、判定はされても
値が違う(全角 `#` で ARTIST が `花譜 #171` になる等)。プラグインを直しても取り込み済みの曲は直らず、補正フォームで
人が埋め直すしかない。却下して取り込み直そうにも、同じ `SOURCE_URL` が Inbox に残るのでダウンローダが「取り込み済み」で止まる。
プラグインはホストからマウントしたバイナリを判定のたびに起動するので、`cp` で置き換えれば spindle を再起動せずに新しい
ルールが効く。あとはその判定をやり直す経路と、どの版で判定したかが画面から分かることが要る。

**決定**:

1. **件ごとの「再判定」ボタン**(`POST /api/inbox/:id/rejudge` → 202 + `job_id`)。対象は state が `pending` で、サイドカーに
`source = youtube` の項を持つ曲が 1 本以上ある件。件の YouTube 由来の全曲を対象にし、未判定が無くても押せる(誤読の救済)。
出どころの無い曲(人が置いたもの)は触らない。承認済み・配置中・配置済み・失敗・却下の件にはボタンを出さない
2. **ジョブ `inbox_rejudge`**(並列 1、dedup `inbox_rejudge:<item_id>`、`max_attempts` 1)。曲ごとにプラグインを起動し
yt-dlp も呼ぶのでリクエストの中では済ませない。人が直す前に繰り返しても意味がないので再試行しない。開始時と曲ごとに件が
`pending` かを確かめ、外れたら止める。終わったら `inbox` の走査を投入する
3. **版は Response の任意欄 `plugin`**(`{ name, version }`)で受ける。未知の欄はプラグインが無視する規則なので後方互換。
無ければ「不明」。形が違えば捨てるだけで応答は不正にしない。サイドカーの曲の項に `plugin_version` と `judged_at` を
ダウンローダの初回判定でも再判定でも残す
4. **動画タイトルの出どころ**: これからの取り込みは、プラグインへ渡した `Item` をサイドカーの `item` に残す。既存の曲
(`item` が無い)は再判定のときにダウンローダの手順 1 と同じ引数で yt-dlp を呼んで取り直し、サイドカーに書き足す。
取れなければその曲だけ失敗(`rejudge_error`)とし、タグは触らず他の曲は続ける
5. **1 曲の写像**は初回の取り込みと同じ関数を共有する。購読由来(`subscription_id` があり購読が残る)は初回と同じく
albumartist / album / category を購読の値で上書きし、`ok: false`(skip を含む)でも合成 Track を書く。購読由来でない曲は
`ok` → TITLE / ARTIST / ALBUM / ALBUMARTIST / DATE / 追加タグを書く。`ok: false`(unmatched / unknown_channel / 未知)→
TITLE を動画タイトルに戻しプラグイン由来のタグを消す。`skip` → 判定を記録するだけでタグは触らない。プラグインの故障 → その曲は
失敗でタグもサイドカーも触らない。購読が消えた購読由来の曲で `ok` でないときは今のタグを保つ。追加タグは書いたキーを
サイドカーの `plugin_tags` に残し、今回の応答に無いものを消す(記録の無い既存の曲は消さず上書きだけ。タグの慣習を
spindle に持ち込まない。D-69)。ファイル名も初回と同じ規則(`<YYYYMMDD> <title> [<id>].opus`)で付け直し、TRACKNUMBER /
PICTURE / SOURCE_URL は触らない。件の `category` は判定の値で書き換え(無ければ消す)、語彙に無ければ足す
6. **移動の規則**: 判定が ok か否かと、**曲が今いるディレクトリ**(`youtube/_unmatched/` の下か)が食い違うときだけ初回の規則
(`inbox_dir`)の宛先へ移す。unmatched → ok は本来の件(`youtube/花譜/花譜のお歌/`)へ、ok → unmatched は
`youtube/_unmatched/<channel>/` へ。ok のまま宛先だけ変わる場合と、購読由来の曲(購読が消えていても)は移さず、件の中で
タグだけ直す。移動先に件が無いか `pending` のときだけ移し、それ以外なら移さずタグだけ直して「移せなかった(移動先が
<state>)」を結果に出す。移動先のサイドカーに項を先に書いてから元を消し、元の項を消す
7. **冪等性**: 曲ごとに「タグを tmp に写して書く → `RENAME_NOREPLACE` で新しい名前へ → 元を消す → サイドカー」。新しい名前に
既にあるファイルは、その `SOURCE_URL` が同じなら前回の途中の自分の成果物なので今回の書いたもので置き換える。
サイドカーの項のファイルが無いときは、項を持たないファイルのうち `SOURCE_URL` が項の `url` と同じものを前回の途中で名前を
変えた後のファイルとして拾う。それも無い項は消す(宛先の項は先に書いてあるので、元の項を消す前に落ちた残りは捨ててよい)
8. **下書きは捨てる**: 補正の下書き(`inbox_items.draft`)は承認後に再開した件などに残り、提案でタグより勝つ。ジョブの
最初に消す(ユーザの判断)。押す前に、保存済みの下書きがある件は「保存済みの補正は消え、判定結果で置き換わります」、
画面に未送信の編集がある件は「未保存の補正は消えます」と確認し、どちらも無ければ確認しない
9. **画面**: 件の見出しに「再判定」(実行中は無効で `再判定中 done/total`)と判定の版(揃っていれば
`判定: spindle-ytmusic-meta v…`、混ざれば `判定: v… ほか`、記録が無ければ `判定: 版不明`)。③ の判定列の詳細に版と
判定時刻と失敗の理由。失敗した曲があれば件の警告に「再判定できなかった曲が N 曲」。終わったら一覧を取り直し、
件の全曲が別の件へ移ったらそれを開き、一部なら「N 曲を <移動先> へ移した」と知らせる。API は `[ytmusic].enabled` で
なければ 404、件が無ければ 404、`pending` でない・YouTube 由来の曲が無ければ 409 `state`、同じ件のジョブが
あれば 409 `duplicate`、Inbox が使えなければ 503 `inbox_unavailable`(他の Inbox の API と同じ)
10. **再判定の間は件を動かさない**: 件の再判定ジョブが queued / running の間、承認・却下・再開は 409 `rejudging`。ジョブも
判定の応答を待った後、書く前に件が `pending` かを確かめ直す。曲ごとの確認だけでは、プラグインを待つ間の承認を見逃して
承認済みの件のファイルを書き換え、走査が承認を pending に戻してしまうため

**理由**: 版を `--version` の別起動で取る案は、プロトコル外のフラグに依存し、答えたバイナリと版が食い違いうる。Response の
欄ならその応答を返した実体の版そのものが取れる。動画タイトルは ok の曲の TITLE が解析後の値で元に戻せないため、
プラグインに渡した `Item` を残すのが確実で、既存の曲だけ取り直せばよい。移動をサイドカーの前回の判定ではなく実際の
ディレクトリで決めるのは、移せなかった曲(移動先が承認済み等)の判定だけが先に書き換わっても、次の再判定でその曲を
移せるようにするため。移動先を `pending` に限るのは、承認済みの下書きが件のファイル集合を前提にし、却下・破棄待ちの件に
足すと走査が破棄を解いてしまうため(D-90)。購読由来を移さないのは、その曲の置き場所が購読の設定で決まりタイトル解析では
変わらないため。新しい名前に同じ `SOURCE_URL` のファイルが残るのは前回の中断の産物なので置き換えて収束させる(ダウンローダの
手順 8 は「違えば Fatal」を保つが、そちらは別の動画との衝突を見分けるため)。再試行しないのは、失敗の多くがプラグインや
yt-dlp の状態に依り、人がルールを直してもう一度押すのが正しい復帰だから。

**却下**: 承認済み・配置済みの件の再判定(Library に入った曲は通常の一括編集で直す)。ok の曲を別のアーティスト・アルバムの件へ
移すこと(ok の albumartist / album はチャンネル定義か購読で決まる)。再判定前後の差分表示と確認ダイアログ(確認は失うものが
ある下書きのときだけ)。Inbox 全体の一括再判定(件ごとのボタンだけ)。動画タイトルの取り直しの常時実行(YouTube 側の変更は
追わない)。
Loading
Loading