192 lines
6.1 KiB
Go
192 lines
6.1 KiB
Go
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
|
|
}
|
|
}
|