/* eslint-disable no-param-reassign, jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */ import React, { useEffect, useState, useCallback } from "react"; import { Badge, Button, Card, Form, Table } from "react-bootstrap"; import _ from "lodash"; import { StashService } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; import { FilterSelect, StudioSelect, LoadingIndicator } from "src/components/Shared"; import { TextUtils } from "src/utils"; import { useToast } from "src/hooks"; import { Pagination } from "../list/Pagination"; import { IParserInput, ParserInput } from "./ParserInput"; import { ParserField } from "./ParserField"; class ParserResult { public value: GQL.Maybe = null; public originalValue: GQL.Maybe = null; public set: boolean = false; public setOriginalValue(v: GQL.Maybe) { this.originalValue = v; this.value = v; } public setValue(v: GQL.Maybe) { if (v) { this.value = v; this.set = !_.isEqual(this.value, this.originalValue); } } } class SceneParserResult { public id: string; public filename: string; public title: ParserResult = new ParserResult(); public date: ParserResult = new ParserResult(); public studio: ParserResult> = new ParserResult(); public studioId: ParserResult = new ParserResult(); public tags: ParserResult = new ParserResult(); public tagIds: ParserResult = new ParserResult(); public performers: ParserResult< Partial[] > = new ParserResult(); public performerIds: ParserResult = new ParserResult(); public scene: GQL.SlimSceneDataFragment; constructor( result: GQL.ParseSceneFilenamesQuery["parseSceneFilenames"]["results"][0] ) { this.scene = result.scene; this.id = this.scene.id; this.filename = TextUtils.fileNameFromPath(this.scene.path); this.title.setOriginalValue(this.scene.title ?? null); this.date.setOriginalValue(this.scene.date ?? null); this.performerIds.setOriginalValue(this.scene.performers.map(p => p.id)); this.performers.setOriginalValue(this.scene.performers); this.tagIds.setOriginalValue(this.scene.tags.map(t => t.id)); this.tags.setOriginalValue(this.scene.tags); this.studioId.setOriginalValue(this.scene.studio?.id ?? null); this.studio.setOriginalValue(this.scene.studio ?? null); this.title.setValue(result.title ?? null); this.date.setValue(result.date ?? null); this.performerIds.setValue(result.performer_ids ?? []); this.tagIds.setValue(result.tag_ids ?? []); this.studioId.setValue(result.studio_id ?? null); if (result.performer_ids) { this.performers.setValue( (result.performer_ids ?? []).map( p => ({ id: p, name: "", favorite: false, image_path: "" } as GQL.Performer) ) ); } if (result.tag_ids) { this.tags.setValue( result.tag_ids.map(t => ({ id: t, name: "" })) ); } if (result.studio_id) { this.studio.setValue({ id: result.studio_id, name: "", image_path: "" } as GQL.Studio); } } private static setInput( obj: any, key: string, parserResult: ParserResult ) { if (parserResult.set) { obj[key] = parserResult.value; } } // returns true if any of its fields have set == true public isChanged() { return ( this.title.set || this.date.set || this.performerIds.set || this.studioId.set || this.tagIds.set ); } public toSceneUpdateInput() { const ret = { id: this.id, title: this.scene.title, details: this.scene.details, url: this.scene.url, date: this.scene.date, rating: this.scene.rating, gallery_id: this.scene.gallery ? this.scene.gallery.id : undefined, studio_id: this.scene.studio ? this.scene.studio.id : undefined, performer_ids: this.scene.performers.map(performer => performer.id), tag_ids: this.scene.tags.map(tag => tag.id) }; SceneParserResult.setInput(ret, "title", this.title); SceneParserResult.setInput(ret, "date", this.date); SceneParserResult.setInput(ret, "performer_ids", this.performerIds); SceneParserResult.setInput(ret, "studio_id", this.studioId); SceneParserResult.setInput(ret, "tag_ids", this.tagIds); return ret; } } const initialParserInput = { pattern: "{title}.{ext}", ignoreWords: [], whitespaceCharacters: "._", capitalizeTitle: true, page: 1, pageSize: 20, findClicked: false }; const initialShowFieldsState = new Map([ ["Title", true], ["Date", true], ["Performers", true], ["Tags", true], ["Studio", true] ]); export const SceneFilenameParser: React.FC = () => { const Toast = useToast(); const [parserResult, setParserResult] = useState([]); const [parserInput, setParserInput] = useState( initialParserInput ); const [allTitleSet, setAllTitleSet] = useState(false); const [allDateSet, setAllDateSet] = useState(false); const [allPerformerSet, setAllPerformerSet] = useState(false); const [allTagSet, setAllTagSet] = useState(false); const [allStudioSet, setAllStudioSet] = useState(false); const [showFields, setShowFields] = useState>( initialShowFieldsState ); const [totalItems, setTotalItems] = useState(0); // Network state const [isLoading, setIsLoading] = useState(false); const [updateScenes] = StashService.useScenesUpdate(getScenesUpdateData()); const determineFieldsToHide = useCallback(() => { const { pattern } = parserInput; const titleSet = pattern.includes("{title}"); const dateSet = pattern.includes("{date}") || pattern.includes("{dd}") || // don't worry about other partial date fields since this should be implied ParserField.fullDateFields.some(f => { return pattern.includes(`{${f.field}}`); }); const performerSet = pattern.includes("{performer}"); const tagSet = pattern.includes("{tag}"); const studioSet = pattern.includes("{studio}"); const newShowFields = new Map([ ["Title", titleSet], ["Date", dateSet], ["Performers", performerSet], ["Tags", tagSet], ["Studio", studioSet] ]); setShowFields(newShowFields); }, [parserInput]); const parseResults = useCallback( ( results: GQL.ParseSceneFilenamesQuery["parseSceneFilenames"]["results"] ) => { if (results) { const result = results .map(r => { return new SceneParserResult(r); }) .filter(r => !!r) as SceneParserResult[]; setParserResult(result); determineFieldsToHide(); } }, [determineFieldsToHide] ); useEffect(() => { if (parserInput.findClicked) { setParserResult([]); setIsLoading(true); const parserFilter = { q: parserInput.pattern, page: parserInput.page, per_page: parserInput.pageSize, sort: "path", direction: GQL.SortDirectionEnum.Asc }; const parserInputData = { ignoreWords: parserInput.ignoreWords, whitespaceCharacters: parserInput.whitespaceCharacters, capitalizeTitle: parserInput.capitalizeTitle }; StashService.queryParseSceneFilenames(parserFilter, parserInputData) .then(response => { const result = response.data.parseSceneFilenames; if (result) { parseResults(result.results); setTotalItems(result.count); } }) .catch(err => Toast.error(err)) .finally(() => setIsLoading(false)); } }, [parserInput, parseResults, Toast]); function onPageSizeChanged(newSize: number) { const newInput = _.clone(parserInput); newInput.page = 1; newInput.pageSize = newSize; setParserInput(newInput); } function onPageChanged(newPage: number) { if (newPage !== parserInput.page) { const newInput = _.clone(parserInput); newInput.page = newPage; setParserInput(newInput); } } function onFindClicked(input: IParserInput) { input.page = 1; input.findClicked = true; setParserInput(input); setTotalItems(0); } function getScenesUpdateData() { return parserResult .filter(result => result.isChanged()) .map(result => result.toSceneUpdateInput()); } async function onApply() { setIsLoading(true); try { await updateScenes(); Toast.success({ content: "Updated scenes" }); } catch (e) { Toast.error(e); } setIsLoading(false); } useEffect(() => { const newAllTitleSet = !parserResult.some(r => { return !r.title.set; }); const newAllDateSet = !parserResult.some(r => { return !r.date.set; }); const newAllPerformerSet = !parserResult.some(r => { return !r.performerIds.set; }); const newAllTagSet = !parserResult.some(r => { return !r.tagIds.set; }); const newAllStudioSet = !parserResult.some(r => { return !r.studioId.set; }); setAllTitleSet(newAllTitleSet); setAllDateSet(newAllDateSet); setAllTagSet(newAllPerformerSet); setAllTagSet(newAllTagSet); setAllStudioSet(newAllStudioSet); }, [parserResult]); function onSelectAllTitleSet(selected: boolean) { const newResult = [...parserResult]; newResult.forEach(r => { r.title.set = selected; }); setParserResult(newResult); setAllTitleSet(selected); } function onSelectAllDateSet(selected: boolean) { const newResult = [...parserResult]; newResult.forEach(r => { r.date.set = selected; }); setParserResult(newResult); setAllDateSet(selected); } function onSelectAllPerformerSet(selected: boolean) { const newResult = [...parserResult]; newResult.forEach(r => { r.performerIds.set = selected; }); setParserResult(newResult); setAllPerformerSet(selected); } function onSelectAllTagSet(selected: boolean) { const newResult = [...parserResult]; newResult.forEach(r => { r.tagIds.set = selected; }); setParserResult(newResult); setAllTagSet(selected); } function onSelectAllStudioSet(selected: boolean) { const newResult = [...parserResult]; newResult.forEach(r => { r.studioId.set = selected; }); setParserResult(newResult); setAllStudioSet(selected); } interface ISceneParserFieldProps { parserResult: ParserResult; className?: string; fieldName: string; onSetChanged: (set: boolean) => void; onValueChanged: (value: any) => void; originalParserResult?: ParserResult; renderOriginalInputField: (props: ISceneParserFieldProps) => JSX.Element; renderNewInputField: ( props: ISceneParserFieldProps, onChange: (event: any) => void ) => JSX.Element; } function SceneParserField(props: ISceneParserFieldProps) { function maybeValueChanged(value: any) { if (value !== props.parserResult.value) { props.onValueChanged(value); } } if (!showFields.get(props.fieldName)) { return null; } return ( <> { props.onSetChanged(!props.parserResult.set); }} /> {props.renderOriginalInputField(props)} {props.renderNewInputField(props, value => maybeValueChanged(value) )} ); } function renderOriginalInputGroup(props: ISceneParserFieldProps) { const result = props.originalParserResult || props.parserResult; return ( ); } interface IInputGroupWrapperProps { parserResult: ParserResult; onChange: (event: any) => void; className?: string; } function InputGroupWrapper(props: IInputGroupWrapperProps) { return ( props.onChange(event.target.value)} /> ); } function renderNewInputGroup( props: ISceneParserFieldProps, onChangeHandler: (value: any) => void ) { return ( { onChangeHandler(value); }} parserResult={props.parserResult} /> ); } interface IHasName { name: string; } function renderOriginalSelect(props: ISceneParserFieldProps) { const result = props.originalParserResult || props.parserResult; const elements = result.originalValue ? Array.isArray(result.originalValue) ? result.originalValue.map((el: IHasName) => el.name) : [result.originalValue.name] : []; return (
{elements.map((name: string) => ( {name} ))}
); } function renderNewMultiSelect( type: "performers" | "tags", props: ISceneParserFieldProps, onChangeHandler: (value: any) => void ) { return ( { const ids = items.map(i => i.id); onChangeHandler(ids); }} initialIds={props.parserResult.value} /> ); } function renderNewPerformerSelect( props: ISceneParserFieldProps, onChangeHandler: (value: any) => void ) { return renderNewMultiSelect("performers", props, onChangeHandler); } function renderNewTagSelect( props: ISceneParserFieldProps, onChangeHandler: (value: any) => void ) { return renderNewMultiSelect("tags", props, onChangeHandler); } function renderNewStudioSelect( props: ISceneParserFieldProps, onChangeHandler: (value: any) => void ) { return ( onChangeHandler(items[0]?.id)} initialIds={props.parserResult.value ? [props.parserResult.value] : []} /> ); } interface ISceneParserRowProps { scene: SceneParserResult; onChange: (changedScene: SceneParserResult) => void; } function SceneParserRow(props: ISceneParserRowProps) { function changeParser(result: ParserResult, set: boolean, value: any) { const newParser = _.clone(result); newParser.set = set; newParser.value = value; return newParser; } function onTitleChanged(set: boolean, value: string | undefined) { const newResult = _.clone(props.scene); newResult.title = changeParser(newResult.title, set, value); props.onChange(newResult); } function onDateChanged(set: boolean, value: string | undefined) { const newResult = _.clone(props.scene); newResult.date = changeParser(newResult.date, set, value); props.onChange(newResult); } function onPerformerIdsChanged(set: boolean, value: string[] | undefined) { const newResult = _.clone(props.scene); newResult.performerIds = changeParser(newResult.performerIds, set, value); props.onChange(newResult); } function onTagIdsChanged(set: boolean, value: string[] | undefined) { const newResult = _.clone(props.scene); newResult.tagIds = changeParser(newResult.tagIds, set, value); props.onChange(newResult); } function onStudioIdChanged(set: boolean, value: string | undefined) { const newResult = _.clone(props.scene); newResult.studioId = changeParser(newResult.studioId, set, value); props.onChange(newResult); } return ( {props.scene.filename} onTitleChanged(set, props.scene.title.value ?? undefined) } onValueChanged={value => onTitleChanged(props.scene.title.set, value)} renderOriginalInputField={renderOriginalInputGroup} renderNewInputField={renderNewInputGroup} /> onDateChanged(set, props.scene.date.value ?? undefined) } onValueChanged={value => onDateChanged(props.scene.date.set, value)} renderOriginalInputField={renderOriginalInputGroup} renderNewInputField={renderNewInputGroup} /> onPerformerIdsChanged( set, props.scene.performerIds.value ?? undefined ) } onValueChanged={value => onPerformerIdsChanged(props.scene.performerIds.set, value) } renderOriginalInputField={renderOriginalSelect} renderNewInputField={renderNewPerformerSelect} /> onTagIdsChanged(set, props.scene.tagIds.value ?? undefined) } onValueChanged={value => onTagIdsChanged(props.scene.tagIds.set, value) } renderOriginalInputField={renderOriginalSelect} renderNewInputField={renderNewTagSelect} /> onStudioIdChanged(set, props.scene.studioId.value ?? undefined) } onValueChanged={value => onStudioIdChanged(props.scene.studioId.set, value) } renderOriginalInputField={renderOriginalSelect} renderNewInputField={renderNewStudioSelect} /> ); } function onChange(scene: SceneParserResult, changedScene: SceneParserResult) { const newResult = [...parserResult]; const index = newResult.indexOf(scene); newResult[index] = changedScene; setParserResult(newResult); } function renderHeader( fieldName: string, allSet: boolean, onAllSet: (set: boolean) => void ) { if (!showFields.get(fieldName)) { return null; } return ( <> { onAllSet(!allSet); }} /> {fieldName} ); } function renderTable() { if (parserResult.length === 0) { return undefined; } return ( <>
{renderHeader("Title", allTitleSet, onSelectAllTitleSet)} {renderHeader("Date", allDateSet, onSelectAllDateSet)} {renderHeader( "Performers", allPerformerSet, onSelectAllPerformerSet )} {renderHeader("Tags", allTagSet, onSelectAllTagSet)} {renderHeader("Studio", allStudioSet, onSelectAllStudioSet)} {parserResult.map(scene => ( onChange(scene, changedScene)} /> ))}
Filename
onPageChanged(page)} />
); } return (

Scene Filename Parser

onFindClicked(input)} onPageSizeChanged={onPageSizeChanged} showFields={showFields} setShowFields={setShowFields} /> {isLoading && } {renderTable()}
); };