Files
djk/internal/app/emby_sync_files.go
T

165 lines
4.4 KiB
Go

package app
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
type embyFolderManifest struct {
Version int `json:"version"`
DramaID string `json:"dramaId"`
Chapters []embyChapter `json:"chapters"`
}
func validEmbyFolder(folder string) bool {
return folder == "" || len(folder) <= 240 && folder != "." && folder != ".." && !strings.ContainsAny(folder, "/\\\x00\r\n") && filepath.Base(folder) == folder
}
func ensureEmbyDirectory(path string) error {
info, err := os.Lstat(path)
if errors.Is(err, os.ErrNotExist) {
return os.Mkdir(path, 0755)
}
if err != nil {
return err
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return errors.New("Emby 生成目录不是普通文件夹,已停止写入")
}
return nil
}
func syncEmbyDramaFiles(ctx context.Context, settings embySyncSettings, drama Drama, chapters []Chapter, folder string, key []byte, owner string) (string, int, int, error) {
if !validEmbyFolder(folder) {
return folder, 0, 0, errors.New("Emby 剧集目录无效")
}
if folder == "" {
folder = embyFolderName(drama)
}
if err := os.MkdirAll(settings.OutputDir, 0755); err != nil {
return folder, 0, 0, err
}
root, err := filepath.EvalSymlinks(settings.OutputDir)
if err != nil {
return folder, 0, 0, err
}
target := filepath.Join(root, folder)
work := target
var previous []embyChapter
staged := false
info, err := os.Lstat(target)
if errors.Is(err, os.ErrNotExist) {
work, err = os.MkdirTemp(root, ".juku-emby-")
if err != nil {
return folder, 0, 0, err
}
staged = true
defer os.RemoveAll(work)
if err = os.Chmod(work, 0755); err != nil {
return folder, 0, 0, err
}
} else {
if err != nil {
return folder, 0, 0, err
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return folder, 0, 0, errors.New("Emby 剧集目录不是普通文件夹")
}
manifestPath := filepath.Join(work, ".juku-emby.json")
info, err := os.Lstat(manifestPath)
if err != nil || !info.Mode().IsRegular() {
return folder, 0, 0, errors.New("同名目录不属于自动同步,原内容已保留;请使用单独的 Emby 输出目录")
}
var manifest embyFolderManifest
if err = readEmbyJSON(manifestPath, &manifest, 8<<20); err != nil {
return folder, 0, 0, err
}
if manifest.Version != 1 || manifest.DramaID != drama.ID {
return folder, 0, 0, errors.New("Emby 目录所属剧集不匹配,原内容已保留")
}
previous = manifest.Chapters
}
records, err := embyChapterRecords(drama.ID, chapters, previous)
if err != nil {
return folder, 0, 0, err
}
if err = ensureEmbyDirectory(filepath.Join(work, "Season 01")); err != nil {
return folder, 0, 0, err
}
written := 0
err = writeEmbyFiles(drama, records, settings.BaseURL, key, owner, func(name string, body []byte) error {
if err := ctx.Err(); err != nil {
return err
}
changed, err := writeEmbyFile(filepath.Join(work, filepath.FromSlash(name)), body, 0644)
if changed {
written++
}
return err
})
if err == nil {
body, marshalErr := json.MarshalIndent(embyFolderManifest{Version: 1, DramaID: drama.ID, Chapters: records}, "", " ")
err = marshalErr
if err == nil {
_, err = writeEmbyFile(filepath.Join(work, ".juku-emby.json"), append(body, '\n'), 0600)
}
}
if err == nil {
err = ctx.Err()
}
if staged {
if err == nil {
err = os.Rename(work, target)
}
if err != nil {
written = 0
}
}
return folder, written, len(records), err
}
func writeEmbyFile(path string, body []byte, mode os.FileMode) (bool, error) {
info, err := os.Lstat(path)
if err == nil {
if !info.Mode().IsRegular() || info.Size() > 8<<20 {
return false, errors.New("Emby 目标文件不是普通文件或文件过大,已停止写入")
}
old, err := os.ReadFile(path)
if err != nil {
return false, err
}
if bytes.Equal(old, body) {
return false, nil
}
} else if !errors.Is(err, os.ErrNotExist) {
return false, err
}
file, err := os.CreateTemp(filepath.Dir(path), ".juku-emby-write-")
if err != nil {
return false, err
}
defer os.Remove(file.Name())
if err = file.Chmod(mode); err == nil {
_, err = file.Write(body)
}
if err == nil {
err = file.Sync()
}
if closeErr := file.Close(); err == nil {
err = closeErr
}
if err != nil {
return false, err
}
if err = os.Rename(file.Name(), path); err != nil {
return false, fmt.Errorf("Emby 文件替换失败:%w", err)
}
return true, nil
}