165 lines
4.4 KiB
Go
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
|
|
}
|