Files
seanime-docker/seanime-2.9.10/internal/library/summary/scan_summary.go
2025-09-20 14:08:38 +01:00

364 lines
10 KiB
Go

package summary
import (
"fmt"
"seanime/internal/api/anilist"
"seanime/internal/library/anime"
"time"
"github.com/google/uuid"
)
const (
LogFileNotMatched LogType = iota
LogComparison
LogSuccessfullyMatched
LogFailedMatch
LogMatchValidated
LogUnmatched
LogMetadataMediaTreeFetched
LogMetadataMediaTreeFetchFailed
LogMetadataEpisodeNormalized
LogMetadataEpisodeNormalizationFailed
LogMetadataEpisodeZero
LogMetadataNC
LogMetadataSpecial
LogMetadataMain
LogMetadataHydrated
LogPanic
LogDebug
)
type (
LogType int
ScanSummaryLogger struct {
Logs []*ScanSummaryLog
LocalFiles []*anime.LocalFile
AllMedia []*anime.NormalizedMedia
AnimeCollection *anilist.AnimeCollectionWithRelations
}
ScanSummaryLog struct { // Holds a log entry. The log entry will then be used to generate a ScanSummary.
ID string `json:"id"`
FilePath string `json:"filePath"`
Level string `json:"level"`
Message string `json:"message"`
}
ScanSummary struct {
ID string `json:"id"`
Groups []*ScanSummaryGroup `json:"groups"`
UnmatchedFiles []*ScanSummaryFile `json:"unmatchedFiles"`
}
ScanSummaryFile struct {
ID string `json:"id"`
LocalFile *anime.LocalFile `json:"localFile"`
Logs []*ScanSummaryLog `json:"logs"`
}
ScanSummaryGroup struct {
ID string `json:"id"`
Files []*ScanSummaryFile `json:"files"`
MediaId int `json:"mediaId"`
MediaTitle string `json:"mediaTitle"`
MediaImage string `json:"mediaImage"`
MediaIsInCollection bool `json:"mediaIsInCollection"` // Whether the media is in the user's AniList collection
}
ScanSummaryItem struct { // Database item
CreatedAt time.Time `json:"createdAt"`
ScanSummary *ScanSummary `json:"scanSummary"`
}
)
func NewScanSummaryLogger() *ScanSummaryLogger {
return &ScanSummaryLogger{
Logs: make([]*ScanSummaryLog, 0),
}
}
// HydrateData will hydrate the data needed to generate the summary.
func (l *ScanSummaryLogger) HydrateData(lfs []*anime.LocalFile, media []*anime.NormalizedMedia, animeCollection *anilist.AnimeCollectionWithRelations) {
l.LocalFiles = lfs
l.AllMedia = media
l.AnimeCollection = animeCollection
}
func (l *ScanSummaryLogger) GenerateSummary() *ScanSummary {
if l == nil || l.LocalFiles == nil || l.AllMedia == nil || l.AnimeCollection == nil {
return nil
}
summary := &ScanSummary{
ID: uuid.NewString(),
Groups: make([]*ScanSummaryGroup, 0),
UnmatchedFiles: make([]*ScanSummaryFile, 0),
}
groupsMap := make(map[int][]*ScanSummaryFile)
// Generate summary files
for _, lf := range l.LocalFiles {
if lf.MediaId == 0 {
summary.UnmatchedFiles = append(summary.UnmatchedFiles, &ScanSummaryFile{
ID: uuid.NewString(),
LocalFile: lf,
Logs: l.getFileLogs(lf),
})
continue
}
summaryFile := &ScanSummaryFile{
ID: uuid.NewString(),
LocalFile: lf,
Logs: l.getFileLogs(lf),
}
//summary.Files = append(summary.Files, summaryFile)
// Add to group
if _, ok := groupsMap[lf.MediaId]; !ok {
groupsMap[lf.MediaId] = make([]*ScanSummaryFile, 0)
groupsMap[lf.MediaId] = append(groupsMap[lf.MediaId], summaryFile)
} else {
groupsMap[lf.MediaId] = append(groupsMap[lf.MediaId], summaryFile)
}
}
// Generate summary groups
for mediaId, files := range groupsMap {
mediaTitle := ""
mediaImage := ""
mediaIsInCollection := false
for _, m := range l.AllMedia {
if m.ID == mediaId {
mediaTitle = m.GetPreferredTitle()
mediaImage = ""
if m.GetCoverImage() != nil && m.GetCoverImage().GetLarge() != nil {
mediaImage = *m.GetCoverImage().GetLarge()
}
break
}
}
if _, found := l.AnimeCollection.GetListEntryFromMediaId(mediaId); found {
mediaIsInCollection = true
}
summary.Groups = append(summary.Groups, &ScanSummaryGroup{
ID: uuid.NewString(),
Files: files,
MediaId: mediaId,
MediaTitle: mediaTitle,
MediaImage: mediaImage,
MediaIsInCollection: mediaIsInCollection,
})
}
return summary
}
func (l *ScanSummaryLogger) LogComparison(lf *anime.LocalFile, algo string, bestTitle string, ratingType string, rating string) {
if l == nil {
return
}
msg := fmt.Sprintf("Comparison using %s. Best title: \"%s\". %s: %s", algo, bestTitle, ratingType, rating)
l.logType(LogComparison, lf, msg)
}
func (l *ScanSummaryLogger) LogSuccessfullyMatched(lf *anime.LocalFile, mediaId int) {
if l == nil {
return
}
msg := fmt.Sprintf("Successfully matched to media %d", mediaId)
l.logType(LogSuccessfullyMatched, lf, msg)
}
func (l *ScanSummaryLogger) LogPanic(lf *anime.LocalFile, stackTrace string) {
if l == nil {
return
}
//msg := fmt.Sprintf("Panic occurred, please report this issue on the GitHub repository with the stack trace printed in the terminal")
l.logType(LogPanic, lf, "PANIC! "+stackTrace)
}
func (l *ScanSummaryLogger) LogFailedMatch(lf *anime.LocalFile, reason string) {
if l == nil {
return
}
msg := fmt.Sprintf("Failed to match: %s", reason)
l.logType(LogFailedMatch, lf, msg)
}
func (l *ScanSummaryLogger) LogMatchValidated(lf *anime.LocalFile, mediaId int) {
if l == nil {
return
}
msg := fmt.Sprintf("Match validated for media %d", mediaId)
l.logType(LogMatchValidated, lf, msg)
}
func (l *ScanSummaryLogger) LogUnmatched(lf *anime.LocalFile, reason string) {
if l == nil {
return
}
msg := fmt.Sprintf("Unmatched: %s", reason)
l.logType(LogUnmatched, lf, msg)
}
func (l *ScanSummaryLogger) LogFileNotMatched(lf *anime.LocalFile, reason string) {
if l == nil {
return
}
msg := fmt.Sprintf("Not matched: %s", reason)
l.logType(LogFileNotMatched, lf, msg)
}
func (l *ScanSummaryLogger) LogMetadataMediaTreeFetched(lf *anime.LocalFile, ms int64, branches int) {
if l == nil {
return
}
msg := fmt.Sprintf("Media tree fetched in %dms. Branches: %d", ms, branches)
l.logType(LogMetadataMediaTreeFetched, lf, msg)
}
func (l *ScanSummaryLogger) LogMetadataMediaTreeFetchFailed(lf *anime.LocalFile, err error, ms int64) {
if l == nil {
return
}
msg := fmt.Sprintf("Could not fetch media tree: %s. Took %dms", err.Error(), ms)
l.logType(LogMetadataMediaTreeFetchFailed, lf, msg)
}
func (l *ScanSummaryLogger) LogMetadataEpisodeNormalized(lf *anime.LocalFile, mediaId int, episode int, newEpisode int, newMediaId int, aniDBEpisode string) {
if l == nil {
return
}
msg := fmt.Sprintf("Episode %d normalized to %d. New media ID: %d. AniDB episode: %s", episode, newEpisode, newMediaId, aniDBEpisode)
l.logType(LogMetadataEpisodeNormalized, lf, msg)
}
func (l *ScanSummaryLogger) LogMetadataEpisodeNormalizationFailed(lf *anime.LocalFile, err error, episode int, aniDBEpisode string) {
if l == nil {
return
}
msg := fmt.Sprintf("Episode normalization failed. Reason \"%s\". Episode %d. AniDB episode %s", err.Error(), episode, aniDBEpisode)
l.logType(LogMetadataEpisodeNormalizationFailed, lf, msg)
}
func (l *ScanSummaryLogger) LogMetadataNC(lf *anime.LocalFile) {
if l == nil {
return
}
msg := fmt.Sprintf("Marked as NC file")
l.logType(LogMetadataNC, lf, msg)
}
func (l *ScanSummaryLogger) LogMetadataSpecial(lf *anime.LocalFile, episode int, aniDBEpisode string) {
if l == nil {
return
}
msg := fmt.Sprintf("Marked as Special episode. Episode %d. AniDB episode: %s", episode, aniDBEpisode)
l.logType(LogMetadataSpecial, lf, msg)
}
func (l *ScanSummaryLogger) LogMetadataMain(lf *anime.LocalFile, episode int, aniDBEpisode string) {
if l == nil {
return
}
msg := fmt.Sprintf("Marked as main episode. Episode %d. AniDB episode: %s", episode, aniDBEpisode)
l.logType(LogMetadataMain, lf, msg)
}
func (l *ScanSummaryLogger) LogMetadataEpisodeZero(lf *anime.LocalFile, episode int, aniDBEpisode string) {
if l == nil {
return
}
msg := fmt.Sprintf("Marked as main episode. Episode %d. AniDB episode set to %s assuming AniDB does not include episode 0 in the episode count.", episode, aniDBEpisode)
l.logType(LogMetadataEpisodeZero, lf, msg)
}
func (l *ScanSummaryLogger) LogMetadataHydrated(lf *anime.LocalFile, mediaId int) {
if l == nil {
return
}
msg := fmt.Sprintf("Metadata hydrated for media %d", mediaId)
l.logType(LogMetadataHydrated, lf, msg)
}
func (l *ScanSummaryLogger) LogDebug(lf *anime.LocalFile, message string) {
if l == nil {
return
}
l.log(lf, "info", message)
}
func (l *ScanSummaryLogger) logType(logType LogType, lf *anime.LocalFile, message string) {
if l == nil {
return
}
switch logType {
case LogComparison:
l.log(lf, "info", message)
case LogSuccessfullyMatched:
l.log(lf, "info", message)
case LogFailedMatch:
l.log(lf, "warning", message)
case LogMatchValidated:
l.log(lf, "info", message)
case LogUnmatched:
l.log(lf, "warning", message)
case LogMetadataMediaTreeFetched:
l.log(lf, "info", message)
case LogMetadataMediaTreeFetchFailed:
l.log(lf, "error", message)
case LogMetadataEpisodeNormalized:
l.log(lf, "info", message)
case LogMetadataEpisodeNormalizationFailed:
l.log(lf, "error", message)
case LogMetadataHydrated:
l.log(lf, "info", message)
case LogMetadataNC:
l.log(lf, "info", message)
case LogMetadataSpecial:
l.log(lf, "info", message)
case LogMetadataMain:
l.log(lf, "info", message)
case LogMetadataEpisodeZero:
l.log(lf, "warning", message)
case LogFileNotMatched:
l.log(lf, "warning", message)
case LogPanic:
l.log(lf, "error", message)
case LogDebug:
l.log(lf, "info", message)
}
}
func (l *ScanSummaryLogger) log(lf *anime.LocalFile, level string, message string) {
if l == nil {
return
}
l.Logs = append(l.Logs, &ScanSummaryLog{
ID: uuid.NewString(),
FilePath: lf.Path,
Level: level,
Message: message,
})
}
func (l *ScanSummaryLogger) getFileLogs(lf *anime.LocalFile) []*ScanSummaryLog {
logs := make([]*ScanSummaryLog, 0)
if l == nil {
return logs
}
for _, log := range l.Logs {
if lf.HasSamePath(log.FilePath) {
logs = append(logs, log)
}
}
return logs
}