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

198 lines
5.8 KiB
Go

package report
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
)
type ClickLog struct {
Timestamp time.Time `json:"timestamp"`
Element string `json:"element"`
PageURL string `json:"pageUrl"`
Text *string `json:"text"`
ClassName *string `json:"className"`
}
type NetworkLog struct {
Type string `json:"type"`
Method string `json:"method"`
URL string `json:"url"`
PageURL string `json:"pageUrl"`
Status int `json:"status"`
Duration int `json:"duration"`
DataPreview string `json:"dataPreview"`
Body string `json:"body"`
Timestamp time.Time `json:"timestamp"`
}
type ReactQueryLog struct {
Type string `json:"type"`
PageURL string `json:"pageUrl"`
Status string `json:"status"`
Hash string `json:"hash"`
Error interface{} `json:"error"`
DataPreview string `json:"dataPreview"`
DataType string `json:"dataType"`
Timestamp time.Time `json:"timestamp"`
}
type ConsoleLog struct {
Type string `json:"type"`
Content string `json:"content"`
PageURL string `json:"pageUrl"`
Timestamp time.Time `json:"timestamp"`
}
type UnlockedLocalFile struct {
Path string `json:"path"`
MediaId int `json:"mediaId"`
}
type IssueReport struct {
CreatedAt time.Time `json:"createdAt"`
UserAgent string `json:"userAgent"`
AppVersion string `json:"appVersion"`
OS string `json:"os"`
Arch string `json:"arch"`
ClickLogs []*ClickLog `json:"clickLogs,omitempty"`
NetworkLogs []*NetworkLog `json:"networkLogs,omitempty"`
ReactQueryLogs []*ReactQueryLog `json:"reactQueryLogs,omitempty"`
ConsoleLogs []*ConsoleLog `json:"consoleLogs,omitempty"`
UnlockedLocalFiles []*UnlockedLocalFile `json:"unlockedLocalFiles,omitempty"`
ScanLogs []string `json:"scanLogs,omitempty"`
ServerLogs string `json:"serverLogs,omitempty"`
ServerStatus string `json:"status,omitempty"`
}
func NewIssueReport(userAgent, appVersion, _os, arch string, logsDir string, isAnimeLibraryIssue bool, serverStatus interface{}, toRedact []string) (ret *IssueReport, err error) {
ret = &IssueReport{
CreatedAt: time.Now(),
UserAgent: userAgent,
AppVersion: appVersion,
OS: _os,
Arch: arch,
ClickLogs: make([]*ClickLog, 0),
NetworkLogs: make([]*NetworkLog, 0),
ReactQueryLogs: make([]*ReactQueryLog, 0),
ConsoleLogs: make([]*ConsoleLog, 0),
UnlockedLocalFiles: make([]*UnlockedLocalFile, 0),
ScanLogs: make([]string, 0),
ServerLogs: "",
ServerStatus: "",
}
// Get all log files in the directory
entries, err := os.ReadDir(logsDir)
if err != nil {
return nil, fmt.Errorf("failed to read log directory: %w", err)
}
var serverLogFiles []os.FileInfo
var scanLogFiles []os.FileInfo
for _, file := range entries {
if strings.HasPrefix(file.Name(), "seanime-") {
info, err := file.Info()
if err != nil {
continue
}
serverLogFiles = append(serverLogFiles, info)
}
if strings.Contains(file.Name(), "-scan") {
info, err := file.Info()
if err != nil {
continue
}
// Check if file is newer than 1 day
if time.Since(info.ModTime()).Hours() < 24 {
scanLogFiles = append(scanLogFiles, info)
}
}
}
userPathPattern := regexp.MustCompile(`(?i)(/home/|/Users/|C:\\Users\\)([^/\\]+)`)
if serverStatus != nil {
serverStatusMarshaled, err := json.Marshal(serverStatus)
if err == nil {
// pretty print the json
var prettyJSON bytes.Buffer
err = json.Indent(&prettyJSON, serverStatusMarshaled, "", " ")
if err == nil {
ret.ServerStatus = prettyJSON.String()
for _, redact := range toRedact {
ret.ServerStatus = strings.ReplaceAll(ret.ServerStatus, redact, "[REDACTED]")
}
ret.ServerStatus = userPathPattern.ReplaceAllString(ret.ServerStatus, "${1}[REDACTED]")
}
}
}
if len(serverLogFiles) > 0 {
sort.Slice(serverLogFiles, func(i, j int) bool {
return serverLogFiles[i].ModTime().After(serverLogFiles[j].ModTime())
})
// Get the most recent log file
latestLog := serverLogFiles[0]
latestLogPath := filepath.Join(logsDir, latestLog.Name())
latestLogContent, err := os.ReadFile(latestLogPath)
if err != nil {
return nil, fmt.Errorf("failed to read log file: %w", err)
}
latestLogContent = userPathPattern.ReplaceAll(latestLogContent, []byte("${1}[REDACTED]"))
for _, redact := range toRedact {
latestLogContent = bytes.ReplaceAll(latestLogContent, []byte(redact), []byte("[REDACTED]"))
}
ret.ServerLogs = string(latestLogContent)
}
if isAnimeLibraryIssue {
if len(scanLogFiles) > 0 {
for _, file := range scanLogFiles {
scanLogPath := filepath.Join(logsDir, file.Name())
scanLogContent, err := os.ReadFile(scanLogPath)
if err != nil {
continue
}
scanLogContent = userPathPattern.ReplaceAll(scanLogContent, []byte("${1}[REDACTED]"))
if len(scanLogContent) == 0 {
ret.ScanLogs = append(ret.ScanLogs, "EMPTY")
} else {
ret.ScanLogs = append(ret.ScanLogs, string(scanLogContent))
}
}
}
}
return
}
func (ir *IssueReport) AddClickLogs(clickLogs []*ClickLog) {
ir.ClickLogs = append(ir.ClickLogs, clickLogs...)
}
func (ir *IssueReport) AddNetworkLogs(networkLogs []*NetworkLog) {
ir.NetworkLogs = append(ir.NetworkLogs, networkLogs...)
}
func (ir *IssueReport) AddReactQueryLogs(reactQueryLogs []*ReactQueryLog) {
ir.ReactQueryLogs = append(ir.ReactQueryLogs, reactQueryLogs...)
}
func (ir *IssueReport) AddConsoleLogs(consoleLogs []*ConsoleLog) {
ir.ConsoleLogs = append(ir.ConsoleLogs, consoleLogs...)
}