Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add scene rating to scene filename parser #432

Merged
merged 13 commits into from
Apr 4, 2020
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
go.mod text eol=lf
go.sum text eol=lf
ui/v2.5/**/*.ts* text eol=lf
ui/v2.5/**/*.scss text eol=lf
18 changes: 18 additions & 0 deletions pkg/manager/filename_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ func initParserFields() {
//I = new ParserField("i", undefined, "Matches any ignored word", false);

ret["d"] = newParserField("d", `(?:\.|-|_)`, false)
ret["rating"] = newParserField("rating", `\d`, true)
ret["performer"] = newParserField("performer", ".*", true)
ret["studio"] = newParserField("studio", ".*", true)
ret["movie"] = newParserField("movie", ".*", true)
Expand Down Expand Up @@ -224,6 +225,10 @@ func newSceneHolder(scene *models.Scene) *sceneHolder {
return &ret
}

func validateRating(rating int) bool {
return rating >= 1 && rating <= 5
}

func validateDate(dateStr string) bool {
splits := strings.Split(dateStr, "-")
if len(splits) != 3 {
Expand Down Expand Up @@ -304,6 +309,14 @@ func (h *sceneHolder) setField(field parserField, value interface{}) {
Valid: true,
}
}
case "rating":
rating, _ := strconv.Atoi(value.(string))
if validateRating(rating) {
h.result.Rating = sql.NullInt64{
Int64: int64(rating),
Valid: true,
}
}
case "performer":
// add performer to list
h.performers = append(h.performers, value.(string))
Expand Down Expand Up @@ -661,6 +674,11 @@ func (p *SceneFilenameParser) setParserResult(h sceneHolder, result *models.Scen
result.Date = &h.result.Date.String
}

if h.result.Rating.Valid {
rating := int(h.result.Rating.Int64)
result.Rating = &rating
}

if len(h.performers) > 0 {
p.setPerformers(h, result)
}
Expand Down
2 changes: 2 additions & 0 deletions ui/v2.5/src/components/SceneFilenameParser/ParserField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export class ParserField {

static Title = new ParserField("title");
static Ext = new ParserField("ext", "File extension");
static Rating = new ParserField("rating");

static I = new ParserField("i", "Matches any ignored word");
static D = new ParserField("d", "Matches any delimiter (.-_)");
Expand All @@ -39,6 +40,7 @@ export class ParserField {
ParserField.Ext,
ParserField.D,
ParserField.I,
ParserField.Rating,
ParserField.Performer,
ParserField.Studio,
ParserField.Tag,
Expand Down
12 changes: 8 additions & 4 deletions ui/v2.5/src/components/SceneFilenameParser/ParserInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export const ParserInput: React.FC<IParserInputProps> = (
</Form.Label>
<InputGroup className="col-8">
<Form.Control
className="text-input"
id="filename-pattern"
onChange={(e: React.FormEvent<HTMLInputElement>) =>
setPattern(e.currentTarget.value)
Expand All @@ -144,8 +145,8 @@ export const ParserInput: React.FC<IParserInputProps> = (
key={item.field}
onSelect={() => addParserField(item)}
>
<span>{item.field}</span>
<span className="ml-auto">{item.helperText}</span>
<span>{item.field || "{}"}</span>
<span className="ml-auto text-muted">{item.helperText}</span>
</Dropdown.Item>
))}
</DropdownButton>
Expand All @@ -160,6 +161,7 @@ export const ParserInput: React.FC<IParserInputProps> = (
<Form.Label className="col-2">Ignored words</Form.Label>
<InputGroup className="col-8">
<Form.Control
className="text-input"
onChange={(e: React.FormEvent<HTMLInputElement>) =>
setIgnoreWords(e.currentTarget.value)
}
Expand All @@ -178,6 +180,7 @@ export const ParserInput: React.FC<IParserInputProps> = (
</Form.Label>
<InputGroup className="col-8">
<Form.Control
className="text-input"
onChange={(e: React.FormEvent<HTMLInputElement>) =>
setWhitespaceCharacters(e.currentTarget.value)
}
Expand Down Expand Up @@ -206,14 +209,15 @@ export const ParserInput: React.FC<IParserInputProps> = (
variant="secondary"
id="recipe-select"
title="Select Parser Recipe"
drop="up"
>
{builtInRecipes.map((item) => (
<Dropdown.Item
key={item.pattern}
onSelect={() => setParserRecipe(item)}
>
<span>{item.pattern}</span>
<span className="mr-auto">{item.description}</span>
<span className="ml-auto text-muted">{item.description}</span>
</Dropdown.Item>
))}
</DropdownButton>
Expand All @@ -237,7 +241,7 @@ export const ParserInput: React.FC<IParserInputProps> = (
props.onPageSizeChanged(parseInt(e.currentTarget.value, 10))
}
defaultValue={props.input.pageSize}
className="col-1 filter-item"
className="col-1 input-control filter-item"
>
{PAGE_SIZE_OPTIONS.map((val) => (
<option key={val} value={val}>
Expand Down
103 changes: 71 additions & 32 deletions ui/v2.5/src/components/SceneFilenameParser/SceneFilenameParser.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* 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 React, { useEffect, useState, useCallback, useRef } from "react";
import { Button, Card, Form, Table } from "react-bootstrap";
import _ from "lodash";
import { StashService } from "src/core/StashService";
Expand All @@ -25,6 +25,7 @@ const initialParserInput = {
const initialShowFieldsState = new Map<string, boolean>([
["Title", true],
["Date", true],
["Rating", true],
["Performers", true],
["Tags", true],
["Studio", true],
Expand All @@ -36,9 +37,12 @@ export const SceneFilenameParser: React.FC = () => {
const [parserInput, setParserInput] = useState<IParserInput>(
initialParserInput
);
const prevParserInputRef = useRef<IParserInput>();
const prevParserInput = prevParserInputRef.current;

const [allTitleSet, setAllTitleSet] = useState<boolean>(false);
const [allDateSet, setAllDateSet] = useState<boolean>(false);
const [allRatingSet, setAllRatingSet] = useState<boolean>(false);
const [allPerformerSet, setAllPerformerSet] = useState<boolean>(false);
const [allTagSet, setAllTagSet] = useState<boolean>(false);
const [allStudioSet, setAllStudioSet] = useState<boolean>(false);
Expand All @@ -54,6 +58,10 @@ export const SceneFilenameParser: React.FC = () => {

const [updateScenes] = StashService.useScenesUpdate(getScenesUpdateData());

useEffect(() => {
prevParserInputRef.current = parserInput;
}, [parserInput]);

const determineFieldsToHide = useCallback(() => {
const { pattern } = parserInput;
const titleSet = pattern.includes("{title}");
Expand All @@ -63,13 +71,15 @@ export const SceneFilenameParser: React.FC = () => {
ParserField.fullDateFields.some((f) => {
return pattern.includes(`{${f.field}}`);
});
const ratingSet = pattern.includes("{rating}");
const performerSet = pattern.includes("{performer}");
const tagSet = pattern.includes("{tag}");
const studioSet = pattern.includes("{studio}");

const newShowFields = new Map<string, boolean>([
["Title", titleSet],
["Date", dateSet],
["Rating", ratingSet],
["Performers", performerSet],
["Tags", tagSet],
["Studio", studioSet],
Expand All @@ -96,37 +106,46 @@ export const SceneFilenameParser: React.FC = () => {
[determineFieldsToHide]
);

const parseSceneFilenames = useCallback(() => {
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]);

useEffect(() => {
// only refresh if parserInput actually changed
if (prevParserInput === parserInput) {
return;
}

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));
parseSceneFilenames();
}
}, [parserInput, parseResults, Toast]);
}, [parserInput, parseSceneFilenames, prevParserInput]);

function onPageSizeChanged(newSize: number) {
const newInput = _.clone(parserInput);
Expand All @@ -144,9 +163,10 @@ export const SceneFilenameParser: React.FC = () => {
}

function onFindClicked(input: IParserInput) {
input.page = 1;
input.findClicked = true;
setParserInput(input);
const newInput = _.clone(input);
newInput.page = 1;
newInput.findClicked = true;
setParserInput(newInput);
setTotalItems(0);
}

Expand All @@ -167,6 +187,9 @@ export const SceneFilenameParser: React.FC = () => {
}

setIsLoading(false);

// trigger a refresh of the results
onFindClicked(parserInput);
}

useEffect(() => {
Expand All @@ -176,6 +199,9 @@ export const SceneFilenameParser: React.FC = () => {
const newAllDateSet = !parserResult.some((r) => {
return !r.date.isSet;
});
const newAllRatingSet = !parserResult.some((r) => {
return !r.rating.isSet;
});
const newAllPerformerSet = !parserResult.some((r) => {
return !r.performers.isSet;
});
Expand All @@ -188,6 +214,7 @@ export const SceneFilenameParser: React.FC = () => {

setAllTitleSet(newAllTitleSet);
setAllDateSet(newAllDateSet);
setAllRatingSet(newAllRatingSet);
setAllTagSet(newAllPerformerSet);
setAllTagSet(newAllTagSet);
setAllStudioSet(newAllStudioSet);
Expand Down Expand Up @@ -215,6 +242,17 @@ export const SceneFilenameParser: React.FC = () => {
setAllDateSet(selected);
}

function onSelectAllRatingSet(selected: boolean) {
const newResult = [...parserResult];

newResult.forEach((r) => {
r.rating.isSet = selected;
});

setParserResult(newResult);
setAllRatingSet(selected);
}

function onSelectAllPerformerSet(selected: boolean) {
const newResult = [...parserResult];

Expand Down Expand Up @@ -295,6 +333,7 @@ export const SceneFilenameParser: React.FC = () => {
<th className="parser-field-filename">Filename</th>
{renderHeader("Title", allTitleSet, onSelectAllTitleSet)}
{renderHeader("Date", allDateSet, onSelectAllDateSet)}
{renderHeader("Rating", allRatingSet, onSelectAllRatingSet)}
{renderHeader(
"Performers",
allPerformerSet,
Expand Down
Loading