Раскладка: сходимость папки сериала (второй сезон в ту же папку)
При подтверждённом матче база папки (имя+год) наследуется от живой папки-якоря того же (provider, provider_id) вместо печати заново из выхода LLM — так второй/последующий сезон ложится в ТУ ЖЕ папку, а не заводит рядом почти одинаковую. Отдельная сущность «тайтл» не вводится. - layout: Plan.FolderBase перекрывает базу в папке и именах файлов; TitleFolder разбирает dst_path в папку тайтла и базу (снятие тега). - store: LiveTitleFolders — dst_path живых ссылок того же матча. - worker: resolveFolderBase (живость якоря — по наличию папки на диске, os.Lstat, а не по статусу ссылки в БД) в linkPlan и в превью ревью (превью=применение); рассинхрон нескольких живых папок → review из linking (deferred→review в графе нет). Схема БД не менялась. Change series-folder-convergence заархивирован, требования влиты в openspec/specs/file-layout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"git.vakhrushev.me/av/jellybit/internal/logctx"
|
||||
@@ -68,7 +69,13 @@ type Plan struct {
|
||||
Title string
|
||||
Year int
|
||||
ProviderTag string // напр. "tmdbid-693134"; пусто — без тега
|
||||
Files []PlanFile
|
||||
// FolderBase — унаследованная от живого якоря база имени ("Название (Год)").
|
||||
// Пусто → база печатается из Title/Year (первая загрузка тайтла). Непусто →
|
||||
// перекрывает Title/Year и идёт и в папку, и в имена файлов (правило
|
||||
// сходимости папки, см. file-layout spec). Provider-тег добавляется отдельно
|
||||
// из ProviderTag.
|
||||
FolderBase string
|
||||
Files []PlanFile
|
||||
}
|
||||
|
||||
// Link — посчитанная пара источник → цель.
|
||||
@@ -136,7 +143,7 @@ func (l *Layouter) BuildLinks(p Plan) ([]Link, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
base, err := titleYear(p.Title, p.Year)
|
||||
base, err := planBase(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -209,6 +216,36 @@ func (l *Layouter) seriesDst(root, folder, base string, f *PlanFile) (string, Ki
|
||||
}
|
||||
}
|
||||
|
||||
// TitleFolder разбирает целевой путь dst живой ссылки в папку тайтла и её базу
|
||||
// имени для правила сходимости (см. file-layout spec). Возвращает абсолютный
|
||||
// путь папки тайтла (первый сегмент под корнем библиотеки типа t), её базу
|
||||
// (снят хвостовой provider-тег) и ok. ok=false, если dst не под корнем нужной
|
||||
// библиотеки или база пуста — вызывающий трактует как «нет якоря». Существование
|
||||
// папки на диске здесь НЕ проверяется (это делает worker через os.Lstat).
|
||||
func (l *Layouter) TitleFolder(t MediaType, dst string) (dir, base string, ok bool) {
|
||||
root, err := l.root(t)
|
||||
if err != nil {
|
||||
return "", "", false
|
||||
}
|
||||
dst = filepath.Clean(dst)
|
||||
if !underRoot(root, dst) {
|
||||
return "", "", false
|
||||
}
|
||||
rel, err := filepath.Rel(root, dst)
|
||||
if err != nil {
|
||||
return "", "", false
|
||||
}
|
||||
first, _, _ := strings.Cut(rel, string(filepath.Separator))
|
||||
if first == "" || first == "." {
|
||||
return "", "", false
|
||||
}
|
||||
base = folderBase(first)
|
||||
if base == "" {
|
||||
return "", "", false
|
||||
}
|
||||
return filepath.Join(root, first), base, true
|
||||
}
|
||||
|
||||
// LinkStatus — исход создания одной ссылки.
|
||||
type LinkStatus string
|
||||
|
||||
|
||||
Reference in New Issue
Block a user