Selective scan (#940)

This commit is contained in:
WithoutPants
2020-11-16 09:20:04 +11:00
committed by GitHub
parent ba8b3b29a4
commit 0a098b1d63
12 changed files with 279 additions and 97 deletions

View File

@@ -31,6 +31,7 @@ input GeneratePreviewOptionsInput {
}
input ScanMetadataInput {
paths: [String!]
useFileMetadata: Boolean!
}

View File

@@ -10,7 +10,7 @@ import (
)
func (r *mutationResolver) MetadataScan(ctx context.Context, input models.ScanMetadataInput) (string, error) {
manager.GetInstance().Scan(input.UseFileMetadata)
manager.GetInstance().Scan(input)
return "todo", nil
}

View File

@@ -78,7 +78,29 @@ func (t *TaskStatus) updated() {
t.LastUpdate = time.Now()
}
func (s *singleton) neededScan() (total *int, newFiles *int) {
func getScanPaths(inputPaths []string) []*models.StashConfig {
if len(inputPaths) == 0 {
return config.GetStashPaths()
}
var ret []*models.StashConfig
for _, p := range inputPaths {
s := getStashFromDirPath(p)
if s == nil {
logger.Warnf("%s is not in the configured stash paths", p)
continue
}
// make a copy, changing the path
ss := *s
ss.Path = p
ret = append(ret, &ss)
}
return ret
}
func (s *singleton) neededScan(paths []*models.StashConfig) (total *int, newFiles *int) {
const timeout = 90 * time.Second
// create a control channel through which to signal the counting loop when the timeout is reached
@@ -91,7 +113,7 @@ func (s *singleton) neededScan() (total *int, newFiles *int) {
timeoutErr := errors.New("timed out")
for _, sp := range config.GetStashPaths() {
for _, sp := range paths {
err := walkFilesToScan(sp, func(path string, info os.FileInfo, err error) error {
t++
task := ScanTask{FilePath: path}
@@ -128,7 +150,7 @@ func (s *singleton) neededScan() (total *int, newFiles *int) {
return &t, &n
}
func (s *singleton) Scan(useFileMetadata bool) {
func (s *singleton) Scan(input models.ScanMetadataInput) {
if s.Status.Status != Idle {
return
}
@@ -138,7 +160,9 @@ func (s *singleton) Scan(useFileMetadata bool) {
go func() {
defer s.returnToIdleState()
total, newFiles := s.neededScan()
paths := getScanPaths(input.Paths)
total, newFiles := s.neededScan(paths)
if s.Status.stopping {
logger.Info("Stopping due to user request")
@@ -162,7 +186,7 @@ func (s *singleton) Scan(useFileMetadata bool) {
var galleries []string
for _, sp := range config.GetStashPaths() {
for _, sp := range paths {
err := walkFilesToScan(sp, func(path string, info os.FileInfo, err error) error {
if total != nil {
s.Status.setProgress(i, *total)
@@ -178,7 +202,7 @@ func (s *singleton) Scan(useFileMetadata bool) {
}
wg.Add(1)
task := ScanTask{FilePath: path, UseFileMetadata: useFileMetadata, fileNamingAlgorithm: fileNamingAlgo, calculateMD5: calculateMD5}
task := ScanTask{FilePath: path, UseFileMetadata: input.UseFileMetadata, fileNamingAlgorithm: fileNamingAlgo, calculateMD5: calculateMD5}
go task.Start(&wg)
wg.Wait()

View File

@@ -41,7 +41,7 @@ func (t *CleanTask) shouldClean(path string) bool {
// use image.FileExists for zip file checking
fileExists := image.FileExists(path)
if !fileExists || t.getStashFromPath(path) == nil {
if !fileExists || getStashFromPath(path) == nil {
logger.Infof("File not found. Cleaning: \"%s\"", path)
return true
}
@@ -54,7 +54,7 @@ func (t *CleanTask) shouldCleanScene(s *models.Scene) bool {
return true
}
stash := t.getStashFromPath(s.Path)
stash := getStashFromPath(s.Path)
if stash.ExcludeVideo {
logger.Infof("File in stash library that excludes video. Cleaning: \"%s\"", s.Path)
return true
@@ -84,7 +84,7 @@ func (t *CleanTask) shouldCleanGallery(g *models.Gallery) bool {
return true
}
stash := t.getStashFromPath(path)
stash := getStashFromPath(path)
if stash.ExcludeImage {
logger.Infof("File in stash library that excludes images. Cleaning: \"%s\"", path)
return true
@@ -113,7 +113,7 @@ func (t *CleanTask) shouldCleanImage(s *models.Image) bool {
return true
}
stash := t.getStashFromPath(s.Path)
stash := getStashFromPath(s.Path)
if stash.ExcludeImage {
logger.Infof("File in stash library that excludes images. Cleaning: \"%s\"", s.Path)
return true
@@ -211,9 +211,8 @@ func (t *CleanTask) fileExists(filename string) (bool, error) {
return !info.IsDir(), nil
}
func (t *CleanTask) getStashFromPath(pathToCheck string) *models.StashConfig {
func getStashFromPath(pathToCheck string) *models.StashConfig {
for _, s := range config.GetStashPaths() {
rel, error := filepath.Rel(s.Path, filepath.Dir(pathToCheck))
if error == nil {
@@ -225,3 +224,17 @@ func (t *CleanTask) getStashFromPath(pathToCheck string) *models.StashConfig {
}
return nil
}
func getStashFromDirPath(pathToCheck string) *models.StashConfig {
for _, s := range config.GetStashPaths() {
rel, error := filepath.Rel(s.Path, pathToCheck)
if error == nil {
if !strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return s
}
}
}
return nil
}

View File

@@ -114,11 +114,7 @@ func ListDir(path string) []string {
if !file.IsDir() {
continue
}
abs, err := filepath.Abs(path)
if err != nil {
continue
}
dirPaths = append(dirPaths, filepath.Join(abs, file.Name()))
dirPaths = append(dirPaths, filepath.Join(path, file.Name()))
}
return dirPaths
}
@@ -220,11 +216,7 @@ func GetDir(path string) string {
path = GetHomeDirectory()
}
absolutePath, err := filepath.Abs(path)
if err == nil {
path = absolutePath
}
return absolutePath
return path
}
func GetParent(path string) *string {

View File

@@ -1,4 +1,5 @@
### ✨ New Features
* Add selective scan.
* Add selective export of all objects.
* Add stash-box tagger to scenes page.
* Add filters tab in scene page.

View File

@@ -0,0 +1,85 @@
import React, { useState } from "react";
import { Button, Col, Form, Row } from "react-bootstrap";
import { useConfiguration } from "src/core/StashService";
import { Icon, Modal } from "src/components/Shared";
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
interface IScanDialogProps {
onClose: (paths?: string[]) => void;
}
export const ScanDialog: React.FC<IScanDialogProps> = (
props: IScanDialogProps
) => {
const { data } = useConfiguration();
const libraryPaths = data?.configuration.general.stashes.map((s) => s.path);
const [paths, setPaths] = useState<string[]>([]);
const [currentDirectory, setCurrentDirectory] = useState<string>("");
function removePath(p: string) {
setPaths(paths.filter((path) => path !== p));
}
function addPath(p: string) {
if (p && !paths.includes(p)) {
setPaths(paths.concat(p));
}
}
return (
<Modal
show
disabled={paths.length === 0}
icon="pencil-alt"
header="Select folders to scan"
accept={{
onClick: () => {
props.onClose(paths);
},
text: "Scan",
}}
cancel={{
onClick: () => props.onClose(),
text: "Cancel",
variant: "secondary",
}}
>
<div className="dialog-container">
{paths.map((p) => (
<Row className="align-items-center mb-1">
<Form.Label column xs={10}>
{p}
</Form.Label>
<Col xs={2} className="d-flex justify-content-end">
<Button
className="ml-auto"
size="sm"
variant="danger"
title="Delete"
onClick={() => removePath(p)}
>
<Icon icon="minus" />
</Button>
</Col>
</Row>
))}
<FolderSelect
currentDirectory={currentDirectory}
setCurrentDirectory={(v) => setCurrentDirectory(v)}
defaultDirectories={libraryPaths}
appendButton={
<Button
variant="secondary"
onClick={() => addPath(currentDirectory)}
>
<Icon icon="plus" />
</Button>
}
/>
</div>
</Modal>
);
};

View File

@@ -19,6 +19,7 @@ import * as GQL from "src/core/generated-graphql";
import { Modal } from "src/components/Shared";
import { GenerateButton } from "./GenerateButton";
import { ImportDialog } from "./ImportDialog";
import { ScanDialog } from "./ScanDialog";
type Plugin = Pick<GQL.Plugin, "id">;
type PluginTask = Pick<GQL.PluginTask, "name" | "description">;
@@ -28,6 +29,7 @@ export const SettingsTasksPanel: React.FC = () => {
const [isImportAlertOpen, setIsImportAlertOpen] = useState<boolean>(false);
const [isCleanAlertOpen, setIsCleanAlertOpen] = useState<boolean>(false);
const [isImportDialogOpen, setIsImportDialogOpen] = useState<boolean>(false);
const [isScanDialogOpen, setIsScanDialogOpen] = useState<boolean>(false);
const [useFileMetadata, setUseFileMetadata] = useState<boolean>(false);
const [status, setStatus] = useState<string>("");
const [progress, setProgress] = useState<number>(0);
@@ -145,9 +147,28 @@ export const SettingsTasksPanel: React.FC = () => {
return <ImportDialog onClose={() => setIsImportDialogOpen(false)} />;
}
async function onScan() {
function renderScanDialog() {
if (!isScanDialogOpen) {
return;
}
return <ScanDialog onClose={onScanDialogClosed} />;
}
function onScanDialogClosed(paths?: string[]) {
if (paths) {
onScan(paths);
}
setIsScanDialogOpen(false);
}
async function onScan(paths?: string[]) {
try {
await mutateMetadataScan({ useFileMetadata });
await mutateMetadataScan({
useFileMetadata,
paths,
});
Toast.success({ content: "Started scan" });
jobStatus.refetch();
} catch (e) {
@@ -267,6 +288,7 @@ export const SettingsTasksPanel: React.FC = () => {
{renderImportAlert()}
{renderCleanAlert()}
{renderImportDialog()}
{renderScanDialog()}
<h4>Running Jobs</h4>
@@ -284,9 +306,21 @@ export const SettingsTasksPanel: React.FC = () => {
/>
</Form.Group>
<Form.Group>
<Button variant="secondary" type="submit" onClick={() => onScan()}>
<Button
className="mr-2"
variant="secondary"
type="submit"
onClick={() => onScan()}
>
Scan
</Button>
<Button
variant="secondary"
type="submit"
onClick={() => setIsScanDialogOpen(true)}
>
Selective Scan
</Button>
<Form.Text className="text-muted">
Scan for new content and add it to the database.
</Form.Text>

View File

@@ -2,7 +2,7 @@ import React, { useState } from "react";
import { Button, Form, Row, Col } from "react-bootstrap";
import { Icon } from "src/components/Shared";
import * as GQL from "src/core/generated-graphql";
import { FolderSelect } from "../Shared/FolderSelect/FolderSelect";
import { FolderSelectDialog } from "../Shared/FolderSelect/FolderSelectDialog";
interface IStashProps {
index: number;
@@ -94,7 +94,7 @@ export const StashConfiguration: React.FC<IStashConfigurationProps> = ({
return;
}
return <FolderSelect onClose={handleAdd} />;
return <FolderSelectDialog onClose={handleAdd} />;
}
return (

View File

@@ -1,42 +1,52 @@
import React, { useEffect, useState } from "react";
import React, { useEffect } from "react";
import { FormattedMessage } from "react-intl";
import { Button, InputGroup, Form, Modal } from "react-bootstrap";
import { Button, InputGroup, Form } from "react-bootstrap";
import { LoadingIndicator } from "src/components/Shared";
import { useDirectory } from "src/core/StashService";
interface IProps {
onClose: (directory?: string) => void;
currentDirectory: string;
setCurrentDirectory: (value: string) => void;
defaultDirectories?: string[];
appendButton?: JSX.Element;
}
export const FolderSelect: React.FC<IProps> = (props: IProps) => {
const [currentDirectory, setCurrentDirectory] = useState<string>("");
export const FolderSelect: React.FC<IProps> = ({
currentDirectory,
setCurrentDirectory,
defaultDirectories,
appendButton,
}) => {
const { data, error, loading } = useDirectory(currentDirectory);
const selectableDirectories: string[] = currentDirectory
? data?.directory.directories ?? defaultDirectories ?? []
: defaultDirectories ?? [];
useEffect(() => {
if (currentDirectory === "" && data?.directory.path)
if (currentDirectory === "" && !defaultDirectories && data?.directory.path)
setCurrentDirectory(data.directory.path);
}, [currentDirectory, data]);
}, [currentDirectory, setCurrentDirectory, data, defaultDirectories]);
const selectableDirectories: string[] = data?.directory.directories ?? [];
const topDirectory = data?.directory?.parent ? (
<li className="folder-list-parent folder-list-item">
<Button
variant="link"
onClick={() =>
data.directory.parent && setCurrentDirectory(data.directory.parent)
function goUp() {
if (defaultDirectories?.includes(currentDirectory)) {
setCurrentDirectory("");
} else if (data?.directory.parent) {
setCurrentDirectory(data.directory.parent);
}
>
}
const topDirectory =
currentDirectory && data?.directory?.parent ? (
<li className="folder-list-parent folder-list-item">
<Button variant="link" onClick={() => goUp()}>
<FormattedMessage defaultMessage="Up a directory" id="up-dir" />
</Button>
</li>
) : null;
return (
<Modal show onHide={() => props.onClose()} title="">
<Modal.Header>Select Directory</Modal.Header>
<Modal.Body>
<div className="dialog-content">
<>
{error ? <h1>{error.message}</h1> : ""}
<InputGroup>
<Form.Control
@@ -47,39 +57,27 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
value={currentDirectory}
spellCheck={false}
/>
<InputGroup.Append>
{appendButton ? (
<InputGroup.Append>{appendButton}</InputGroup.Append>
) : undefined}
{!data || !data.directory || loading ? (
<LoadingIndicator inline />
) : (
""
)}
<InputGroup.Append>
<LoadingIndicator inline small message="" />
</InputGroup.Append>
) : undefined}
</InputGroup>
<ul className="folder-list">
{topDirectory}
{selectableDirectories.map((path) => {
return (
<li key={path} className="folder-list-item">
<Button
variant="link"
onClick={() => setCurrentDirectory(path)}
>
<Button variant="link" onClick={() => setCurrentDirectory(path)}>
{path}
</Button>
</li>
);
})}
</ul>
</div>
</Modal.Body>
<Modal.Footer>
<Button
variant="success"
onClick={() => props.onClose(currentDirectory)}
>
Add
</Button>
</Modal.Footer>
</Modal>
</>
);
};

View File

@@ -0,0 +1,33 @@
import React, { useState } from "react";
import { Button, Modal } from "react-bootstrap";
import { FolderSelect } from "./FolderSelect";
interface IProps {
onClose: (directory?: string) => void;
}
export const FolderSelectDialog: React.FC<IProps> = (props: IProps) => {
const [currentDirectory, setCurrentDirectory] = useState<string>("");
return (
<Modal show onHide={() => props.onClose()} title="">
<Modal.Header>Select Directory</Modal.Header>
<Modal.Body>
<div className="dialog-content">
<FolderSelect
currentDirectory={currentDirectory}
setCurrentDirectory={(v) => setCurrentDirectory(v)}
/>
</div>
</Modal.Body>
<Modal.Footer>
<Button
variant="success"
onClick={() => props.onClose(currentDirectory)}
>
Add
</Button>
</Modal.Footer>
</Modal>
);
};

View File

@@ -16,7 +16,8 @@
}
&.inline {
display: inline;
display: inline-block;
height: auto;
margin-left: 0.5rem;
}