feat(app): add internal components batch 3

This commit is contained in:
吴璨
2026-09-30 10:20:01 +08:00
parent fce3d91aa0
commit 713098f5b7
10 changed files with 3110 additions and 0 deletions
+191
View File
@@ -0,0 +1,191 @@
package app
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"os"
"path/filepath"
"sort"
"time"
)
var embyPosterNames = []string{"poster.jpg", "poster.jpeg", "poster.png", "poster.webp", "poster.gif", "folder.jpg", "folder.png"}
func embyArtworkHash(body []byte) string {
digest := sha256.Sum256(body)
return hex.EncodeToString(digest[:])
}
func readEmbyPosterDirectory(settings embySyncSettings, drama Drama, folder string) (string, embyFolderManifest, error) {
var manifest embyFolderManifest
root, err := filepath.EvalSymlinks(settings.OutputDir)
if err != nil {
return "", manifest, err
}
if _, err = embyDirectoryParent(root, folder, false); err != nil {
return "", manifest, err
}
directory := filepath.Join(root, filepath.FromSlash(folder))
info, err := os.Lstat(directory)
if err != nil {
return "", manifest, err
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return "", manifest, errors.New("Emby 海报目录不是普通文件夹")
}
manifestPath := filepath.Join(directory, ".juku-emby.json")
info, err = os.Lstat(manifestPath)
if err != nil {
return "", manifest, err
}
if !info.Mode().IsRegular() || info.Size() > 8<<20 {
return "", manifest, errors.New("Emby 目录标识不是有效的普通文件")
}
if err = readEmbyJSON(manifestPath, &manifest, 8<<20); err != nil {
return "", manifest, err
}
if manifest.Version != 1 || manifest.DramaID != drama.ID {
return "", manifest, errors.New("Emby 海报目录所属剧集不匹配")
}
return directory, manifest, nil
}
func inspectEmbyArtwork(directory string, manifest embyFolderManifest) (managed, preserved string, err error) {
for _, name := range embyPosterNames {
path := filepath.Join(directory, name)
info, err := os.Lstat(path)
if errors.Is(err, os.ErrNotExist) {
continue
}
if err != nil {
return "", "", err
}
if !info.Mode().IsRegular() {
return "", "", errors.New("Emby 海报不是普通文件,已保留原内容")
}
if name != manifest.PosterFile || manifest.PosterHash == "" || info.Size() > maxCoverBytes {
return "", name, nil
}
body, err := os.ReadFile(path)
if err != nil {
return "", "", err
}
if embyArtworkHash(body) != manifest.PosterHash {
return "", name, nil
}
managed = name
}
return managed, "", nil
}
func (app *UIApp) syncLocalEmbyPoster(ctx context.Context, settings embySyncSettings, drama Drama, folder, remote string) (string, bool, error) {
directory, manifest, err := readEmbyPosterDirectory(settings, drama, folder)
if err != nil {
return "", false, err
}
managed, preserved, err := inspectEmbyArtwork(directory, manifest)
if err != nil || preserved != "" {
return preserved, false, err
}
sourceHash := embyPosterSourceHash(remote)
if managed != "" && manifest.PosterSource == sourceHash {
return managed, false, nil
}
source := sourceHongguo
data, err := app.loadCoverImage(context.WithValue(ctx, coverSourceKey{}, source), remote, nil)
if err != nil {
return "", false, err
}
extension := map[string]string{"image/jpeg": ".jpg", "image/png": ".png", "image/webp": ".webp", "image/gif": ".gif"}[imageContentType(data)]
if extension == "" {
return "", false, errors.New("海报图片格式不受支持")
}
if err = ctx.Err(); err != nil {
return "", false, err
}
directory, manifest, err = readEmbyPosterDirectory(settings, drama, folder)
if err != nil {
return "", false, err
}
managed, preserved, err = inspectEmbyArtwork(directory, manifest)
if err != nil || preserved != "" {
return preserved, false, err
}
name := "poster" + extension
changed, err := writeEmbyFile(filepath.Join(directory, name), data, 0644)
if err != nil {
return "", false, err
}
previousHash := manifest.PosterHash
manifest.PosterFile, manifest.PosterHash, manifest.PosterSource = name, embyArtworkHash(data), sourceHash
if err = writeEmbyJSON(filepath.Join(directory, ".juku-emby.json"), manifest, 0600); err != nil {
return name, changed, err
}
if managed != "" && managed != name {
old := filepath.Join(directory, managed)
if info, err := os.Lstat(old); err == nil && info.Mode().IsRegular() && info.Size() <= maxCoverBytes {
if body, err := os.ReadFile(old); err == nil && embyArtworkHash(body) == previousHash {
_ = os.Remove(old)
}
}
}
return name, changed, nil
}
func (manager *embySyncManager) synchronizeLocalPosters(ctx context.Context, document *embySyncDocument, targets []Drama) {
interval := time.Duration(document.Settings.IntervalMinutes) * time.Minute
var due []Drama
for _, drama := range targets {
entry := document.Entries[drama.ID]
poster := entry.Poster
if entry.Folder == "" {
continue
}
if next := embyLocalPosterDue(poster, interval); !next.IsZero() && !next.After(time.Now()) {
due = append(due, drama)
}
}
sort.Slice(due, func(i, j int) bool {
left, right := document.Entries[due[i].ID].Poster.LocalCheckedAt, document.Entries[due[j].ID].Poster.LocalCheckedAt
if left.Equal(right) {
return due[i].ID < due[j].ID
}
return left.Before(right)
})
if len(due) > 20 {
due = due[:20]
}
for _, drama := range due {
if ctx.Err() != nil {
return
}
entry := document.Entries[drama.ID]
poster := entry.Poster
requestCtx, cancel := context.WithTimeout(ctx, 45*time.Second)
name, changed, err := manager.app.syncLocalEmbyPoster(requestCtx, document.Settings, drama, entry.Folder, poster.SourceURL)
cancel()
document.PosterChecked++
poster.LocalCheckedAt = time.Now()
poster.LocalError = manager.safeError(err, document.Settings.APIKey)
if err != nil {
poster.LocalRetryAt = time.Now().Add(5 * time.Minute)
} else {
poster.LocalFile, poster.LocalSource, poster.LocalRetryAt = name, embyPosterSourceHash(poster.SourceURL), time.Time{}
}
if changed {
document.WrittenFiles++
document.RefreshPending = document.Settings.ServerURL != ""
}
if document.Settings.ServerURL == "" {
poster.Status, poster.Error, poster.CheckedAt, poster.RetryAt = "local", poster.LocalError, poster.LocalCheckedAt, poster.LocalRetryAt
if err != nil {
poster.Status = "failed"
}
}
entry.Poster = poster
document.Entries[drama.ID] = entry
}
}