mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 12:54:38 +03:00
Add force transcode option (#2126)
This commit is contained in:
@@ -9,6 +9,8 @@ input GenerateMetadataInput {
|
|||||||
markerImagePreviews: Boolean
|
markerImagePreviews: Boolean
|
||||||
markerScreenshots: Boolean
|
markerScreenshots: Boolean
|
||||||
transcodes: Boolean
|
transcodes: Boolean
|
||||||
|
"""Generate transcodes even if not required"""
|
||||||
|
forceTranscodes: Boolean
|
||||||
phashes: Boolean
|
phashes: Boolean
|
||||||
interactiveHeatmapsSpeeds: Boolean
|
interactiveHeatmapsSpeeds: Boolean
|
||||||
|
|
||||||
|
|||||||
@@ -252,9 +252,11 @@ func (j *GenerateJob) queueSceneJobs(scene *models.Scene, queue chan<- Task, tot
|
|||||||
}
|
}
|
||||||
|
|
||||||
if utils.IsTrue(j.input.Transcodes) {
|
if utils.IsTrue(j.input.Transcodes) {
|
||||||
|
forceTranscode := utils.IsTrue(j.input.ForceTranscodes)
|
||||||
task := &GenerateTranscodeTask{
|
task := &GenerateTranscodeTask{
|
||||||
Scene: *scene,
|
Scene: *scene,
|
||||||
Overwrite: j.overwrite,
|
Overwrite: j.overwrite,
|
||||||
|
Force: forceTranscode,
|
||||||
fileNamingAlgorithm: j.fileNamingAlgo,
|
fileNamingAlgorithm: j.fileNamingAlgo,
|
||||||
}
|
}
|
||||||
if task.isTranscodeNeeded() {
|
if task.isTranscodeNeeded() {
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ type GenerateTranscodeTask struct {
|
|||||||
Scene models.Scene
|
Scene models.Scene
|
||||||
Overwrite bool
|
Overwrite bool
|
||||||
fileNamingAlgorithm models.HashAlgorithm
|
fileNamingAlgorithm models.HashAlgorithm
|
||||||
|
|
||||||
|
// is true, generate even if video is browser-supported
|
||||||
|
Force bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *GenerateTranscodeTask) GetDescription() string {
|
func (t *GenerateTranscodeTask) GetDescription() string {
|
||||||
@@ -49,7 +52,7 @@ func (t *GenerateTranscodeTask) Start(ctc context.Context) {
|
|||||||
audioCodec = ffmpeg.AudioCodec(t.Scene.AudioCodec.String)
|
audioCodec = ffmpeg.AudioCodec(t.Scene.AudioCodec.String)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ffmpeg.IsStreamable(videoCodec, audioCodec, container) {
|
if !t.Force && ffmpeg.IsStreamable(videoCodec, audioCodec, container) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +98,14 @@ func (t *GenerateTranscodeTask) Start(ctc context.Context) {
|
|||||||
// used only when counting files to generate, doesn't affect the actual transcode generation
|
// used only when counting files to generate, doesn't affect the actual transcode generation
|
||||||
// if container is missing from DB it is treated as non supported in order not to delay the user
|
// if container is missing from DB it is treated as non supported in order not to delay the user
|
||||||
func (t *GenerateTranscodeTask) isTranscodeNeeded() bool {
|
func (t *GenerateTranscodeTask) isTranscodeNeeded() bool {
|
||||||
|
hasTranscode := HasTranscode(&t.Scene, t.fileNamingAlgorithm)
|
||||||
|
if !t.Overwrite && hasTranscode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Force {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
videoCodec := t.Scene.VideoCodec.String
|
videoCodec := t.Scene.VideoCodec.String
|
||||||
container := ""
|
container := ""
|
||||||
@@ -111,9 +122,5 @@ func (t *GenerateTranscodeTask) isTranscodeNeeded() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
hasTranscode := HasTranscode(&t.Scene, t.fileNamingAlgorithm)
|
|
||||||
if !t.Overwrite && hasTranscode {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
### ✨ New Features
|
### ✨ New Features
|
||||||
|
* Added option to force generation of transcodes for selected scenes. ([#2126](https://github.com/stashapp/stash/pull/2126))
|
||||||
* Added selective clean task. ([#2125](https://github.com/stashapp/stash/pull/2125))
|
* Added selective clean task. ([#2125](https://github.com/stashapp/stash/pull/2125))
|
||||||
* Show heatmaps and median stroke speed for interactive scenes on the scenes page. ([#2096](https://github.com/stashapp/stash/pull/2096))
|
* Show heatmaps and median stroke speed for interactive scenes on the scenes page. ([#2096](https://github.com/stashapp/stash/pull/2096))
|
||||||
* Save task options when scanning, generating and auto-tagging. ([#1949](https://github.com/stashapp/stash/pull/1949), [#2061](https://github.com/stashapp/stash/pull/2061))
|
* Save task options when scanning, generating and auto-tagging. ([#1949](https://github.com/stashapp/stash/pull/1949), [#2061](https://github.com/stashapp/stash/pull/2061))
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import React, { useState, useEffect, useMemo } from "react";
|
import React, { useState, useEffect, useMemo } from "react";
|
||||||
import { Form, Button } from "react-bootstrap";
|
import { Form, Button } from "react-bootstrap";
|
||||||
import {
|
import { mutateMetadataGenerate } from "src/core/StashService";
|
||||||
mutateMetadataGenerate,
|
import { Modal, Icon } from "src/components/Shared";
|
||||||
useConfigureDefaults,
|
|
||||||
} from "src/core/StashService";
|
|
||||||
import { Modal, Icon, OperationButton } from "src/components/Shared";
|
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
// import { DirectorySelectionDialog } from "../Settings/SettingsTasksPanel/DirectorySelectionDialog";
|
|
||||||
import { Manual } from "../Help/Manual";
|
import { Manual } from "../Help/Manual";
|
||||||
import { withoutTypename } from "src/utils";
|
import { withoutTypename } from "src/utils";
|
||||||
import { GenerateOptions } from "../Settings/Tasks/GenerateOptions";
|
import { GenerateOptions } from "../Settings/Tasks/GenerateOptions";
|
||||||
@@ -25,7 +21,6 @@ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
const { configuration } = React.useContext(ConfigurationContext);
|
const { configuration } = React.useContext(ConfigurationContext);
|
||||||
const [configureDefaults] = useConfigureDefaults();
|
|
||||||
|
|
||||||
function getDefaultOptions(): GQL.GenerateMetadataInput {
|
function getDefaultOptions(): GQL.GenerateMetadataInput {
|
||||||
return {
|
return {
|
||||||
@@ -45,10 +40,7 @@ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
|
|||||||
getDefaultOptions()
|
getDefaultOptions()
|
||||||
);
|
);
|
||||||
const [configRead, setConfigRead] = useState(false);
|
const [configRead, setConfigRead] = useState(false);
|
||||||
const [paths /* , setPaths */] = useState<string[]>([]);
|
|
||||||
const [showManual, setShowManual] = useState(false);
|
const [showManual, setShowManual] = useState(false);
|
||||||
// const [settingPaths, setSettingPaths] = useState(false);
|
|
||||||
const [savingDefaults, setSavingDefaults] = useState(false);
|
|
||||||
const [animation, setAnimation] = useState(true);
|
const [animation, setAnimation] = useState(true);
|
||||||
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -111,16 +103,7 @@ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const message = paths.length ? (
|
const message = (
|
||||||
<div>
|
|
||||||
<FormattedMessage id="config.tasks.generate.generating_from_paths" />:
|
|
||||||
<ul>
|
|
||||||
{paths.map((p) => (
|
|
||||||
<li key={p}>{p}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span>
|
<span>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="config.tasks.generate.generating_scenes"
|
id="config.tasks.generate.generating_scenes"
|
||||||
@@ -140,27 +123,12 @@ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
// function onClick() {
|
|
||||||
// setAnimation(false);
|
|
||||||
// setSettingPaths(true);
|
|
||||||
// }
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Group className="dialog-selected-folders">
|
<Form.Group className="dialog-selected-folders">
|
||||||
<div>
|
<div>{message}</div>
|
||||||
{message}
|
|
||||||
{/* <div>
|
|
||||||
<Button
|
|
||||||
title={intl.formatMessage({ id: "actions.select_folders" })}
|
|
||||||
onClick={() => onClick()}
|
|
||||||
>
|
|
||||||
<Icon icon="folder-open" />
|
|
||||||
</Button>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
}, [selectedIds, intl, paths]);
|
}, [selectedIds, intl]);
|
||||||
|
|
||||||
async function onGenerate() {
|
async function onGenerate() {
|
||||||
try {
|
try {
|
||||||
@@ -181,58 +149,11 @@ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeDefaultGenerateInput() {
|
|
||||||
const ret = options;
|
|
||||||
// const { paths: _paths, ...withoutSpecifics } = ret;
|
|
||||||
const { overwrite: _overwrite, ...withoutSpecifics } = ret;
|
|
||||||
return withoutSpecifics;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onShowManual() {
|
function onShowManual() {
|
||||||
setAnimation(false);
|
setAnimation(false);
|
||||||
setShowManual(true);
|
setShowManual(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setAsDefault() {
|
|
||||||
try {
|
|
||||||
setSavingDefaults(true);
|
|
||||||
await configureDefaults({
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
generate: makeDefaultGenerateInput(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
Toast.success({
|
|
||||||
content: intl.formatMessage(
|
|
||||||
{ id: "config.tasks.defaults_set" },
|
|
||||||
{ action: intl.formatMessage({ id: "actions.generate" }) }
|
|
||||||
),
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
Toast.error(e);
|
|
||||||
} finally {
|
|
||||||
setSavingDefaults(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (settingPaths) {
|
|
||||||
// return (
|
|
||||||
// <DirectorySelectionDialog
|
|
||||||
// animation={false}
|
|
||||||
// allowEmpty
|
|
||||||
// initialPaths={paths}
|
|
||||||
// onClose={(p) => {
|
|
||||||
// if (p) {
|
|
||||||
// setPaths(p);
|
|
||||||
// }
|
|
||||||
// setSettingPaths(false);
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (showManual) {
|
if (showManual) {
|
||||||
return (
|
return (
|
||||||
<Manual
|
<Manual
|
||||||
@@ -259,12 +180,6 @@ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
|
|||||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
variant: "secondary",
|
variant: "secondary",
|
||||||
}}
|
}}
|
||||||
disabled={savingDefaults}
|
|
||||||
footerButtons={
|
|
||||||
<OperationButton variant="secondary" operation={setAsDefault}>
|
|
||||||
<FormattedMessage id="actions.set_as_default" />
|
|
||||||
</OperationButton>
|
|
||||||
}
|
|
||||||
leftFooterButtons={
|
leftFooterButtons={
|
||||||
<Button
|
<Button
|
||||||
title="Help"
|
title="Help"
|
||||||
@@ -278,7 +193,11 @@ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
|
|||||||
<Form>
|
<Form>
|
||||||
{selectionStatus}
|
{selectionStatus}
|
||||||
<SettingSection>
|
<SettingSection>
|
||||||
<GenerateOptions options={options} setOptions={setOptions} />
|
<GenerateOptions
|
||||||
|
options={options}
|
||||||
|
setOptions={setOptions}
|
||||||
|
selection
|
||||||
|
/>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import {
|
|||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
interface IGenerateOptions {
|
interface IGenerateOptions {
|
||||||
|
selection?: boolean;
|
||||||
options: GQL.GenerateMetadataInput;
|
options: GQL.GenerateMetadataInput;
|
||||||
setOptions: (s: GQL.GenerateMetadataInput) => void;
|
setOptions: (s: GQL.GenerateMetadataInput) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GenerateOptions: React.FC<IGenerateOptions> = ({
|
export const GenerateOptions: React.FC<IGenerateOptions> = ({
|
||||||
|
selection,
|
||||||
options,
|
options,
|
||||||
setOptions: setOptionsState,
|
setOptions: setOptionsState,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -110,6 +112,18 @@ export const GenerateOptions: React.FC<IGenerateOptions> = ({
|
|||||||
tooltipID="dialogs.scene_gen.transcodes_tooltip"
|
tooltipID="dialogs.scene_gen.transcodes_tooltip"
|
||||||
onChange={(v) => setOptions({ transcodes: v })}
|
onChange={(v) => setOptions({ transcodes: v })}
|
||||||
/>
|
/>
|
||||||
|
{selection ? (
|
||||||
|
<BooleanSetting
|
||||||
|
id="force-transcode"
|
||||||
|
className="sub-setting"
|
||||||
|
checked={options.forceTranscodes ?? false}
|
||||||
|
disabled={!options.transcodes}
|
||||||
|
headingID="dialogs.scene_gen.force_transcodes"
|
||||||
|
tooltipID="dialogs.scene_gen.force_transcodes_tooltip"
|
||||||
|
onChange={(v) => setOptions({ forceTranscodes: v })}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
id="phash-task"
|
id="phash-task"
|
||||||
checked={options.phashes ?? false}
|
checked={options.phashes ?? false}
|
||||||
|
|||||||
@@ -585,6 +585,8 @@
|
|||||||
},
|
},
|
||||||
"overwrite_filter_confirm": "Are you sure you want to overwrite existing saved query {entityName}?",
|
"overwrite_filter_confirm": "Are you sure you want to overwrite existing saved query {entityName}?",
|
||||||
"scene_gen": {
|
"scene_gen": {
|
||||||
|
"force_transcodes": "Force Transcode generation",
|
||||||
|
"force_transcodes_tooltip": "By default, transcodes are only generated when the video file is not supported in the browser. When enabled, transcodes will be generated even when the video file appears to be supported in the browser.",
|
||||||
"image_previews": "Animated Image Previews",
|
"image_previews": "Animated Image Previews",
|
||||||
"image_previews_tooltip": "Animated WebP previews, only required if Preview Type is set to Animated Image.",
|
"image_previews_tooltip": "Animated WebP previews, only required if Preview Type is set to Animated Image.",
|
||||||
"interactive_heatmap_speed": "Generate heatmaps and speeds for interactive scenes",
|
"interactive_heatmap_speed": "Generate heatmaps and speeds for interactive scenes",
|
||||||
|
|||||||
Reference in New Issue
Block a user