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 @@ +
@@ -249,6 +250,223 @@
{{ delayStartMsg(linkScenesStartDelay) }}
+
+
+ + Enable schedule + + + +
{{`Run every ${this.autoTagScheduleHourInterval} hour${this.autoTagScheduleHourInterval > 1 ? 's': ''}`}}
+
+ + Limit time of day + +
+ + + 00:00 + 06:00 + 12:00 + 18:00 + Midnight + 06:00 + 12:00 + 18:00 + 00:00 + +
{{`${this.timeRange[this.autoTagScheduleTimeRange[0]]} - ${this.timeRange[this.autoTagScheduleTimeRange[1]]}`}}
+
+ + +
{{ minutesStartMsg(autoTagScheduleMinuteStart) }}
+
+
+
+ + +
{{ delayStartMsg(autoTagScheduleStartDelay) }}
+
+
+

{{$t("Generators")}}

+ +
+
{{$t("Gender Filter")}}
+

+ {{$t("Only apply actor-based tags to these genders. Leave all unchecked to tag all genders.")}} +

+ +
+ + {{ gender }} + +
+
+
+ +
+
+
{{$t("Actor Characteristics")}}
+ + + {{$t("Breast Size")}} +
+ {{$t("Generates: Cup: C, Cup: DD, etc.")}} + + {{tag.name}} ({{tag.count}}) + +
+
+ + + {{$t("Breast Type")}} +
+ {{$t("Generates: Breast Type - Natural, Breast Type - Fake")}} + + {{tag.name}} ({{tag.count}}) + +
+
+ + + {{$t("Age")}} +
+ {{$t("Generates: Age: 25, Age: 30, etc.")}} + + {{tag.name}} ({{tag.count}}) + +
+
+ + + {{$t("Height")}} +
+ {{$t("Generates: Height: Short/Average/Tall")}} + + {{tag.name}} ({{tag.count}}) + +
+
+
+ + + + + + +
+ + + + {{$t("Nationality")}} +
+ {{$t("Generates: Nationality: USA, etc.")}} + + {{tag.name}} ({{tag.count}}) + +
+
+ + + {{$t("Ethnicity")}} +
+ {{$t("Generates: Ethnicity: Asian, etc.")}} + + {{tag.name}} ({{tag.count}}) + +
+
+ + + {{$t("Hair Color")}} +
+ {{$t("Generates: Hair: Blonde, etc.")}} + + {{tag.name}} ({{tag.count}}) + +
+
+ + + {{$t("Eye Color")}} +
+ {{$t("Generates: Eyes: Blue, etc.")}} + + {{tag.name}} ({{tag.count}}) + +
+
+
+
+
{{$t("Video Quality")}}
+ + + {{$t("Resolution")}} +
+ {{$t("Generates: Res: 1080p, Res: 4K, etc.")}} + + {{tag.name}} ({{tag.count}}) + +
+
+ + + {{$t("Video Format")}} +
+ {{$t("Generates: Format: 180°, etc.")}} + + {{tag.name}} ({{tag.count}}) + +
+
+ +
{{$t("Scene Attributes")}}
+ + + {{$t("Duration")}} +
+ {{$t("Generates: Duration: Short/Standard/Long")}} + + {{tag.name}} ({{tag.count}}) + +
+
+
+ + + + + + +
+ + + {{$t("Interracial")}} +
+ {{$t("Generates: Interracial")}} + + {{tag.name}} ({{tag.count}}) + +
+
+
+
+ +
+

{{$t("Advanced Controls")}}

+
+
+ Run Now + Reset System Tags +
+
+ + +

@@ -319,6 +537,37 @@ export default { lastlinkScenesTimeRange: [0,23], useLinkScenesTimeRange: false, linkScenesStartDelay: 0, + autoTagScheduleEnabled: false, + autoTagScheduleTimeRange:[0,23], + autoTagScheduleHourInterval: 0, + autoTagScheduleMinuteStart: 0, + lastAutoTagScheduleTimeRange: [0,23], + useAutoTagScheduleTimeRange: false, + autoTagScheduleStartDelay: 0, + autoTagGenderFilter: [], + availableGenders: [], + autoTagBreastType: false, + + autoTagAge: false, + autoTagHeight: false, + autoTagNationality: false, + autoTagEthnicity: false, + autoTagHairColor: false, + autoTagEyeColor: false, + autoTagCupSize: false, + autoTagResolution: false, + autoTagVideoFormat: false, + autoTagDuration: false, + + autoTagInterracial: false, + autoTagHeightShortMax: 160, + autoTagHeightAverageMax: 175, + autoTagDurationShortMax: 15, + autoTagDurationStandardMax: 40, + isRunNowLoading: false, + isResetLoading: false, + systemTags: [], + timeRange: ['00:00', '01:00', '02:00', '03:00', '04:00', '05:00', '06:00', '07:00', '08:00', '09:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00', '16:00', '17:00', '18:00', '19:00', '20:00', '21:00', '22:00', '23:00', '00:00', '01:00', '02:00', '03:00', '04:00', '05:00', '06:00', '07:00', '08:00', '09:00', '10:00', '11:00', @@ -327,8 +576,23 @@ export default { }, async mounted () { await this.loadState() + await this.loadSystemTags() + await this.loadActorGenders() }, computed: { + breastSizeTags () { return this.systemTags.filter(t => t.name.startsWith('Cup:')) }, + breastTypeTags () { return this.systemTags.filter(t => t.name.startsWith('Breast Type -')) }, + ageTags () { return this.systemTags.filter(t => t.name.startsWith('Age:')) }, + heightTags () { return this.systemTags.filter(t => t.name.startsWith('Height:')) }, + nationalityTags () { return this.systemTags.filter(t => t.name.startsWith('Nationality:')) }, + ethnicityTags () { return this.systemTags.filter(t => t.name.startsWith('Ethnicity:')) }, + hairColorTags () { return this.systemTags.filter(t => t.name.startsWith('Hair:')) }, + eyeColorTags () { return this.systemTags.filter(t => t.name.startsWith('Eyes:')) }, + resolutionTags () { return this.systemTags.filter(t => t.name.startsWith('Res:')) }, + videoFormatTags () { return this.systemTags.filter(t => t.name.startsWith('Format:')) }, + durationTags () { return this.systemTags.filter(t => t.name.startsWith('Duration:')) }, + interracialTags () { return this.systemTags.filter(t => t.name === 'Interracial') }, + stashApiKey: { get () { return this.$store.state.optionsAdvanced.advanced.stashApiKey @@ -364,6 +628,10 @@ export default { this.linkScenesTimeRange = this.restrictTo24Hours(this.linkScenesTimeRange, this.lastLinkScenesTimeRange) this.lastLinkScenesTimeRange = this.LinkScenesTimeRange }, + restrictAutoTagScheduleTo24Hours () { + this.autoTagScheduleTimeRange = this.restrictTo24Hours(this.autoTagScheduleTimeRange, this.lastAutoTagScheduleTimeRange) + this.lastAutoTagScheduleTimeRange = this.autoTagScheduleTimeRange + }, restrictTo24Hours (timeRange, lastTimeRange) { // check the first time is not in the second 24 hours, no need, should be in the first 24 hours if (timeRange[0] > 23) { @@ -380,6 +648,59 @@ export default { } return timeRange }, + async runAutoTag () { + this.isRunNowLoading = true + await ky.get('/api/task/auto-tag') + this.isRunNowLoading = false + this.$buefy.toast.open({ + message: 'Auto-tagging started in background. Reload this tab when complete to see updated tags.', + type: 'is-success' + }) + }, + async loadSystemTags () { + await ky.get('/api/task/system-tags') + .json() + .then(data => { + this.systemTags = data + }) + }, + async loadActorGenders () { + await ky.get('/api/task/actor-genders') + .json() + .then(data => { + this.availableGenders = data || [] + }) + }, + showTagScenes (tagName) { + this.$store.state.sceneList.filters.cast = [] + this.$store.state.sceneList.filters.sites = [] + this.$store.state.sceneList.filters.tags = [tagName] + this.$store.state.sceneList.filters.attributes = [] + this.$router.push({ + name: 'scenes', + query: { q: this.$store.getters['sceneList/filterQueryParams'] } + }) + }, + async resetAutoTag () { + this.$buefy.dialog.confirm({ + title: 'Reset System Tags', + message: 'Are you sure you want to delete all system-generated tags from all scenes?', + confirmText: 'Reset', + type: 'is-danger', + hasIcon: true, + onConfirm: async () => { + this.isResetLoading = true + await ky.get('/api/task/auto-tag-reset') + this.isResetLoading = false + this.$buefy.toast.open({ + message: 'System tags reset', + type: 'is-success' + }) + this.systemTags = [] + } + }) + }, + async loadState () { this.isLoading = true await ky.get('/api/options/state') @@ -441,6 +762,38 @@ export default { } else { this.linkScenesTimeRange = [data.config.cron.linkScenesSchedule.hourStart, data.config.cron.linkScenesSchedule.hourEnd] } + + this.autoTagScheduleEnabled = data.config.cron.autoTagSchedule.enabled + this.autoTagScheduleHourInterval = data.config.cron.autoTagSchedule.hourInterval + this.useAutoTagScheduleTimeRange = data.config.cron.autoTagSchedule.useRange + this.autoTagScheduleMinuteStart = data.config.cron.autoTagSchedule.minuteStart + + if (data.config.cron.autoTagSchedule.hourStart > data.config.cron.autoTagSchedule.hourEnd) { + this.autoTagScheduleTimeRange = [data.config.cron.autoTagSchedule.hourStart, data.config.cron.autoTagSchedule.hourEnd + 24] + } else { + this.autoTagScheduleTimeRange = [data.config.cron.autoTagSchedule.hourStart, data.config.cron.autoTagSchedule.hourEnd] + } + + this.autoTagScheduleStartDelay = data.config.cron.autoTagSchedule.runAtStartDelay + this.autoTagBreastType = data.config.autoTag.breastType + + this.autoTagAge = data.config.autoTag.age + this.autoTagHeight = data.config.autoTag.height + this.autoTagNationality = data.config.autoTag.nationality + this.autoTagEthnicity = data.config.autoTag.ethnicity + this.autoTagHairColor = data.config.autoTag.hairColor + this.autoTagEyeColor = data.config.autoTag.eyeColor + this.autoTagCupSize = data.config.autoTag.cupSize + this.autoTagResolution = data.config.autoTag.resolution + this.autoTagVideoFormat = data.config.autoTag.videoFormat + this.autoTagDuration = data.config.autoTag.duration + + this.autoTagInterracial = data.config.autoTag.interracial + this.autoTagGenderFilter = data.config.autoTag.genderFilter || [] + this.autoTagHeightShortMax = data.config.autoTag.heightShortMax + this.autoTagHeightAverageMax = data.config.autoTag.heightAverageMax + this.autoTagDurationShortMax = data.config.autoTag.durationShortMax + this.autoTagDurationStandardMax = data.config.autoTag.durationStandardMax this.rescrapeStartDelay = data.config.cron.rescrapeSchedule.runAtStartDelay this.rescanStartDelay = data.config.cron.rescanSchedule.runAtStartDelay @@ -516,7 +869,33 @@ export default { linkScenesMinuteStart: this.linkScenesMinuteStart, linkScenesHourStart: this.linkScenesTimeRange[0], linkScenesHourEnd: this.linkScenesTimeRange[1], - linkScenesStartDelay:this.linkScenesStartDelay + linkScenesStartDelay:this.linkScenesStartDelay, + autoTagScheduleEnabled: this.autoTagScheduleEnabled, + autoTagScheduleHourInterval: this.autoTagScheduleHourInterval, + autoTagScheduleUseRange: this.useAutoTagScheduleTimeRange, + autoTagScheduleMinuteStart: this.autoTagScheduleMinuteStart, + autoTagScheduleHourStart: this.autoTagScheduleTimeRange[0], + autoTagScheduleHourEnd: this.autoTagScheduleTimeRange[1], + autoTagScheduleStartDelay:this.autoTagScheduleStartDelay, + autoTagBreastType: this.autoTagBreastType, + + autoTagAge: this.autoTagAge, + autoTagHeight: this.autoTagHeight, + autoTagNationality: this.autoTagNationality, + autoTagEthnicity: this.autoTagEthnicity, + autoTagHairColor: this.autoTagHairColor, + autoTagEyeColor: this.autoTagEyeColor, + autoTagCupSize: this.autoTagCupSize, + autoTagResolution: this.autoTagResolution, + autoTagVideoFormat: this.autoTagVideoFormat, + autoTagDuration: this.autoTagDuration, + + autoTagInterracial: this.autoTagInterracial, + autoTagGenderFilter: this.autoTagGenderFilter, + autoTagHeightShortMax: parseInt(this.autoTagHeightShortMax), + autoTagHeightAverageMax: parseInt(this.autoTagHeightAverageMax), + autoTagDurationShortMax: parseInt(this.autoTagDurationShortMax), + autoTagDurationStandardMax: parseInt(this.autoTagDurationStandardMax) } }) .json()