mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
Merge tags functionality (#1481)
* Add API to merge tags Add new API endpoint, `tagsMerge(source, destination)` to merge multiple tags into a single one. The "sources" must be provided as a list of ids and the destination as a single id. All usages of the source tags (scenes, markers (primary and additional), images, galleries and performers) will be updated to the destination tag, all aliases of the source tags will be updated to the destination, and the name of the source will be added as alias to the destination as well. * Add merge tag UI * Add unit tests * Update test mocks * Update internationalisation * Add changelog entry Co-authored-by: gitgiggety <gitgiggety@outlook.com>
This commit is contained in:
@@ -5,6 +5,7 @@ package sqlite_test
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
@@ -342,6 +343,16 @@ func withTxn(f func(r models.Repository) error) error {
|
||||
return t.WithTxn(context.TODO(), f)
|
||||
}
|
||||
|
||||
func withRollbackTxn(f func(r models.Repository) error) error {
|
||||
var ret error
|
||||
withTxn(func(repo models.Repository) error {
|
||||
ret = f(repo)
|
||||
return errors.New("fake error for rollback")
|
||||
})
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func testTeardown(databaseFile string) {
|
||||
err := database.DB.Close()
|
||||
|
||||
|
||||
@@ -536,3 +536,64 @@ func (qb *tagQueryBuilder) GetAliases(tagID int) ([]string, error) {
|
||||
func (qb *tagQueryBuilder) UpdateAliases(tagID int, aliases []string) error {
|
||||
return qb.aliasRepository().replace(tagID, aliases)
|
||||
}
|
||||
|
||||
func (qb *tagQueryBuilder) Merge(source []int, destination int) error {
|
||||
if len(source) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
inBinding := getInBinding(len(source))
|
||||
|
||||
args := []interface{}{destination}
|
||||
for _, id := range source {
|
||||
if id == destination {
|
||||
return errors.New("cannot merge where source == destination")
|
||||
}
|
||||
args = append(args, id)
|
||||
}
|
||||
|
||||
tagTables := map[string]string{
|
||||
scenesTagsTable: sceneIDColumn,
|
||||
"scene_markers_tags": "scene_marker_id",
|
||||
galleriesTagsTable: galleryIDColumn,
|
||||
imagesTagsTable: imageIDColumn,
|
||||
"performers_tags": "performer_id",
|
||||
}
|
||||
|
||||
tagArgs := append(args, destination)
|
||||
for table, idColumn := range tagTables {
|
||||
_, err := qb.tx.Exec(`UPDATE `+table+`
|
||||
SET tag_id = ?
|
||||
WHERE tag_id IN `+inBinding+`
|
||||
AND NOT EXISTS(SELECT 1 FROM `+table+` o WHERE o.`+idColumn+` = `+table+`.`+idColumn+` AND o.tag_id = ?)`,
|
||||
tagArgs...,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err := qb.tx.Exec("UPDATE "+sceneMarkerTable+" SET primary_tag_id = ? WHERE primary_tag_id IN "+inBinding, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = qb.tx.Exec("INSERT INTO "+tagAliasesTable+" (tag_id, alias) SELECT ?, name FROM "+tagTable+" WHERE id IN "+inBinding, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = qb.tx.Exec("UPDATE "+tagAliasesTable+" SET tag_id = ? WHERE tag_id IN "+inBinding, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, id := range source {
|
||||
err = qb.Destroy(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -600,6 +600,116 @@ func TestTagUpdateAlias(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagMerge(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// merge tests - perform these in a transaction that we'll rollback
|
||||
if err := withRollbackTxn(func(r models.Repository) error {
|
||||
qb := r.Tag()
|
||||
|
||||
// try merging into same tag
|
||||
err := qb.Merge([]int{tagIDs[tagIdx1WithScene]}, tagIDs[tagIdx1WithScene])
|
||||
assert.NotNil(err)
|
||||
|
||||
// merge everything into tagIdxWithScene
|
||||
srcIdxs := []int{
|
||||
tagIdx1WithScene,
|
||||
tagIdx2WithScene,
|
||||
tagIdxWithPrimaryMarker,
|
||||
tagIdxWithMarker,
|
||||
tagIdxWithCoverImage,
|
||||
tagIdxWithImage,
|
||||
tagIdx1WithImage,
|
||||
tagIdx2WithImage,
|
||||
tagIdxWithPerformer,
|
||||
tagIdx1WithPerformer,
|
||||
tagIdx2WithPerformer,
|
||||
tagIdxWithGallery,
|
||||
tagIdx1WithGallery,
|
||||
tagIdx2WithGallery,
|
||||
}
|
||||
var srcIDs []int
|
||||
for _, idx := range srcIdxs {
|
||||
srcIDs = append(srcIDs, tagIDs[idx])
|
||||
}
|
||||
|
||||
destID := tagIDs[tagIdxWithScene]
|
||||
if err = qb.Merge(srcIDs, destID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ensure other tags are deleted
|
||||
for _, tagId := range srcIDs {
|
||||
t, err := qb.Find(tagId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assert.Nil(t)
|
||||
}
|
||||
|
||||
// ensure aliases are set on the destination
|
||||
destAliases, err := qb.GetAliases(destID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, tagIdx := range srcIdxs {
|
||||
assert.Contains(destAliases, getTagStringValue(tagIdx, "Name"))
|
||||
}
|
||||
|
||||
// ensure scene points to new tag
|
||||
sceneTagIDs, err := r.Scene().GetTagIDs(sceneIDs[sceneIdxWithTwoTags])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assert.Contains(sceneTagIDs, destID)
|
||||
|
||||
// ensure marker points to new tag
|
||||
marker, err := r.SceneMarker().Find(markerIDs[markerIdxWithScene])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assert.Equal(destID, marker.PrimaryTagID)
|
||||
|
||||
markerTagIDs, err := r.SceneMarker().GetTagIDs(marker.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assert.Contains(markerTagIDs, destID)
|
||||
|
||||
// ensure image points to new tag
|
||||
imageTagIDs, err := r.Image().GetTagIDs(imageIDs[imageIdxWithTwoTags])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assert.Contains(imageTagIDs, destID)
|
||||
|
||||
// ensure gallery points to new tag
|
||||
galleryTagIDs, err := r.Gallery().GetTagIDs(galleryIDs[galleryIdxWithTwoTags])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assert.Contains(galleryTagIDs, destID)
|
||||
|
||||
// ensure performer points to new tag
|
||||
performerTagIDs, err := r.Gallery().GetTagIDs(performerIDs[performerIdxWithTwoTags])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assert.Contains(performerTagIDs, destID)
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Create
|
||||
// TODO Update
|
||||
// TODO Destroy
|
||||
|
||||
Reference in New Issue
Block a user