Generate cover image (#376)

* Make mutating metadata ops mutation
* Implement scene generate screenshot
* Remove fetch policy on metadata mutations
* Port UI changes to v2.5
* Set generated image in database
This commit is contained in:
WithoutPants 2020-03-12 08:34:04 +11:00 committed by GitHub
parent 34d829338d
commit 3de6955a9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 442 additions and 168 deletions

View file

@ -0,0 +1,27 @@
mutation MetadataImport {
metadataImport
}
mutation MetadataExport {
metadataExport
}
mutation MetadataScan($input: ScanMetadataInput!) {
metadataScan(input: $input)
}
mutation MetadataGenerate($input: GenerateMetadataInput!) {
metadataGenerate(input: $input)
}
mutation MetadataAutoTag($input: AutoTagMetadataInput!) {
metadataAutoTag(input: $input)
}
mutation MetadataClean {
metadataClean
}
mutation StopJob {
stopJob
}

View file

@ -78,4 +78,8 @@ mutation SceneResetO($id: ID!) {
mutation SceneDestroy($id: ID!, $delete_file: Boolean, $delete_generated : Boolean) {
sceneDestroy(input: {id: $id, delete_file: $delete_file, delete_generated: $delete_generated})
}
}
mutation SceneGenerateScreenshot($id: ID!, $at: Float) {
sceneGenerateScreenshot(id: $id, at: $at)
}

View file

@ -1,27 +1,3 @@
query MetadataImport {
metadataImport
}
query MetadataExport {
metadataExport
}
query MetadataScan($input: ScanMetadataInput!) {
metadataScan(input: $input)
}
query MetadataGenerate($input: GenerateMetadataInput!) {
metadataGenerate(input: $input)
}
query MetadataAutoTag($input: AutoTagMetadataInput!) {
metadataAutoTag(input: $input)
}
query MetadataClean {
metadataClean
}
query JobStatus {
jobStatus {
progress
@ -29,7 +5,3 @@ query JobStatus {
message
}
}
query StopJob {
stopJob
}

View file

@ -77,21 +77,7 @@ type Query {
# Metadata
"""Start an import. Returns the job ID"""
metadataImport: String!
"""Start an export. Returns the job ID"""
metadataExport: String!
"""Start a scan. Returns the job ID"""
metadataScan(input: ScanMetadataInput!): String!
"""Start generating content. Returns the job ID"""
metadataGenerate(input: GenerateMetadataInput!): String!
"""Start auto-tagging. Returns the job ID"""
metadataAutoTag(input: AutoTagMetadataInput!): String!
"""Clean metadata. Returns the job ID"""
metadataClean: String!
jobStatus: MetadataUpdateStatus!
stopJob: Boolean!
# Get everything
@ -120,6 +106,9 @@ type Mutation {
"""Resets the o-counter for a scene to 0. Returns the new value"""
sceneResetO(id: ID!): Int!
"""Generates screenshot at specified time in seconds. Leave empty to generate default screenshot"""
sceneGenerateScreenshot(id: ID!, at: Float): String!
sceneMarkerCreate(input: SceneMarkerCreateInput!): SceneMarker
sceneMarkerUpdate(input: SceneMarkerUpdateInput!): SceneMarker
sceneMarkerDestroy(id: ID!): Boolean!
@ -143,6 +132,21 @@ type Mutation {
"""Change general configuration options"""
configureGeneral(input: ConfigGeneralInput!): ConfigGeneralResult!
configureInterface(input: ConfigInterfaceInput!): ConfigInterfaceResult!
"""Start an import. Returns the job ID"""
metadataImport: String!
"""Start an export. Returns the job ID"""
metadataExport: String!
"""Start a scan. Returns the job ID"""
metadataScan(input: ScanMetadataInput!): String!
"""Start generating content. Returns the job ID"""
metadataGenerate(input: GenerateMetadataInput!): String!
"""Start auto-tagging. Returns the job ID"""
metadataAutoTag(input: AutoTagMetadataInput!): String!
"""Clean metadata. Returns the job ID"""
metadataClean: String!
stopJob: Boolean!
}
type Subscription {

View file

@ -0,0 +1,53 @@
package api
import (
"context"
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
)
func (r *mutationResolver) MetadataScan(ctx context.Context, input models.ScanMetadataInput) (string, error) {
manager.GetInstance().Scan(input.UseFileMetadata)
return "todo", nil
}
func (r *mutationResolver) MetadataImport(ctx context.Context) (string, error) {
manager.GetInstance().Import()
return "todo", nil
}
func (r *mutationResolver) MetadataExport(ctx context.Context) (string, error) {
manager.GetInstance().Export()
return "todo", nil
}
func (r *mutationResolver) MetadataGenerate(ctx context.Context, input models.GenerateMetadataInput) (string, error) {
manager.GetInstance().Generate(input.Sprites, input.Previews, input.Markers, input.Transcodes)
return "todo", nil
}
func (r *mutationResolver) MetadataAutoTag(ctx context.Context, input models.AutoTagMetadataInput) (string, error) {
manager.GetInstance().AutoTag(input.Performers, input.Studios, input.Tags)
return "todo", nil
}
func (r *mutationResolver) MetadataClean(ctx context.Context) (string, error) {
manager.GetInstance().Clean()
return "todo", nil
}
func (r *mutationResolver) JobStatus(ctx context.Context) (*models.MetadataUpdateStatus, error) {
status := manager.GetInstance().Status
ret := models.MetadataUpdateStatus{
Progress: status.Progress,
Status: status.Status.String(),
Message: "",
}
return &ret, nil
}
func (r *mutationResolver) StopJob(ctx context.Context) (bool, error) {
return manager.GetInstance().Status.Stop(), nil
}

View file

@ -500,3 +500,13 @@ func (r *mutationResolver) SceneResetO(ctx context.Context, id string) (int, err
return newVal, nil
}
func (r *mutationResolver) SceneGenerateScreenshot(ctx context.Context, id string, at *float64) (string, error) {
if at != nil {
manager.GetInstance().GenerateScreenshot(id, *at)
} else {
manager.GetInstance().GenerateDefaultScreenshot(id)
}
return "todo", nil
}

View file

@ -7,36 +7,6 @@ import (
"github.com/stashapp/stash/pkg/models"
)
func (r *queryResolver) MetadataScan(ctx context.Context, input models.ScanMetadataInput) (string, error) {
manager.GetInstance().Scan(input.UseFileMetadata)
return "todo", nil
}
func (r *queryResolver) MetadataImport(ctx context.Context) (string, error) {
manager.GetInstance().Import()
return "todo", nil
}
func (r *queryResolver) MetadataExport(ctx context.Context) (string, error) {
manager.GetInstance().Export()
return "todo", nil
}
func (r *queryResolver) MetadataGenerate(ctx context.Context, input models.GenerateMetadataInput) (string, error) {
manager.GetInstance().Generate(input.Sprites, input.Previews, input.Markers, input.Transcodes)
return "todo", nil
}
func (r *queryResolver) MetadataAutoTag(ctx context.Context, input models.AutoTagMetadataInput) (string, error) {
manager.GetInstance().AutoTag(input.Performers, input.Studios, input.Tags)
return "todo", nil
}
func (r *queryResolver) MetadataClean(ctx context.Context) (string, error) {
manager.GetInstance().Clean()
return "todo", nil
}
func (r *queryResolver) JobStatus(ctx context.Context) (*models.MetadataUpdateStatus, error) {
status := manager.GetInstance().Status
ret := models.MetadataUpdateStatus{
@ -47,7 +17,3 @@ func (r *queryResolver) JobStatus(ctx context.Context) (*models.MetadataUpdateSt
return &ret, nil
}
func (r *queryResolver) StopJob(ctx context.Context) (bool, error) {
return manager.GetInstance().Status.Stop(), nil
}

View file

@ -10,7 +10,7 @@ type ScreenshotOptions struct {
Verbosity string
}
func (e *Encoder) Screenshot(probeResult VideoFile, options ScreenshotOptions) {
func (e *Encoder) Screenshot(probeResult VideoFile, options ScreenshotOptions) error {
if options.Verbosity == "" {
options.Verbosity = "error"
}
@ -28,5 +28,7 @@ func (e *Encoder) Screenshot(probeResult VideoFile, options ScreenshotOptions) {
"-f", "image2",
options.OutputPath,
}
_, _ = e.run(probeResult, args)
_, err := e.run(probeResult, args)
return err
}

View file

@ -216,6 +216,55 @@ func (s *singleton) Generate(sprites bool, previews bool, markers bool, transcod
}()
}
func (s *singleton) GenerateDefaultScreenshot(sceneId string) {
s.generateScreenshot(sceneId, nil)
}
func (s *singleton) GenerateScreenshot(sceneId string, at float64) {
s.generateScreenshot(sceneId, &at)
}
// generate default screenshot if at is nil
func (s *singleton) generateScreenshot(sceneId string, at *float64) {
if s.Status.Status != Idle {
return
}
s.Status.SetStatus(Generate)
s.Status.indefiniteProgress()
qb := models.NewSceneQueryBuilder()
instance.Paths.Generated.EnsureTmpDir()
go func() {
defer s.returnToIdleState()
sceneIdInt, err := strconv.Atoi(sceneId)
if err != nil {
logger.Errorf("Error parsing scene id %s: %s", sceneId, err.Error())
return
}
scene, err := qb.Find(sceneIdInt)
if err != nil || scene == nil {
logger.Errorf("failed to get scene for generate")
return
}
task := GenerateScreenshotTask{
Scene: *scene,
ScreenshotAt: at,
}
var wg sync.WaitGroup
wg.Add(1)
go task.Start(&wg)
wg.Wait()
logger.Infof("Generate finished")
}()
}
func (s *singleton) AutoTag(performerIds []string, studioIds []string, tagIds []string) {
if s.Status.Status != Idle {
return

16
pkg/manager/screenshot.go Normal file
View file

@ -0,0 +1,16 @@
package manager
import (
"github.com/stashapp/stash/pkg/ffmpeg"
)
func makeScreenshot(probeResult ffmpeg.VideoFile, outputPath string, quality int, width int, time float64) {
encoder := ffmpeg.NewEncoder(instance.FFMPEGPath)
options := ffmpeg.ScreenshotOptions{
OutputPath: outputPath,
Quality: quality,
Time: time,
Width: width,
}
encoder.Screenshot(probeResult, options)
}

View file

@ -0,0 +1,84 @@
package manager
import (
"context"
"io/ioutil"
"os"
"sync"
"time"
"github.com/stashapp/stash/pkg/database"
"github.com/stashapp/stash/pkg/ffmpeg"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
)
type GenerateScreenshotTask struct {
Scene models.Scene
ScreenshotAt *float64
}
func (t *GenerateScreenshotTask) Start(wg *sync.WaitGroup) {
defer wg.Done()
scenePath := t.Scene.Path
probeResult, err := ffmpeg.NewVideoFile(instance.FFProbePath, scenePath)
if err != nil {
logger.Error(err.Error())
return
}
var at float64
if t.ScreenshotAt == nil {
at = float64(probeResult.Duration) * 0.2
} else {
at = *t.ScreenshotAt
}
checksum := t.Scene.Checksum
normalPath := instance.Paths.Scene.GetScreenshotPath(checksum)
// we'll generate the screenshot, grab the generated data and set it
// in the database. We'll use SetSceneScreenshot to set the data
// which also generates the thumbnail
logger.Debugf("Creating screenshot for %s", scenePath)
makeScreenshot(*probeResult, normalPath, 2, probeResult.Width, at)
f, err := os.Open(normalPath)
if err != nil {
logger.Errorf("Error reading screenshot: %s", err.Error())
return
}
defer f.Close()
coverImageData, err := ioutil.ReadAll(f)
if err != nil {
logger.Errorf("Error reading screenshot: %s", err.Error())
return
}
ctx := context.TODO()
tx := database.DB.MustBeginTx(ctx, nil)
qb := models.NewSceneQueryBuilder()
updatedTime := time.Now()
updatedScene := models.ScenePartial{
ID: t.Scene.ID,
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
}
updatedScene.Cover = &coverImageData
err = SetSceneScreenshot(t.Scene.Checksum, coverImageData)
_, err = qb.Update(updatedScene, tx)
if err != nil {
logger.Errorf("Error setting screenshot: %s", err.Error())
return
}
if err := tx.Commit(); err != nil {
logger.Errorf("Error setting screenshot: %s", err.Error())
return
}
}

View file

@ -177,28 +177,19 @@ func (t *ScanTask) makeScreenshots(probeResult *ffmpeg.VideoFile, checksum strin
logger.Infof("Regenerating images for %s", t.FilePath)
}
at := float64(probeResult.Duration) * 0.2
if !thumbExists {
logger.Debugf("Creating thumbnail for %s", t.FilePath)
t.makeScreenshot(*probeResult, thumbPath, 5, 320)
makeScreenshot(*probeResult, thumbPath, 5, 320, at)
}
if !normalExists {
logger.Debugf("Creating screenshot for %s", t.FilePath)
t.makeScreenshot(*probeResult, normalPath, 2, probeResult.Width)
makeScreenshot(*probeResult, normalPath, 2, probeResult.Width, at)
}
}
func (t *ScanTask) makeScreenshot(probeResult ffmpeg.VideoFile, outputPath string, quality int, width int) {
encoder := ffmpeg.NewEncoder(instance.FFMPEGPath)
options := ffmpeg.ScreenshotOptions{
OutputPath: outputPath,
Quality: quality,
Time: float64(probeResult.Duration) * 0.2,
Width: width,
}
encoder.Screenshot(probeResult, options)
}
func (t *ScanTask) calculateChecksum() (string, error) {
logger.Infof("%s not found. Calculating checksum...", t.FilePath)
checksum, err := utils.MD5FromFilePath(t.FilePath)

View file

@ -18,7 +18,7 @@ export const PerformerOperationsPanel: React.FC<IPerformerOperationsProps> = ({
return;
}
try {
await StashService.queryMetadataAutoTag({ performers: [performer.id] });
await StashService.mutateMetadataAutoTag({ performers: [performer.id] });
Toast.success({ content: "Started auto tagging" });
} catch (e) {
Toast.error(e);

View file

@ -14,6 +14,7 @@ import { SceneFileInfoPanel } from "./SceneFileInfoPanel";
import { SceneEditPanel } from "./SceneEditPanel";
import { SceneDetailPanel } from "./SceneDetailPanel";
import { OCounterButton } from "./OCounterButton";
import { SceneOperationsPanel } from "./SceneOperationsPanel";
export const Scene: React.FC = () => {
const { id = "new" } = useParams();
@ -144,6 +145,11 @@ export const Scene: React.FC = () => {
onDelete={() => history.push("/scenes")}
/>
</Tab>
<Tab eventKey="scene-operations-panel" title="Operations">
<SceneOperationsPanel
scene={scene}
/>
</Tab>
</Tabs>
</div>
</>

View file

@ -0,0 +1,36 @@
import { Button } from "react-bootstrap";
import React, { FunctionComponent } from "react";
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { useToast } from "src/hooks";
import { JWUtils } from "src/utils";
interface IOperationsPanelProps {
scene: GQL.SceneDataFragment;
}
export const SceneOperationsPanel: FunctionComponent<IOperationsPanelProps> = (props: IOperationsPanelProps) => {
const Toast = useToast();
const [generateScreenshot] = StashService.useSceneGenerateScreenshot();
async function onGenerateScreenshot(at?: number) {
await generateScreenshot({
variables: {
id: props.scene.id,
at: at
}
});
Toast.success({ content: "Generating screenshot" });
}
return (
<>
<Button className="edit-button" onClick={() => onGenerateScreenshot(JWUtils.getPlayer().getPosition())}>
Generate thumbnail from current
</Button>
<Button className="edit-button" onClick={() => onGenerateScreenshot()}>
Generate default thumbnail
</Button>
</>
);
};

View file

@ -12,7 +12,7 @@ export const GenerateButton: React.FC = () => {
async function onGenerate() {
try {
await StashService.queryMetadataGenerate({
await StashService.mutateMetadataGenerate({
sprites,
previews,
markers,

View file

@ -68,7 +68,7 @@ export const SettingsTasksPanel: React.FC = () => {
function onImport() {
setIsImportAlertOpen(false);
StashService.queryMetadataImport().then(() => {
StashService.mutateMetadataImport().then(() => {
jobStatus.refetch();
});
}
@ -91,7 +91,7 @@ export const SettingsTasksPanel: React.FC = () => {
function onClean() {
setIsCleanAlertOpen(false);
StashService.queryMetadataClean().then(() => {
StashService.mutateMetadataClean().then(() => {
jobStatus.refetch();
});
}
@ -115,7 +115,7 @@ export const SettingsTasksPanel: React.FC = () => {
async function onScan() {
try {
await StashService.queryMetadataScan({ useFileMetadata });
await StashService.mutateMetadataScan({ useFileMetadata });
Toast.success({ content: "Started scan" });
jobStatus.refetch();
} catch (e) {
@ -134,7 +134,7 @@ export const SettingsTasksPanel: React.FC = () => {
async function onAutoTag() {
try {
await StashService.queryMetadataAutoTag(getAutoTagInput());
await StashService.mutateMetadataAutoTag(getAutoTagInput());
Toast.success({ content: "Started auto tagging" });
jobStatus.refetch();
} catch (e) {
@ -153,7 +153,7 @@ export const SettingsTasksPanel: React.FC = () => {
id="stop"
variant="danger"
onClick={() =>
StashService.queryStopJob().then(() => jobStatus.refetch())
StashService.mutateStopJob().then(() => jobStatus.refetch())
}
>
Stop
@ -277,7 +277,7 @@ export const SettingsTasksPanel: React.FC = () => {
variant="secondary"
type="submit"
onClick={() =>
StashService.queryMetadataExport().then(() => {
StashService.mutateMetadataExport().then(() => {
jobStatus.refetch();
})
}

View file

@ -115,7 +115,7 @@ export const Studio: React.FC = () => {
async function onAutoTag() {
if (!studio.id) return;
try {
await StashService.queryMetadataAutoTag({ studios: [studio.id] });
await StashService.mutateMetadataAutoTag({ studios: [studio.id] });
Toast.success({ content: "Started auto tagging" });
} catch (e) {
Toast.error(e);

View file

@ -62,7 +62,7 @@ export const TagList: React.FC = () => {
async function onAutoTag(tag: GQL.TagDataFragment) {
if (!tag) return;
try {
await StashService.queryMetadataAutoTag({ tags: [tag.id] });
await StashService.mutateMetadataAutoTag({ tags: [tag.id] });
Toast.success({ content: "Started auto tagging" });
} catch (e) {
Toast.error(e);

View file

@ -407,6 +407,12 @@ export class StashService {
});
}
public static useSceneGenerateScreenshot() {
return GQL.useSceneGenerateScreenshotMutation({
update: () => StashService.invalidateQueries(["findScenes"]),
});
}
private static studioMutationImpactedQueries = [
"findStudios",
"findScenes",
@ -506,10 +512,9 @@ export class StashService {
});
}
public static queryStopJob() {
return StashService.client.query<GQL.StopJobQuery>({
query: GQL.StopJobDocument,
fetchPolicy: "network-only"
public static mutateStopJob() {
return StashService.client.mutate<GQL.StopJobMutation>({
mutation: GQL.StopJobDocument,
});
}
@ -566,48 +571,42 @@ export class StashService {
});
}
public static queryMetadataScan(input: GQL.ScanMetadataInput) {
return StashService.client.query<GQL.MetadataScanQuery>({
query: GQL.MetadataScanDocument,
public static mutateMetadataScan(input: GQL.ScanMetadataInput) {
return StashService.client.mutate<GQL.MetadataScanMutation>({
mutation: GQL.MetadataScanDocument,
variables: { input },
fetchPolicy: "network-only"
});
}
public static queryMetadataAutoTag(input: GQL.AutoTagMetadataInput) {
return StashService.client.query<GQL.MetadataAutoTagQuery>({
query: GQL.MetadataAutoTagDocument,
public static mutateMetadataAutoTag(input: GQL.AutoTagMetadataInput) {
return StashService.client.mutate<GQL.MetadataAutoTagMutation>({
mutation: GQL.MetadataAutoTagDocument,
variables: { input },
fetchPolicy: "network-only"
});
}
public static queryMetadataGenerate(input: GQL.GenerateMetadataInput) {
return StashService.client.query<GQL.MetadataGenerateQuery>({
query: GQL.MetadataGenerateDocument,
public static mutateMetadataGenerate(input: GQL.GenerateMetadataInput) {
return StashService.client.mutate<GQL.MetadataGenerateMutation>({
mutation: GQL.MetadataGenerateDocument,
variables: { input },
fetchPolicy: "network-only"
});
}
public static queryMetadataClean() {
return StashService.client.query<GQL.MetadataCleanQuery>({
query: GQL.MetadataCleanDocument,
fetchPolicy: "network-only"
public static mutateMetadataClean() {
return StashService.client.mutate<GQL.MetadataCleanMutation>({
mutation: GQL.MetadataCleanDocument,
});
}
public static queryMetadataExport() {
return StashService.client.query<GQL.MetadataExportQuery>({
query: GQL.MetadataExportDocument,
fetchPolicy: "network-only"
public static mutateMetadataExport() {
return StashService.client.mutate<GQL.MetadataExportMutation>({
mutation: GQL.MetadataExportDocument,
});
}
public static queryMetadataImport() {
return StashService.client.query<GQL.MetadataImportQuery>({
query: GQL.MetadataImportDocument,
fetchPolicy: "network-only"
public static mutateMetadataImport() {
return StashService.client.mutate<GQL.MetadataImportMutation>({
mutation: GQL.MetadataImportDocument,
});
}

View file

@ -18,7 +18,7 @@ export const GenerateButton: FunctionComponent<IProps> = () => {
async function onGenerate() {
try {
await StashService.queryMetadataGenerate({sprites, previews, markers, transcodes});
await StashService.mutateMetadataGenerate({sprites, previews, markers, transcodes});
ToastUtils.success("Started generating");
} catch (e) {
ErrorUtils.handle(e);

View file

@ -78,7 +78,7 @@ export const SettingsTasksPanel: FunctionComponent<IProps> = (props: IProps) =>
function onImport() {
setIsImportAlertOpen(false);
StashService.queryMetadataImport().then(() => { jobStatus.refetch()});
StashService.mutateMetadataImport().then(() => { jobStatus.refetch()});
}
function renderImportAlert() {
@ -102,7 +102,7 @@ export const SettingsTasksPanel: FunctionComponent<IProps> = (props: IProps) =>
function onClean() {
setIsCleanAlertOpen(false);
StashService.queryMetadataClean().then(() => { jobStatus.refetch()});
StashService.mutateMetadataClean().then(() => { jobStatus.refetch()});
}
function renderCleanAlert() {
@ -127,7 +127,7 @@ export const SettingsTasksPanel: FunctionComponent<IProps> = (props: IProps) =>
async function onScan() {
try {
await StashService.queryMetadataScan({useFileMetadata: useFileMetadata});
await StashService.mutateMetadataScan({useFileMetadata: useFileMetadata});
ToastUtils.success("Started scan");
jobStatus.refetch();
} catch (e) {
@ -146,7 +146,7 @@ export const SettingsTasksPanel: FunctionComponent<IProps> = (props: IProps) =>
async function onAutoTag() {
try {
await StashService.queryMetadataAutoTag(getAutoTagInput());
await StashService.mutateMetadataAutoTag(getAutoTagInput());
ToastUtils.success("Started auto tagging");
jobStatus.refetch();
} catch (e) {
@ -162,7 +162,7 @@ export const SettingsTasksPanel: FunctionComponent<IProps> = (props: IProps) =>
return (
<>
<FormGroup>
<Button id="stop" text="Stop" intent="danger" onClick={() => StashService.queryStopJob().then(() => jobStatus.refetch())} />
<Button id="stop" text="Stop" intent="danger" onClick={() => StashService.mutateStopJob().then(() => jobStatus.refetch())} />
</FormGroup>
</>
);
@ -256,7 +256,7 @@ export const SettingsTasksPanel: FunctionComponent<IProps> = (props: IProps) =>
labelFor="export"
inline={true}
>
<Button id="export" text="Export" onClick={() => StashService.queryMetadataExport().then(() => { jobStatus.refetch()})} />
<Button id="export" text="Export" onClick={() => StashService.mutateMetadataExport().then(() => { jobStatus.refetch()})} />
</FormGroup>
<FormGroup

View file

@ -109,7 +109,7 @@ export const Studio: FunctionComponent<IProps> = (props: IProps) => {
return;
}
try {
await StashService.queryMetadataAutoTag({ studios: [studio.id]});
await StashService.mutateMetadataAutoTag({ studios: [studio.id]});
ToastUtils.success("Started auto tagging");
} catch (e) {
ErrorUtils.handle(e);

View file

@ -76,7 +76,7 @@ export const TagList: FunctionComponent<IProps> = (props: IProps) => {
return;
}
try {
await StashService.queryMetadataAutoTag({ tags: [tag.id]});
await StashService.mutateMetadataAutoTag({ tags: [tag.id]});
ToastUtils.success("Started auto tagging");
} catch (e) {
ErrorUtils.handle(e);

View file

@ -18,7 +18,7 @@ export const PerformerOperationsPanel: FunctionComponent<IPerformerOperationsPro
return;
}
try {
await StashService.queryMetadataAutoTag({ performers: [props.performer.id]});
await StashService.mutateMetadataAutoTag({ performers: [props.performer.id]});
ToastUtils.success("Started auto tagging");
} catch (e) {
ErrorUtils.handle(e);

View file

@ -19,6 +19,7 @@ import { ScenePerformerPanel } from "./ScenePerformerPanel";
import { SceneMoviePanel } from "./SceneMoviePanel";
import { ErrorUtils } from "../../../utils/errors";
import { IOCounterButtonProps, OCounterButton } from "../OCounterButton";
import { SceneOperationsPanel } from "./SceneOperationsPanel";
interface ISceneProps extends IBaseProps {}
@ -160,6 +161,14 @@ export const Scene: FunctionComponent<ISceneProps> = (props: ISceneProps) => {
onDelete={() => props.history.push("/scenes")}
/>}
/>
<Tab
id="scene-operations-panel"
title="Operations"
panel={
<SceneOperationsPanel
scene={modifiedScene}
/>}
/>
<Tabs.Expander />
<OCounterButton

View file

@ -0,0 +1,47 @@
import {
Button,
} from "@blueprintjs/core";
import React, { FunctionComponent } from "react";
import * as GQL from "../../../core/generated-graphql";
import { StashService } from "../../../core/StashService";
import { SceneHelpers } from "../helpers";
import { ToastUtils } from "../../../utils/toasts";
interface IOperationsPanelProps {
scene: GQL.SceneDataFragment;
}
export const SceneOperationsPanel: FunctionComponent<IOperationsPanelProps> = (props: IOperationsPanelProps) => {
const jwplayer = SceneHelpers.getPlayer();
const generateScreenshot = StashService.useSceneGenerateScreenshot();
async function onGenerateScreenshot() {
let position = jwplayer.getPosition();
await generateScreenshot({
variables: {
id: props.scene.id,
at: position
}
});
ToastUtils.success("Generating screenshot");
}
async function onGenerateDefaultScreenshot() {
await generateScreenshot({
variables: {
id: props.scene.id,
}
});
ToastUtils.success("Generating screenshot");
}
return (
<>
<Button className="edit-button" text="Generate thumbnail from current" onClick={() => onGenerateScreenshot()}/>
<Button className="edit-button" text="Generate default thumbnail" onClick={() => onGenerateDefaultScreenshot()}/>
</>
);
};

View file

@ -355,6 +355,12 @@ export class StashService {
});
}
public static useSceneGenerateScreenshot() {
return GQL.useSceneGenerateScreenshot({
update: () => StashService.invalidateQueries(["findScenes"]),
});
}
private static studioMutationImpactedQueries = [
"findStudios",
"findScenes",
@ -467,10 +473,9 @@ export class StashService {
});
}
public static queryStopJob() {
return StashService.client.query<GQL.StopJobQuery>({
query: GQL.StopJobDocument,
fetchPolicy: "network-only",
public static mutateStopJob() {
return StashService.client.mutate<GQL.StopJobMutation>({
mutation: GQL.StopJobDocument,
});
}
@ -521,48 +526,42 @@ export class StashService {
});
}
public static queryMetadataScan(input: GQL.ScanMetadataInput) {
return StashService.client.query<GQL.MetadataScanQuery>({
query: GQL.MetadataScanDocument,
public static mutateMetadataScan(input: GQL.ScanMetadataInput) {
return StashService.client.mutate<GQL.MetadataScanMutation>({
mutation: GQL.MetadataScanDocument,
variables: { input },
fetchPolicy: "network-only",
});
}
public static queryMetadataAutoTag(input: GQL.AutoTagMetadataInput) {
return StashService.client.query<GQL.MetadataAutoTagQuery>({
query: GQL.MetadataAutoTagDocument,
public static mutateMetadataAutoTag(input: GQL.AutoTagMetadataInput) {
return StashService.client.mutate<GQL.MetadataAutoTagMutation>({
mutation: GQL.MetadataAutoTagDocument,
variables: { input },
fetchPolicy: "network-only",
});
}
public static queryMetadataGenerate(input: GQL.GenerateMetadataInput) {
return StashService.client.query<GQL.MetadataGenerateQuery>({
query: GQL.MetadataGenerateDocument,
public static mutateMetadataGenerate(input: GQL.GenerateMetadataInput) {
return StashService.client.mutate<GQL.MetadataGenerateMutation>({
mutation: GQL.MetadataGenerateDocument,
variables: { input },
fetchPolicy: "network-only",
});
}
public static queryMetadataClean() {
return StashService.client.query<GQL.MetadataCleanQuery>({
query: GQL.MetadataCleanDocument,
fetchPolicy: "network-only",
public static mutateMetadataClean() {
return StashService.client.mutate<GQL.MetadataCleanMutation>({
mutation: GQL.MetadataCleanDocument,
});
}
public static queryMetadataExport() {
return StashService.client.query<GQL.MetadataExportQuery>({
query: GQL.MetadataExportDocument,
fetchPolicy: "network-only",
public static mutateMetadataExport() {
return StashService.client.mutate<GQL.MetadataExportMutation>({
mutation: GQL.MetadataExportDocument,
});
}
public static queryMetadataImport() {
return StashService.client.query<GQL.MetadataImportQuery>({
query: GQL.MetadataImportDocument,
fetchPolicy: "network-only",
public static mutateMetadataImport() {
return StashService.client.mutate<GQL.MetadataImportMutation>({
mutation: GQL.MetadataImportDocument,
});
}