mirror of
https://github.com/stashapp/stash.git
synced 2025-12-16 20:07:05 +03:00
Default view filters (#4962)
* Merge/adapt from yoshnopa:defaultDetails * Deprecate and remove default filter calls * Fix weird behaviour when clicking set as default * Update deprecated get/set default filter resolvers * Add config migration --------- Co-authored-by: yoshnopa <usingusenet@protonmail.com>
This commit is contained in:
@@ -7,13 +7,11 @@ type SavedFilterReader interface {
|
||||
Find(ctx context.Context, id int) (*SavedFilter, error)
|
||||
FindMany(ctx context.Context, ids []int, ignoreNotFound bool) ([]*SavedFilter, error)
|
||||
FindByMode(ctx context.Context, mode FilterMode) ([]*SavedFilter, error)
|
||||
FindDefault(ctx context.Context, mode FilterMode) (*SavedFilter, error)
|
||||
}
|
||||
|
||||
type SavedFilterWriter interface {
|
||||
Create(ctx context.Context, obj *SavedFilter) error
|
||||
Update(ctx context.Context, obj *SavedFilter) error
|
||||
SetDefault(ctx context.Context, obj *SavedFilter) error
|
||||
Destroy(ctx context.Context, id int) error
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ const (
|
||||
dbConnTimeout = 30
|
||||
)
|
||||
|
||||
var appSchemaVersion uint = 59
|
||||
var appSchemaVersion uint = 60
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsBox embed.FS
|
||||
|
||||
2
pkg/sqlite/migrations/60_default_filter_move.up.sql
Normal file
2
pkg/sqlite/migrations/60_default_filter_move.up.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- no schema changes
|
||||
-- default filters will be removed in post-migration
|
||||
176
pkg/sqlite/migrations/60_postmigrate.go
Normal file
176
pkg/sqlite/migrations/60_postmigrate.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/sqlite"
|
||||
)
|
||||
|
||||
type schema60Migrator struct {
|
||||
migrator
|
||||
}
|
||||
|
||||
func post60(ctx context.Context, db *sqlx.DB) error {
|
||||
logger.Info("Running post-migration for schema version 60")
|
||||
|
||||
m := schema60Migrator{
|
||||
migrator: migrator{
|
||||
db: db,
|
||||
},
|
||||
}
|
||||
|
||||
return m.migrate(ctx)
|
||||
}
|
||||
|
||||
func (m *schema60Migrator) decodeJSON(s string, v interface{}) {
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(s), v); err != nil {
|
||||
logger.Errorf("error decoding json %q: %v", s, err)
|
||||
}
|
||||
}
|
||||
|
||||
type schema60DefaultFilters map[string]interface{}
|
||||
|
||||
func (m *schema60Migrator) migrate(ctx context.Context) error {
|
||||
|
||||
// save default filters into the UI config
|
||||
if err := m.withTxn(ctx, func(tx *sqlx.Tx) error {
|
||||
query := "SELECT id, mode, find_filter, object_filter, ui_options FROM `saved_filters` WHERE `name` = ''"
|
||||
|
||||
rows, err := m.db.Query(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
defaultFilters := make(schema60DefaultFilters)
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
id int
|
||||
mode string
|
||||
findFilterStr string
|
||||
objectFilterStr string
|
||||
uiOptionsStr string
|
||||
)
|
||||
|
||||
if err := rows.Scan(&id, &mode, &findFilterStr, &objectFilterStr, &uiOptionsStr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// convert the filters to the correct format
|
||||
findFilter := make(map[string]interface{})
|
||||
objectFilter := make(map[string]interface{})
|
||||
uiOptions := make(map[string]interface{})
|
||||
|
||||
m.decodeJSON(findFilterStr, &findFilter)
|
||||
m.decodeJSON(objectFilterStr, &objectFilter)
|
||||
m.decodeJSON(uiOptionsStr, &uiOptions)
|
||||
|
||||
o := map[string]interface{}{
|
||||
"mode": mode,
|
||||
"find_filter": findFilter,
|
||||
"object_filter": objectFilter,
|
||||
"ui_options": uiOptions,
|
||||
}
|
||||
|
||||
defaultFilters[strings.ToLower(mode)] = o
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.saveDefaultFilters(defaultFilters); err != nil {
|
||||
return fmt.Errorf("saving default filters: %w", err)
|
||||
}
|
||||
|
||||
// remove the default filters from the database
|
||||
query = "DELETE FROM `saved_filters` WHERE `name` = ''"
|
||||
if _, err := m.db.Exec(query); err != nil {
|
||||
return fmt.Errorf("deleting default filters: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *schema60Migrator) saveDefaultFilters(defaultFilters schema60DefaultFilters) error {
|
||||
if len(defaultFilters) == 0 {
|
||||
logger.Debugf("no default filters to save")
|
||||
return nil
|
||||
}
|
||||
|
||||
// save the default filters into the UI config
|
||||
config := config.GetInstance()
|
||||
|
||||
orgPath := config.GetConfigFile()
|
||||
|
||||
if orgPath == "" {
|
||||
// no config file to migrate (usually in a test or new system)
|
||||
logger.Debugf("no config file to migrate")
|
||||
return nil
|
||||
}
|
||||
|
||||
uiConfig := config.GetUIConfiguration()
|
||||
if uiConfig == nil {
|
||||
uiConfig = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// if the defaultFilters key already exists, don't overwrite them
|
||||
if _, found := uiConfig["defaultFilters"]; found {
|
||||
logger.Warn("defaultFilters already exists in the UI config, skipping migration")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := m.backupConfig(orgPath); err != nil {
|
||||
return fmt.Errorf("backing up config: %w", err)
|
||||
}
|
||||
|
||||
uiConfig["defaultFilters"] = map[string]interface{}(defaultFilters)
|
||||
config.SetUIConfiguration(uiConfig)
|
||||
|
||||
if err := config.Write(); err != nil {
|
||||
return fmt.Errorf("failed to write config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *schema60Migrator) backupConfig(orgPath string) error {
|
||||
c := config.GetInstance()
|
||||
|
||||
// save a backup of the original config file
|
||||
backupPath := fmt.Sprintf("%s.59.%s", orgPath, time.Now().Format("20060102_150405"))
|
||||
|
||||
data, err := c.Marshal()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal backup config: %w", err)
|
||||
}
|
||||
|
||||
logger.Infof("Backing up config to %s", backupPath)
|
||||
if err := os.WriteFile(backupPath, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write backup config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
sqlite.RegisterPostMigration(60, post60)
|
||||
}
|
||||
@@ -141,23 +141,6 @@ func (qb *SavedFilterStore) Update(ctx context.Context, updatedObject *models.Sa
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *SavedFilterStore) SetDefault(ctx context.Context, obj *models.SavedFilter) error {
|
||||
// find the existing default
|
||||
existing, err := qb.FindDefault(ctx, obj.Mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
obj.Name = savedFilterDefaultName
|
||||
|
||||
if existing != nil {
|
||||
obj.ID = existing.ID
|
||||
return qb.Update(ctx, obj)
|
||||
}
|
||||
|
||||
return qb.Create(ctx, obj)
|
||||
}
|
||||
|
||||
func (qb *SavedFilterStore) Destroy(ctx context.Context, id int) error {
|
||||
return qb.destroyExisting(ctx, []int{id})
|
||||
}
|
||||
@@ -258,22 +241,6 @@ func (qb *SavedFilterStore) FindByMode(ctx context.Context, mode models.FilterMo
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (qb *SavedFilterStore) FindDefault(ctx context.Context, mode models.FilterMode) (*models.SavedFilter, error) {
|
||||
// SELECT * FROM saved_filters WHERE mode = ? AND name = ?
|
||||
table := qb.table()
|
||||
sq := qb.selectDataset().Prepared(true).Where(
|
||||
table.Col("mode").Eq(mode),
|
||||
table.Col("name").Eq(savedFilterDefaultName),
|
||||
)
|
||||
|
||||
ret, err := qb.get(ctx, sq)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (qb *SavedFilterStore) All(ctx context.Context) ([]*models.SavedFilter, error) {
|
||||
return qb.getMany(ctx, qb.selectDataset())
|
||||
}
|
||||
|
||||
@@ -96,66 +96,6 @@ func TestSavedFilterDestroy(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestSavedFilterFindDefault(t *testing.T) {
|
||||
withTxn(func(ctx context.Context) error {
|
||||
def, err := db.SavedFilter.FindDefault(ctx, models.FilterModeScenes)
|
||||
if err == nil {
|
||||
assert.Equal(t, savedFilterIDs[savedFilterIdxDefaultScene], def.ID)
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func TestSavedFilterSetDefault(t *testing.T) {
|
||||
filterQ := ""
|
||||
filterPage := 1
|
||||
filterPerPage := 40
|
||||
filterSort := "date"
|
||||
filterDirection := models.SortDirectionEnumAsc
|
||||
findFilter := models.FindFilterType{
|
||||
Q: &filterQ,
|
||||
Page: &filterPage,
|
||||
PerPage: &filterPerPage,
|
||||
Sort: &filterSort,
|
||||
Direction: &filterDirection,
|
||||
}
|
||||
objectFilter := map[string]interface{}{
|
||||
"test": "foo",
|
||||
}
|
||||
uiOptions := map[string]interface{}{
|
||||
"display_mode": 1,
|
||||
"zoom_index": 1,
|
||||
}
|
||||
|
||||
withTxn(func(ctx context.Context) error {
|
||||
err := db.SavedFilter.SetDefault(ctx, &models.SavedFilter{
|
||||
Mode: models.FilterModeMovies,
|
||||
FindFilter: &findFilter,
|
||||
ObjectFilter: objectFilter,
|
||||
UIOptions: uiOptions,
|
||||
})
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
var defID int
|
||||
withTxn(func(ctx context.Context) error {
|
||||
def, err := db.SavedFilter.FindDefault(ctx, models.FilterModeMovies)
|
||||
if err == nil {
|
||||
defID = def.ID
|
||||
assert.Equal(t, &findFilter, def.FindFilter)
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
// destroy it again
|
||||
withTxn(func(ctx context.Context) error {
|
||||
return db.SavedFilter.Destroy(ctx, defID)
|
||||
})
|
||||
}
|
||||
|
||||
// TODO Update
|
||||
// TODO Destroy
|
||||
// TODO Find
|
||||
|
||||
Reference in New Issue
Block a user