2026-05-22 20:26:11 -04:00
|
|
|
package sections
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
2026-05-27 15:48:00 -04:00
|
|
|
|
|
|
|
|
"github.com/Silo-Server/silo-server/internal/catalog"
|
2026-05-22 20:26:11 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const GeneratedHomeLibraryRecentSource = "home_library_recent"
|
|
|
|
|
|
2026-05-27 15:48:00 -04:00
|
|
|
const (
|
|
|
|
|
generatedHomeLibraryRecentKindAdded = "recently_added"
|
|
|
|
|
generatedHomeLibraryRecentKindReleased = "recently_released"
|
|
|
|
|
generatedHomeLibraryRecentKindReleasedEpisodes = "recently_released_episodes"
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-22 20:26:11 -04:00
|
|
|
type generatedHomeLibraryRecentConfig struct {
|
2026-05-27 15:48:00 -04:00
|
|
|
FilterLibraryID *int `json:"filter_library_id"`
|
|
|
|
|
GeneratedLibraryID *int `json:"generated_library_id"`
|
|
|
|
|
GeneratedKind string `json:"generated_kind"`
|
|
|
|
|
GeneratedSource string `json:"generated_source"`
|
2026-05-22 20:26:11 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func GeneratedHomeLibraryRecentConfig(libraryID int) json.RawMessage {
|
|
|
|
|
config, err := json.Marshal(generatedHomeLibraryRecentConfig{
|
|
|
|
|
FilterLibraryID: &libraryID,
|
|
|
|
|
GeneratedSource: GeneratedHomeLibraryRecentSource,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return json.RawMessage(`{}`)
|
|
|
|
|
}
|
|
|
|
|
return config
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 20:13:10 -04:00
|
|
|
// GeneratedHomeLibraryRecentConfigScoped builds the generated home "recent"
|
|
|
|
|
// config for a library while constraining results to a single media scope.
|
|
|
|
|
// This is required for mixed-type libraries (e.g. manga, which contains both
|
|
|
|
|
// type='manga' series and type='ebook' chapters) so the auto-generated home
|
|
|
|
|
// rows only surface the series and not the junk chapter filenames. It mirrors
|
|
|
|
|
// the modern QueryDefinition shape used by GeneratedHomeLibraryRecentEpisodesConfig
|
|
|
|
|
// (library_ids + media_scope) — note we intentionally avoid filter_library_id
|
|
|
|
|
// here, since that flat key routes the config through the legacy parser which
|
|
|
|
|
// drops media_scope. Library targeting comes from both library_ids and the
|
|
|
|
|
// generated_library_id metadata read by parseGeneratedHomeLibraryRecentConfig.
|
|
|
|
|
func GeneratedHomeLibraryRecentConfigScoped(libraryID int, mediaScope string) json.RawMessage {
|
|
|
|
|
config, err := json.Marshal(struct {
|
|
|
|
|
catalog.QueryDefinition
|
|
|
|
|
GeneratedLibraryID int `json:"generated_library_id"`
|
|
|
|
|
GeneratedSource string `json:"generated_source"`
|
|
|
|
|
}{
|
|
|
|
|
QueryDefinition: catalog.QueryDefinition{
|
|
|
|
|
LibraryIDs: []int{libraryID},
|
|
|
|
|
MediaScope: mediaScope,
|
|
|
|
|
Match: "all",
|
|
|
|
|
Groups: []catalog.QueryGroup{},
|
|
|
|
|
Sort: catalog.QuerySort{Field: "added_at", Order: "desc"},
|
|
|
|
|
}.Normalize(),
|
|
|
|
|
GeneratedLibraryID: libraryID,
|
|
|
|
|
GeneratedSource: GeneratedHomeLibraryRecentSource,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return json.RawMessage(`{}`)
|
|
|
|
|
}
|
|
|
|
|
return config
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 15:48:00 -04:00
|
|
|
func GeneratedHomeLibraryRecentEpisodesConfig(libraryID int) json.RawMessage {
|
|
|
|
|
config, err := json.Marshal(struct {
|
|
|
|
|
catalog.QueryDefinition
|
|
|
|
|
GeneratedLibraryID int `json:"generated_library_id"`
|
|
|
|
|
GeneratedKind string `json:"generated_kind"`
|
|
|
|
|
GeneratedSource string `json:"generated_source"`
|
|
|
|
|
}{
|
|
|
|
|
QueryDefinition: catalog.QueryDefinition{
|
|
|
|
|
LibraryIDs: []int{libraryID},
|
|
|
|
|
MediaScope: "episode",
|
|
|
|
|
Match: "all",
|
|
|
|
|
Groups: []catalog.QueryGroup{},
|
|
|
|
|
Sort: catalog.QuerySort{Field: "release_date", Order: "desc"},
|
|
|
|
|
}.Normalize(),
|
|
|
|
|
GeneratedLibraryID: libraryID,
|
|
|
|
|
GeneratedKind: generatedHomeLibraryRecentKindReleasedEpisodes,
|
|
|
|
|
GeneratedSource: GeneratedHomeLibraryRecentSource,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return json.RawMessage(`{}`)
|
|
|
|
|
}
|
|
|
|
|
return config
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 20:26:11 -04:00
|
|
|
func GeneratedHomeLibraryRecentTitle(sectionType SectionType, libraryName string) string {
|
|
|
|
|
switch sectionType {
|
|
|
|
|
case SectionRecentlyAdded:
|
|
|
|
|
return fmt.Sprintf("Recently Added in %s", libraryName)
|
|
|
|
|
case SectionRecentlyReleased:
|
|
|
|
|
return fmt.Sprintf("Recently Released in %s", libraryName)
|
|
|
|
|
default:
|
|
|
|
|
return string(sectionType)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 15:48:00 -04:00
|
|
|
func generatedHomeLibraryRecentTitle(kind string, sectionType SectionType, libraryName string) string {
|
|
|
|
|
switch kind {
|
|
|
|
|
case generatedHomeLibraryRecentKindReleasedEpisodes:
|
|
|
|
|
return fmt.Sprintf("Recently Released Episodes in %s", libraryName)
|
|
|
|
|
case generatedHomeLibraryRecentKindAdded:
|
|
|
|
|
return fmt.Sprintf("Recently Added in %s", libraryName)
|
|
|
|
|
case generatedHomeLibraryRecentKindReleased:
|
|
|
|
|
return fmt.Sprintf("Recently Released in %s", libraryName)
|
|
|
|
|
default:
|
|
|
|
|
return GeneratedHomeLibraryRecentTitle(sectionType, libraryName)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func generatedHomeLibraryRecentKindForSection(s *PageSection) string {
|
|
|
|
|
if s == nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
_, kind, _ := parseGeneratedHomeLibraryRecentConfig(s.Config)
|
|
|
|
|
if kind != "" {
|
|
|
|
|
return kind
|
|
|
|
|
}
|
|
|
|
|
switch s.SectionType {
|
|
|
|
|
case SectionRecentlyAdded:
|
|
|
|
|
return generatedHomeLibraryRecentKindAdded
|
|
|
|
|
case SectionRecentlyReleased:
|
|
|
|
|
return generatedHomeLibraryRecentKindReleased
|
|
|
|
|
default:
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 20:26:11 -04:00
|
|
|
func ParseGeneratedHomeLibraryRecentConfig(config json.RawMessage) (int, bool) {
|
2026-05-27 15:48:00 -04:00
|
|
|
id, _, ok := parseGeneratedHomeLibraryRecentConfig(config)
|
|
|
|
|
return id, ok
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func parseGeneratedHomeLibraryRecentConfig(config json.RawMessage) (int, string, bool) {
|
2026-05-22 20:26:11 -04:00
|
|
|
var cfg generatedHomeLibraryRecentConfig
|
|
|
|
|
if len(config) == 0 {
|
2026-05-27 15:48:00 -04:00
|
|
|
return 0, "", false
|
2026-05-22 20:26:11 -04:00
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal(config, &cfg); err != nil {
|
2026-05-27 15:48:00 -04:00
|
|
|
return 0, "", false
|
2026-05-22 20:26:11 -04:00
|
|
|
}
|
2026-05-27 15:48:00 -04:00
|
|
|
if cfg.GeneratedSource != GeneratedHomeLibraryRecentSource {
|
|
|
|
|
return 0, "", false
|
2026-05-22 20:26:11 -04:00
|
|
|
}
|
2026-05-27 15:48:00 -04:00
|
|
|
libraryID := cfg.GeneratedLibraryID
|
|
|
|
|
if libraryID == nil {
|
|
|
|
|
libraryID = cfg.FilterLibraryID
|
|
|
|
|
}
|
|
|
|
|
if libraryID == nil || *libraryID <= 0 {
|
|
|
|
|
return 0, "", false
|
|
|
|
|
}
|
|
|
|
|
return *libraryID, cfg.GeneratedKind, true
|
2026-05-22 20:26:11 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func IsGeneratedHomeLibraryRecentSection(s *PageSection, libraryID int) bool {
|
|
|
|
|
if s == nil || s.Scope != "home" || s.LibraryID != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-05-27 15:48:00 -04:00
|
|
|
id, _, ok := parseGeneratedHomeLibraryRecentConfig(s.Config)
|
|
|
|
|
if !ok {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if s.SectionType != SectionRecentlyAdded && s.SectionType != SectionRecentlyReleased && s.SectionType != SectionCustomFilter {
|
2026-05-22 20:26:11 -04:00
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return ok && id == libraryID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ShouldSyncGeneratedHomeLibraryRecentTitle(s *PageSection, oldLibraryName string) bool {
|
|
|
|
|
if s == nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-05-27 15:48:00 -04:00
|
|
|
kind := generatedHomeLibraryRecentKindForSection(s)
|
|
|
|
|
expected := generatedHomeLibraryRecentTitle(kind, s.SectionType, oldLibraryName)
|
2026-05-22 20:26:11 -04:00
|
|
|
return strings.TrimSpace(s.Title) == expected
|
|
|
|
|
}
|
2026-05-27 15:48:00 -04:00
|
|
|
|
|
|
|
|
func GeneratedHomeLibraryRecentSyncedTitle(s *PageSection, libraryName string) string {
|
|
|
|
|
if s == nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
kind := generatedHomeLibraryRecentKindForSection(s)
|
|
|
|
|
return generatedHomeLibraryRecentTitle(kind, s.SectionType, libraryName)
|
|
|
|
|
}
|