feat(app): add internal components batch 3
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user