mirror of https://github.com/stashapp/stash.git
Merge pull request #146 from endqwerty/add_clean_func
Add functionality to the Clean Button
This commit is contained in:
commit
ccde317a56
|
@ -28,5 +28,6 @@ func (r *queryResolver) MetadataGenerate(ctx context.Context, input models.Gener
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) MetadataClean(ctx context.Context) (string, error) {
|
func (r *queryResolver) MetadataClean(ctx context.Context) (string, error) {
|
||||||
panic("not implemented")
|
manager.GetInstance().Clean()
|
||||||
|
return "todo", nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,6 +133,41 @@ func (s *singleton) Generate(sprites bool, previews bool, markers bool, transcod
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *singleton) Clean() {
|
||||||
|
if s.Status != Idle {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Status = Clean
|
||||||
|
|
||||||
|
qb := models.NewSceneQueryBuilder()
|
||||||
|
go func() {
|
||||||
|
defer s.returnToIdleState()
|
||||||
|
|
||||||
|
logger.Infof("Starting cleaning of tracked files")
|
||||||
|
scenes, err := qb.All()
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("failed to fetch list of scenes for cleaning")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, scene := range scenes {
|
||||||
|
if scene == nil {
|
||||||
|
logger.Errorf("nil scene, skipping generate")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
task := CleanTask{Scene: *scene}
|
||||||
|
go task.Start(&wg)
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Finished Cleaning")
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *singleton) returnToIdleState() {
|
func (s *singleton) returnToIdleState() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
logger.Info("recovered from ", r)
|
logger.Info("recovered from ", r)
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/stashapp/stash/pkg/database"
|
||||||
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CleanTask struct {
|
||||||
|
Scene models.Scene
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *CleanTask) Start(wg *sync.WaitGroup) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
if t.fileExists(t.Scene.Path) {
|
||||||
|
logger.Debugf("File Found: %s", t.Scene.Path)
|
||||||
|
} else {
|
||||||
|
logger.Infof("File not found. Cleaning: %s", t.Scene.Path)
|
||||||
|
t.deleteScene(t.Scene.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *CleanTask) deleteScene(sceneID int) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
qb := models.NewSceneQueryBuilder()
|
||||||
|
jqb := models.NewJoinsQueryBuilder()
|
||||||
|
tx := database.DB.MustBeginTx(ctx, nil)
|
||||||
|
strSceneID := strconv.Itoa(sceneID)
|
||||||
|
defer tx.Commit()
|
||||||
|
|
||||||
|
//check and make sure it still exists. scene is also used to delete generated files
|
||||||
|
scene, err := qb.Find(sceneID)
|
||||||
|
if err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := jqb.DestroyScenesTags(sceneID, tx); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := jqb.DestroyPerformersScenes(sceneID, tx); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := jqb.DestroyScenesMarkers(sceneID, tx); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := jqb.DestroyScenesGalleries(sceneID, tx); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := qb.Destroy(strSceneID, tx); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}
|
||||||
|
|
||||||
|
t.deleteGeneratedSceneFiles(scene)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (t *CleanTask) deleteGeneratedSceneFiles(scene *models.Scene) {
|
||||||
|
markersFolder := filepath.Join(instance.Paths.Generated.Markers, scene.Checksum)
|
||||||
|
|
||||||
|
exists, _ := utils.FileExists(markersFolder)
|
||||||
|
if exists {
|
||||||
|
err := os.RemoveAll(markersFolder)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("Could not delete file %s: %s", scene.Path, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
thumbPath := instance.Paths.Scene.GetThumbnailScreenshotPath(scene.Checksum)
|
||||||
|
exists, _ = utils.FileExists(thumbPath)
|
||||||
|
if exists {
|
||||||
|
err := os.Remove(thumbPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("Could not delete file %s: %s", thumbPath, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
screenshotPath := instance.Paths.Scene.GetScreenshotPath(scene.Checksum)
|
||||||
|
exists, _ = utils.FileExists(screenshotPath)
|
||||||
|
if exists {
|
||||||
|
err := os.Remove(screenshotPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("Could not delete file %s: %s", screenshotPath, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
streamPreviewPath := instance.Paths.Scene.GetStreamPreviewPath(scene.Checksum)
|
||||||
|
exists, _ = utils.FileExists(streamPreviewPath)
|
||||||
|
if exists {
|
||||||
|
err := os.Remove(streamPreviewPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("Could not delete file %s: %s", streamPreviewPath, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
streamPreviewImagePath := instance.Paths.Scene.GetStreamPreviewImagePath(scene.Checksum)
|
||||||
|
exists, _ = utils.FileExists(streamPreviewImagePath)
|
||||||
|
if exists {
|
||||||
|
err := os.Remove(streamPreviewImagePath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("Could not delete file %s: %s", streamPreviewImagePath, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transcodePath := instance.Paths.Scene.GetTranscodePath(scene.Checksum)
|
||||||
|
exists, _ = utils.FileExists(transcodePath)
|
||||||
|
if exists {
|
||||||
|
err := os.Remove(transcodePath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("Could not delete file %s: %s", transcodePath, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spritePath := instance.Paths.Scene.GetSpriteImageFilePath(scene.Checksum)
|
||||||
|
exists, _ = utils.FileExists(spritePath)
|
||||||
|
if exists {
|
||||||
|
err := os.Remove(spritePath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("Could not delete file %s: %s", spritePath, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vttPath := instance.Paths.Scene.GetSpriteVttFilePath(scene.Checksum)
|
||||||
|
exists, _ = utils.FileExists(vttPath)
|
||||||
|
if exists {
|
||||||
|
err := os.Remove(vttPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("Could not delete file %s: %s", vttPath, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *CleanTask) fileExists(filename string) bool {
|
||||||
|
info, err := os.Stat(filename)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !info.IsDir()
|
||||||
|
}
|
|
@ -33,6 +33,14 @@ func (qb *JoinsQueryBuilder) UpdatePerformersScenes(sceneID int, updatedJoins []
|
||||||
return qb.CreatePerformersScenes(updatedJoins, tx)
|
return qb.CreatePerformersScenes(updatedJoins, tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qb *JoinsQueryBuilder) DestroyPerformersScenes(sceneID int, tx *sqlx.Tx) error {
|
||||||
|
ensureTx(tx)
|
||||||
|
|
||||||
|
// Delete the existing joins
|
||||||
|
_, err := tx.Exec("DELETE FROM performers_scenes WHERE scene_id = ?", sceneID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *JoinsQueryBuilder) CreateScenesTags(newJoins []ScenesTags, tx *sqlx.Tx) error {
|
func (qb *JoinsQueryBuilder) CreateScenesTags(newJoins []ScenesTags, tx *sqlx.Tx) error {
|
||||||
ensureTx(tx)
|
ensureTx(tx)
|
||||||
for _, join := range newJoins {
|
for _, join := range newJoins {
|
||||||
|
@ -47,6 +55,16 @@ func (qb *JoinsQueryBuilder) CreateScenesTags(newJoins []ScenesTags, tx *sqlx.Tx
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qb *JoinsQueryBuilder) DestroyScenesTags(sceneID int, tx *sqlx.Tx) error {
|
||||||
|
ensureTx(tx)
|
||||||
|
|
||||||
|
// Delete the existing joins
|
||||||
|
_, err := tx.Exec("DELETE FROM scenes_tags WHERE scene_id = ?", sceneID)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func (qb *JoinsQueryBuilder) UpdateScenesTags(sceneID int, updatedJoins []ScenesTags, tx *sqlx.Tx) error {
|
func (qb *JoinsQueryBuilder) UpdateScenesTags(sceneID int, updatedJoins []ScenesTags, tx *sqlx.Tx) error {
|
||||||
ensureTx(tx)
|
ensureTx(tx)
|
||||||
|
|
||||||
|
@ -82,3 +100,32 @@ func (qb *JoinsQueryBuilder) UpdateSceneMarkersTags(sceneMarkerID int, updatedJo
|
||||||
}
|
}
|
||||||
return qb.CreateSceneMarkersTags(updatedJoins, tx)
|
return qb.CreateSceneMarkersTags(updatedJoins, tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qb *JoinsQueryBuilder) DestroySceneMarkersTags(sceneMarkerID int, updatedJoins []SceneMarkersTags, tx *sqlx.Tx) error {
|
||||||
|
ensureTx(tx)
|
||||||
|
|
||||||
|
// Delete the existing joins
|
||||||
|
_, err := tx.Exec("DELETE FROM scene_markers_tags WHERE scene_marker_id = ?", sceneMarkerID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *JoinsQueryBuilder) DestroyScenesGalleries(sceneID int, tx *sqlx.Tx) error {
|
||||||
|
ensureTx(tx)
|
||||||
|
|
||||||
|
// Unset the existing scene id from galleries
|
||||||
|
_, err := tx.Exec("UPDATE galleries SET scene_id = null WHERE scene_id = ?", sceneID)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *JoinsQueryBuilder) DestroyScenesMarkers(sceneID int, tx *sqlx.Tx) error {
|
||||||
|
ensureTx(tx)
|
||||||
|
|
||||||
|
// Delete the scene marker tags
|
||||||
|
_, err := tx.Exec("DELETE t FROM scene_markers_tags t join scene_markers m on t.scene_marker_id = m.id WHERE m.scene_id = ?", sceneID)
|
||||||
|
|
||||||
|
// Delete the existing joins
|
||||||
|
_, err = tx.Exec("DELETE FROM scene_markers WHERE scene_id = ?", sceneID)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
|
@ -74,6 +74,10 @@ func (qb *SceneQueryBuilder) Update(updatedScene ScenePartial, tx *sqlx.Tx) (*Sc
|
||||||
return qb.find(updatedScene.ID, tx)
|
return qb.find(updatedScene.ID, tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qb *SceneQueryBuilder) Destroy(id string, tx *sqlx.Tx) error {
|
||||||
|
return executeDeleteQuery("scenes", id, tx)
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *SceneQueryBuilder) Find(id int) (*Scene, error) {
|
func (qb *SceneQueryBuilder) Find(id int) (*Scene, error) {
|
||||||
return qb.find(id, nil)
|
return qb.find(id, nil)
|
||||||
}
|
}
|
||||||
|
@ -292,3 +296,4 @@ func (qb *SceneQueryBuilder) queryScenes(query string, args []interface{}, tx *s
|
||||||
|
|
||||||
return scenes, nil
|
return scenes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ interface IProps {}
|
||||||
|
|
||||||
export const SettingsTasksPanel: FunctionComponent<IProps> = (props: IProps) => {
|
export const SettingsTasksPanel: FunctionComponent<IProps> = (props: IProps) => {
|
||||||
const [isImportAlertOpen, setIsImportAlertOpen] = useState<boolean>(false);
|
const [isImportAlertOpen, setIsImportAlertOpen] = useState<boolean>(false);
|
||||||
|
const [isCleanAlertOpen, setIsCleanAlertOpen] = useState<boolean>(false);
|
||||||
const [nameFromMetadata, setNameFromMetadata] = useState<boolean>(true);
|
const [nameFromMetadata, setNameFromMetadata] = useState<boolean>(true);
|
||||||
|
|
||||||
function onImport() {
|
function onImport() {
|
||||||
|
@ -44,6 +45,31 @@ export const SettingsTasksPanel: FunctionComponent<IProps> = (props: IProps) =>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onClean() {
|
||||||
|
setIsCleanAlertOpen(false);
|
||||||
|
StashService.queryMetadataClean();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCleanAlert() {
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
cancelButtonText="Cancel"
|
||||||
|
confirmButtonText="Clean"
|
||||||
|
icon="trash"
|
||||||
|
intent="danger"
|
||||||
|
isOpen={isCleanAlertOpen}
|
||||||
|
onCancel={() => setIsCleanAlertOpen(false)}
|
||||||
|
onConfirm={() => onClean()}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Are you sure you want to Clean?
|
||||||
|
This will delete db information and generated content
|
||||||
|
for all scenes that are no longer found in the filesystem.
|
||||||
|
</p>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function onScan() {
|
async function onScan() {
|
||||||
try {
|
try {
|
||||||
await StashService.queryMetadataScan({nameFromMetadata});
|
await StashService.queryMetadataScan({nameFromMetadata});
|
||||||
|
@ -56,6 +82,7 @@ export const SettingsTasksPanel: FunctionComponent<IProps> = (props: IProps) =>
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderImportAlert()}
|
{renderImportAlert()}
|
||||||
|
{renderCleanAlert()}
|
||||||
|
|
||||||
<H4>Library</H4>
|
<H4>Library</H4>
|
||||||
<FormGroup
|
<FormGroup
|
||||||
|
@ -75,11 +102,11 @@ export const SettingsTasksPanel: FunctionComponent<IProps> = (props: IProps) =>
|
||||||
<H4>Generated Content</H4>
|
<H4>Generated Content</H4>
|
||||||
<GenerateButton />
|
<GenerateButton />
|
||||||
<FormGroup
|
<FormGroup
|
||||||
helperText="TODO"
|
helperText="Check for missing files and remove them from the database. This is a destructive action."
|
||||||
labelFor="clean"
|
labelFor="clean"
|
||||||
inline={true}
|
inline={true}
|
||||||
>
|
>
|
||||||
<Button id="clean" text="Clean" onClick={() => StashService.queryMetadataClean()} />
|
<Button id="clean" text="Clean" intent="danger" onClick={() => setIsCleanAlertOpen(true)} />
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue