diff --git a/pkg/api/options.go b/pkg/api/options.go
index d6b6b9c6a..f38b2ef35 100644
--- a/pkg/api/options.go
+++ b/pkg/api/options.go
@@ -186,6 +186,36 @@ type RequestSaveOptionsTaskSchedule struct {
LinkScenesHourStart int `json:"linkScenesHourStart"`
LinkScenesHourEnd int `json:"linkScenesHourEnd"`
LinkScenesStartDelay int `json:"linkScenesStartDelay"`
+
+ AutoTagScheduleEnabled bool `json:"autoTagScheduleEnabled"`
+ AutoTagScheduleHourInterval int `json:"autoTagScheduleHourInterval"`
+ AutoTagScheduleUseRange bool `json:"autoTagScheduleUseRange"`
+ AutoTagScheduleMinuteStart int `json:"autoTagScheduleMinuteStart"`
+ AutoTagScheduleHourStart int `json:"autoTagScheduleHourStart"`
+ AutoTagScheduleHourEnd int `json:"autoTagScheduleHourEnd"`
+ AutoTagScheduleStartDelay int `json:"autoTagScheduleStartDelay"`
+
+ AutoTagBreastType bool `json:"autoTagBreastType"`
+ AutoTagAge bool `json:"autoTagAge"`
+ AutoTagHeight bool `json:"autoTagHeight"`
+ AutoTagNationality bool `json:"autoTagNationality"`
+ AutoTagEthnicity bool `json:"autoTagEthnicity"`
+ AutoTagHairColor bool `json:"autoTagHairColor"`
+ AutoTagEyeColor bool `json:"autoTagEyeColor"`
+ AutoTagCupSize bool `json:"autoTagCupSize"`
+ AutoTagResolution bool `json:"autoTagResolution"`
+ AutoTagVideoFormat bool `json:"autoTagVideoFormat"`
+ AutoTagDuration bool `json:"autoTagDuration"`
+
+ AutoTagInterracial bool `json:"autoTagInterracial"`
+
+ AutoTagGenderFilter []string `json:"autoTagGenderFilter"`
+
+ AutoTagHeightShortMax int `json:"autoTagHeightShortMax"`
+ AutoTagHeightAverageMax int `json:"autoTagHeightAverageMax"`
+
+ AutoTagDurationShortMax int `json:"autoTagDurationShortMax"`
+ AutoTagDurationStandardMax int `json:"autoTagDurationStandardMax"`
}
type RequestSaveSiteMatchParams struct {
SiteId string `json:"site"`
@@ -989,6 +1019,9 @@ func (i ConfigResource) saveOptionsTaskSchedule(req *restful.Request, resp *rest
if r.PreviewHourEnd > 23 {
r.PreviewHourEnd -= 24
}
+ if r.AutoTagScheduleHourEnd > 23 {
+ r.AutoTagScheduleHourEnd -= 24
+ }
config.Config.Cron.RescrapeSchedule.Enabled = r.RescrapeEnabled
config.Config.Cron.RescrapeSchedule.HourInterval = r.RescrapeHourInterval
@@ -1038,6 +1071,35 @@ func (i ConfigResource) saveOptionsTaskSchedule(req *restful.Request, resp *rest
config.Config.Cron.LinkScenesSchedule.HourEnd = r.LinkScenesHourEnd
config.Config.Cron.LinkScenesSchedule.RunAtStartDelay = r.LinkScenesStartDelay
+ config.Config.Cron.AutoTagSchedule.Enabled = r.AutoTagScheduleEnabled
+ config.Config.Cron.AutoTagSchedule.HourInterval = r.AutoTagScheduleHourInterval
+ config.Config.Cron.AutoTagSchedule.UseRange = r.AutoTagScheduleUseRange
+ config.Config.Cron.AutoTagSchedule.MinuteStart = r.AutoTagScheduleMinuteStart
+ config.Config.Cron.AutoTagSchedule.HourStart = r.AutoTagScheduleHourStart
+ config.Config.Cron.AutoTagSchedule.HourEnd = r.AutoTagScheduleHourEnd
+ config.Config.Cron.AutoTagSchedule.RunAtStartDelay = r.AutoTagScheduleStartDelay
+
+ config.Config.AutoTag.BreastType = r.AutoTagBreastType
+ config.Config.AutoTag.Age = r.AutoTagAge
+ config.Config.AutoTag.Height = r.AutoTagHeight
+ config.Config.AutoTag.Nationality = r.AutoTagNationality
+ config.Config.AutoTag.Ethnicity = r.AutoTagEthnicity
+ config.Config.AutoTag.HairColor = r.AutoTagHairColor
+ config.Config.AutoTag.EyeColor = r.AutoTagEyeColor
+ config.Config.AutoTag.CupSize = r.AutoTagCupSize
+ config.Config.AutoTag.Resolution = r.AutoTagResolution
+ config.Config.AutoTag.VideoFormat = r.AutoTagVideoFormat
+ config.Config.AutoTag.Duration = r.AutoTagDuration
+
+ config.Config.AutoTag.Interracial = r.AutoTagInterracial
+ config.Config.AutoTag.GenderFilter = r.AutoTagGenderFilter
+
+ config.Config.AutoTag.HeightShortMax = r.AutoTagHeightShortMax
+ config.Config.AutoTag.HeightAverageMax = r.AutoTagHeightAverageMax
+
+ config.Config.AutoTag.DurationShortMax = r.AutoTagDurationShortMax
+ config.Config.AutoTag.DurationStandardMax = r.AutoTagDurationStandardMax
+
config.SaveConfig()
resp.WriteHeaderAndEntity(http.StatusOK, r)
diff --git a/pkg/api/tasks.go b/pkg/api/tasks.go
index f3a119b56..1fef02469 100644
--- a/pkg/api/tasks.go
+++ b/pkg/api/tasks.go
@@ -59,6 +59,18 @@ func (i TaskResource) WebService() *restful.WebService {
ws.Route(ws.GET("/rescan").To(i.rescan).
Metadata(restfulspec.KeyOpenAPITags, tags))
+ ws.Route(ws.GET("/auto-tag").To(i.autoTag).
+ Metadata(restfulspec.KeyOpenAPITags, tags))
+
+ ws.Route(ws.GET("/auto-tag-reset").To(i.autoTagReset).
+ Metadata(restfulspec.KeyOpenAPITags, tags))
+
+ ws.Route(ws.GET("/system-tags").To(i.getSystemTags).
+ Metadata(restfulspec.KeyOpenAPITags, tags))
+
+ ws.Route(ws.GET("/actor-genders").To(i.getActorGenders).
+ Metadata(restfulspec.KeyOpenAPITags, tags))
+
ws.Route(ws.GET("/rescan/{storage-id}").To(i.rescan).
Metadata(restfulspec.KeyOpenAPITags, tags))
@@ -117,6 +129,34 @@ func (i TaskResource) rescan(req *restful.Request, resp *restful.Response) {
}
}
+func (i TaskResource) autoTag(req *restful.Request, resp *restful.Response) {
+ go tasks.GenerateAutoTags()
+}
+
+func (i TaskResource) autoTagReset(req *restful.Request, resp *restful.Response) {
+ go tasks.ResetAutoTags()
+}
+
+func (i TaskResource) getSystemTags(req *restful.Request, resp *restful.Response) {
+ db, _ := models.GetDB()
+ defer db.Close()
+
+ var tags []models.Tag
+ db.Where("is_system = ?", true).Order("name asc").Find(&tags)
+
+ resp.WriteHeaderAndEntity(http.StatusOK, tags)
+}
+
+func (i TaskResource) getActorGenders(req *restful.Request, resp *restful.Response) {
+ db, _ := models.GetDB()
+ defer db.Close()
+
+ var genders []string
+ db.Model(&models.Actor{}).Where("gender != ''").Pluck("DISTINCT gender", &genders)
+
+ resp.WriteHeaderAndEntity(http.StatusOK, genders)
+}
+
func (i TaskResource) sceneRrefresh(req *restful.Request, resp *restful.Response) {
go tasks.RefreshSceneStatuses()
}
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 0a2a4a8d8..ce1973363 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -174,7 +174,40 @@ type ObjectConfig struct {
HourEnd int `default:"23" json:"hourEnd"`
RunAtStartDelay int `default:"0" json:"runAtStartDelay"`
} `json:"linkScenesSchedule"`
+ AutoTagSchedule struct {
+ Enabled bool `default:"false" json:"enabled"`
+ HourInterval int `default:"12" json:"hourInterval"`
+ UseRange bool `default:"false" json:"useRange"`
+ MinuteStart int `default:"0" json:"minuteStart"`
+ HourStart int `default:"0" json:"hourStart"`
+ HourEnd int `default:"23" json:"hourEnd"`
+ RunAtStartDelay int `default:"0" json:"runAtStartDelay"`
+ } `json:"autoTagSchedule"`
} `json:"cron"`
+ AutoTag struct {
+ BreastType bool `default:"false" json:"breastType"`
+ Age bool `default:"false" json:"age"`
+ Height bool `default:"false" json:"height"`
+ Nationality bool `default:"false" json:"nationality"`
+ Ethnicity bool `default:"false" json:"ethnicity"`
+ HairColor bool `default:"false" json:"hairColor"`
+ EyeColor bool `default:"false" json:"eyeColor"`
+ CupSize bool `default:"false" json:"cupSize"`
+ Resolution bool `default:"false" json:"resolution"`
+ VideoFormat bool `default:"false" json:"videoFormat"`
+ Duration bool `default:"false" json:"duration"`
+
+ Interracial bool `default:"false" json:"interracial"`
+ BMI bool `default:"false" json:"bmi"`
+
+ GenderFilter []string `default:"[]" json:"genderFilter"`
+
+ HeightShortMax int `default:"160" json:"heightShortMax"`
+ HeightAverageMax int `default:"175" json:"heightAverageMax"`
+
+ DurationShortMax int `default:"15" json:"durationShortMax"`
+ DurationStandardMax int `default:"40" json:"durationStandardMax"`
+ } `json:"autoTag"`
Storage struct {
MatchOhash bool `default:"false" json:"match_ohash"`
VideoExt []string `json:"video_ext"`
diff --git a/pkg/migrations/migrations.go b/pkg/migrations/migrations.go
index 9d2a818b2..93dc5c38c 100644
--- a/pkg/migrations/migrations.go
+++ b/pkg/migrations/migrations.go
@@ -2387,6 +2387,12 @@ func Migrate(migrateTo string) {
return tx.Table("scenes").AddIndex("idx_scenes_scraper_id", "scraper_id").Error
},
},
+ {
+ ID: "0088-add-is_system-to-tags",
+ Migrate: func(tx *gorm.DB) error {
+ return tx.AutoMigrate(&models.Tag{}).Error
+ },
+ },
}
// Wrap migrations to automatically track progress
diff --git a/pkg/models/model_tag.go b/pkg/models/model_tag.go
index 248d400ae..18fd2b877 100644
--- a/pkg/models/model_tag.go
+++ b/pkg/models/model_tag.go
@@ -8,11 +8,12 @@ import (
)
type Tag struct {
- ID uint `gorm:"primary_key" json:"id" xbvrbackup:"-"`
- Scenes []Scene `gorm:"many2many:scene_tags;" json:"scenes" xbvrbackup:"-"`
- Name string `gorm:"index" json:"name" xbvrbackup:"name"`
- Clean string `gorm:"index" json:"clean" xbvrbackup:"-"`
- Count int `json:"count" xbvrbackup:"-"`
+ ID uint `gorm:"primary_key" json:"id" xbvrbackup:"-"`
+ Scenes []Scene `gorm:"many2many:scene_tags;" json:"scenes" xbvrbackup:"-"`
+ Name string `gorm:"index" json:"name" xbvrbackup:"name"`
+ Clean string `gorm:"index" json:"clean" xbvrbackup:"-"`
+ Count int `json:"count" xbvrbackup:"-"`
+ IsSystem bool `json:"is_system" gorm:"default:false" xbvrbackup:"-"`
}
func (t *Tag) Save() error {
diff --git a/pkg/server/cron.go b/pkg/server/cron.go
index 001fd1964..52f6dfb8e 100644
--- a/pkg/server/cron.go
+++ b/pkg/server/cron.go
@@ -18,6 +18,7 @@ var previewTask cron.EntryID
var actorScrapeTask cron.EntryID
var stashdbScrapeTask cron.EntryID
var linkScenesTask cron.EntryID
+var autoTagTask cron.EntryID
func SetupCron() {
cronInstance = cron.New()
@@ -48,6 +49,10 @@ func SetupCron() {
log.Println(fmt.Sprintf("Setup Link Scenes Task %v", formatCronSchedule(config.CronSchedule(config.Config.Cron.LinkScenesSchedule))))
linkScenesTask, _ = cronInstance.AddFunc(formatCronSchedule(config.CronSchedule(config.Config.Cron.LinkScenesSchedule)), linkScenesCron)
}
+ if config.Config.Cron.AutoTagSchedule.Enabled {
+ log.Println(fmt.Sprintf("Setup AutoTag Task %v", formatCronSchedule(config.CronSchedule(config.Config.Cron.AutoTagSchedule))))
+ autoTagTask, _ = cronInstance.AddFunc(formatCronSchedule(config.CronSchedule(config.Config.Cron.AutoTagSchedule)), autoTagCron)
+ }
cronInstance.Start()
go tasks.CalculateCacheSizes()
@@ -70,6 +75,9 @@ func SetupCron() {
if config.Config.Cron.LinkScenesSchedule.RunAtStartDelay > 0 {
time.AfterFunc(time.Duration(config.Config.Cron.LinkScenesSchedule.RunAtStartDelay)*time.Minute, linkScenesCron)
}
+ if config.Config.Cron.AutoTagSchedule.RunAtStartDelay > 0 {
+ time.AfterFunc(time.Duration(config.Config.Cron.AutoTagSchedule.RunAtStartDelay)*time.Minute, autoTagCron)
+ }
log.Println(fmt.Sprintf("Next Rescrape Task at %v", cronInstance.Entry(rescrapTask).Next))
log.Println(fmt.Sprintf("Next Rescan Task at %v", cronInstance.Entry(rescanTask).Next))
@@ -77,6 +85,7 @@ func SetupCron() {
log.Println(fmt.Sprintf("Next Actor Rescripe Task at %v", cronInstance.Entry(actorScrapeTask).Next))
log.Println(fmt.Sprintf("Next Stashdb Rescrape Task at %v", cronInstance.Entry(stashdbScrapeTask).Next))
log.Println(fmt.Sprintf("Next Link Scenes Task at %v", cronInstance.Entry(linkScenesTask).Next))
+ log.Println(fmt.Sprintf("Next AutoTag Task at %v", cronInstance.Entry(autoTagTask).Next))
}
func scrapeCron() {
@@ -112,6 +121,13 @@ func linkScenesCron() {
log.Println(fmt.Sprintf("Next Link Scenes Task at %v", cronInstance.Entry(rescrapTask).Next))
}
+func autoTagCron() {
+ if !session.HasActiveSession() {
+ tasks.GenerateAutoTags()
+ }
+ log.Println(fmt.Sprintf("Next AutoTag Task at %v", cronInstance.Entry(autoTagTask).Next))
+}
+
var previewGenerateInProgress = false
func generatePreviewCron() {
diff --git a/pkg/tasks/auto_tag.go b/pkg/tasks/auto_tag.go
new file mode 100644
index 000000000..6dd730cad
--- /dev/null
+++ b/pkg/tasks/auto_tag.go
@@ -0,0 +1,281 @@
+package tasks
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/jinzhu/gorm"
+ "github.com/xbapps/xbvr/pkg/config"
+ "github.com/xbapps/xbvr/pkg/models"
+)
+
+func ResetAutoTags() {
+ db, _ := models.GetDB()
+ defer db.Close()
+
+ var tags []models.Tag
+ db.Where("is_system = ?", true).Find(&tags)
+
+ for _, tag := range tags {
+ db.Exec("DELETE FROM scene_tags WHERE tag_id = ?", tag.ID)
+ db.Delete(&tag)
+ }
+
+ var t models.Tag
+ t.CountTags()
+}
+
+func GenerateAutoTags() {
+ cfg := config.Config.AutoTag
+ if !cfg.BreastType && !cfg.Age && !cfg.Height && !cfg.Nationality &&
+ !cfg.Ethnicity && !cfg.HairColor && !cfg.EyeColor && !cfg.CupSize &&
+ !cfg.Resolution && !cfg.VideoFormat && !cfg.Duration && !cfg.Interracial {
+ return
+ }
+
+ db, _ := models.GetDB()
+ defer db.Close()
+
+ // Build gender filter set (empty = include all genders)
+ genderFilter := make(map[string]bool)
+ for _, g := range cfg.GenderFilter {
+ genderFilter[strings.ToLower(strings.TrimSpace(g))] = true
+ }
+ filterByGender := len(genderFilter) > 0
+
+ // Pre-cache existing system tags: lowercase name -> ID
+ tagCache := make(map[string]uint)
+ var existingTags []models.Tag
+ db.Where("is_system = ?", true).Find(&existingTags)
+ for _, t := range existingTags {
+ tagCache[strings.ToLower(t.Name)] = t.ID
+ }
+
+ heightShortMax := cfg.HeightShortMax
+ heightAvgMax := cfg.HeightAverageMax
+ durShortMax := cfg.DurationShortMax
+ durStdMax := cfg.DurationStandardMax
+
+ const batchSize = 500
+ offset := 0
+ for {
+ var scenes []models.Scene
+ db.Model(&models.Scene{}).Preload("Cast").Preload("Tags").Preload("Files").
+ Limit(batchSize).Offset(offset).Find(&scenes)
+ if len(scenes) == 0 {
+ break
+ }
+
+ for _, scene := range scenes {
+ // Snapshot existing scene tags for duplicate detection
+ existingSceneTags := make(map[string]bool)
+ for _, t := range scene.Tags {
+ existingSceneTags[strings.ToLower(t.Name)] = true
+ }
+
+ // Filter cast by gender if configured
+ filteredCast := scene.Cast
+ if filterByGender {
+ filteredCast = nil
+ for _, actor := range scene.Cast {
+ if genderFilter[strings.ToLower(strings.TrimSpace(actor.Gender))] {
+ filteredCast = append(filteredCast, actor)
+ }
+ }
+ }
+
+ addTag := func(name string) {
+ addTagToScene(db, &scene, name, existingSceneTags, tagCache)
+ }
+
+ // Breast Type
+ if cfg.BreastType {
+ isNatural, isFake := false, false
+ for _, actor := range filteredCast {
+ if strings.EqualFold(actor.BreastType, "Natural") {
+ isNatural = true
+ }
+ if strings.EqualFold(actor.BreastType, "Fake") ||
+ strings.EqualFold(actor.BreastType, "Silicone") ||
+ strings.EqualFold(actor.BreastType, "Enhanced") {
+ isFake = true
+ }
+ }
+ if isNatural {
+ addTag("Breast Type - Natural")
+ }
+ if isFake {
+ addTag("Breast Type - Fake")
+ }
+ }
+
+ // Age
+ if cfg.Age && !scene.ReleaseDate.IsZero() {
+ for _, actor := range filteredCast {
+ if !actor.BirthDate.IsZero() {
+ age := scene.ReleaseDate.Year() - actor.BirthDate.Year()
+ if scene.ReleaseDate.YearDay() < actor.BirthDate.YearDay() {
+ age--
+ }
+ if age >= 18 && age < 100 {
+ addTag(fmt.Sprintf("Age: %d", age))
+ }
+ }
+ }
+ }
+
+ // Height
+ if cfg.Height {
+ for _, actor := range filteredCast {
+ if actor.Height > 0 {
+ if actor.Height <= heightShortMax {
+ addTag("Height: Short")
+ } else if actor.Height <= heightAvgMax {
+ addTag("Height: Average")
+ } else {
+ addTag("Height: Tall")
+ }
+ }
+ }
+ }
+
+ // Nationality
+ if cfg.Nationality {
+ for _, actor := range filteredCast {
+ if actor.Nationality != "" {
+ addTag("Nationality: " + actor.Nationality)
+ }
+ }
+ }
+
+ // Ethnicity
+ if cfg.Ethnicity {
+ for _, actor := range filteredCast {
+ if actor.Ethnicity != "" {
+ addTag("Ethnicity: " + actor.Ethnicity)
+ }
+ }
+ }
+
+ // Hair Color
+ if cfg.HairColor {
+ for _, actor := range filteredCast {
+ if actor.HairColor != "" {
+ addTag("Hair: " + actor.HairColor)
+ }
+ }
+ }
+
+ // Eye Color
+ if cfg.EyeColor {
+ for _, actor := range filteredCast {
+ if actor.EyeColor != "" {
+ addTag("Eyes: " + actor.EyeColor)
+ }
+ }
+ }
+
+ // Cup Size
+ if cfg.CupSize {
+ for _, actor := range filteredCast {
+ if actor.CupSize != "" {
+ addTag("Cup: " + actor.CupSize)
+ }
+ }
+ }
+
+ // Interracial — only among gender-filtered cast
+ if cfg.Interracial && len(filteredCast) > 0 {
+ ethnicities := make(map[string]bool)
+ for _, actor := range filteredCast {
+ if actor.Ethnicity != "" {
+ ethnicities[strings.ToLower(actor.Ethnicity)] = true
+ }
+ }
+ if len(ethnicities) > 1 {
+ addTag("Interracial")
+ }
+ }
+
+ // Duration
+ if cfg.Duration && scene.Duration > 0 {
+ if scene.Duration <= durShortMax {
+ addTag("Duration: Short")
+ } else if scene.Duration <= durStdMax {
+ addTag("Duration: Standard")
+ } else {
+ addTag("Duration: Long")
+ }
+ }
+
+ // Resolution & Format
+ if (cfg.Resolution || cfg.VideoFormat) && len(scene.Files) > 0 {
+ var bestFile models.File
+ for _, f := range scene.Files {
+ if f.VideoHeight > bestFile.VideoHeight {
+ bestFile = f
+ }
+ }
+
+ if cfg.Resolution && bestFile.VideoHeight > 0 {
+ switch {
+ case bestFile.VideoHeight >= 4320:
+ addTag("Res: 8K")
+ case bestFile.VideoHeight >= 2880:
+ addTag("Res: 6K")
+ case bestFile.VideoHeight >= 2700:
+ addTag("Res: 5K")
+ case bestFile.VideoHeight >= 1900:
+ addTag("Res: 4K")
+ case bestFile.VideoHeight >= 1440:
+ addTag("Res: 1440p")
+ case bestFile.VideoHeight >= 1080:
+ addTag("Res: 1080p")
+ case bestFile.VideoHeight >= 720:
+ addTag("Res: 720p")
+ default:
+ addTag("Res: SD")
+ }
+ }
+
+ if cfg.VideoFormat {
+ switch bestFile.VideoProjection {
+ case "180_sbs", "180_tb":
+ addTag("Format: 180°")
+ case "360_sbs", "360_tb", "360_mono":
+ addTag("Format: 360°")
+ case "flat":
+ addTag("Format: Flat")
+ }
+ }
+ }
+ }
+ offset += batchSize
+ }
+
+ var t models.Tag
+ t.CountTags()
+}
+
+func addTagToScene(db *gorm.DB, scene *models.Scene, tagName string, existingSceneTags map[string]bool, tagCache map[string]uint) {
+ lowerName := strings.ToLower(tagName)
+ if existingSceneTags[lowerName] {
+ return
+ }
+ existingSceneTags[lowerName] = true
+
+ var tag models.Tag
+ if id, ok := tagCache[lowerName]; ok {
+ tag.ID = id
+ tag.Name = tagName
+ } else {
+ db.Where(models.Tag{Name: tagName}).FirstOrCreate(&tag)
+ if !tag.IsSystem {
+ db.Model(&tag).Update("is_system", true)
+ tag.IsSystem = true
+ }
+ tagCache[lowerName] = tag.ID
+ }
+
+ db.Model(scene).Association("Tags").Append(tag)
+}
diff --git a/pkg/tasks/dms.go b/pkg/tasks/dms.go
index 8daacae27..35f25fd29 100644
--- a/pkg/tasks/dms.go
+++ b/pkg/tasks/dms.go
@@ -10,6 +10,7 @@ import (
"time"
"github.com/nfnt/resize"
+ "github.com/xbapps/xbvr/pkg/common"
"github.com/xbapps/xbvr/pkg/config"
"github.com/xbapps/xbvr/pkg/dms/dlna/dms"
"github.com/xbapps/xbvr/ui"
@@ -110,11 +111,21 @@ func getIconReader(fn string) (io.Reader, error) {
func readIcon(path string, size uint) *bytes.Reader {
r, err := getIconReader(path)
if err != nil {
- panic(err)
+ common.Log.Errorf("Failed to read DLNA icon %s: %v. Using default placeholder.", path, err)
+ // Create a 1x1 transparent image as fallback
+ img := image.NewRGBA(image.Rect(0, 0, 1, 1))
+ var buf bytes.Buffer
+ if err := png.Encode(&buf, img); err != nil {
+ common.Log.Error(err)
+ return bytes.NewReader([]byte{})
+ }
+ return bytes.NewReader(buf.Bytes())
}
+
imageData, _, err := image.Decode(r)
if err != nil {
- panic(err)
+ common.Log.Error(err)
+ return bytes.NewReader([]byte{})
}
return resizeImage(imageData, size)
}
diff --git a/ui/src/views/options/sections/Schedules.vue b/ui/src/views/options/sections/Schedules.vue
index 18df4b3c4..c234b7388 100644
--- a/ui/src/views/options/sections/Schedules.vue
+++ b/ui/src/views/options/sections/Schedules.vue
@@ -11,6 +11,7 @@
+ {{$t("Only apply actor-based tags to these genders. Leave all unchecked to tag all genders.")}} +
+