Add phash generation and dupe checking (#1158)

This commit is contained in:
InfiniteTF
2021-04-12 01:04:40 +02:00
committed by GitHub
parent a2582047ca
commit c38660d209
70 changed files with 4342 additions and 214 deletions

View File

@@ -0,0 +1,270 @@
import React, { useState } from "react";
import { Button, Col, Form, Row, Table } from "react-bootstrap";
import { Link, useHistory } from "react-router-dom";
import { FormattedNumber } from "react-intl";
import querystring from "query-string";
import * as GQL from "src/core/generated-graphql";
import {
LoadingIndicator,
ErrorMessage,
HoverPopover,
} from "src/components/Shared";
import { Pagination } from "src/components/List/Pagination";
import { TextUtils } from "src/utils";
import { DeleteScenesDialog } from "src/components/Scenes/DeleteScenesDialog";
const CLASSNAME = "DuplicateChecker";
export const SettingsDuplicatePanel: React.FC = () => {
const history = useHistory();
const { page, size, distance } = querystring.parse(history.location.search);
const currentPage = Number.parseInt(
Array.isArray(page) ? page[0] : page ?? "1",
10
);
const pageSize = Number.parseInt(
Array.isArray(size) ? size[0] : size ?? "20",
10
);
const hashDistance = Number.parseInt(
Array.isArray(distance) ? distance[0] : distance ?? "0",
10
);
const [isMultiDelete, setIsMultiDelete] = useState(false);
const [checkedScenes, setCheckedScenes] = useState<Record<string, boolean>>(
{}
);
const { data, loading, refetch } = GQL.useFindDuplicateScenesQuery({
fetchPolicy: "no-cache",
variables: { distance: hashDistance },
});
const [deletingScene, setDeletingScene] = useState<
GQL.SlimSceneDataFragment[] | null
>(null);
if (loading) return <LoadingIndicator />;
if (!data) return <ErrorMessage error="Error searching for duplicates." />;
const scenes = data?.findDuplicateScenes ?? [];
const filteredScenes = scenes.slice(
(currentPage - 1) * pageSize,
currentPage * pageSize
);
const checkCount = Object.keys(checkedScenes).filter(
(id) => checkedScenes[id]
).length;
const setQuery = (q: Record<string, string | number | undefined>) => {
history.push({
search: querystring.stringify({
...querystring.parse(history.location.search),
...q,
}),
});
};
function onDeleteDialogClosed(deleted: boolean) {
setDeletingScene(null);
if (deleted) {
refetch();
if (isMultiDelete) setCheckedScenes({});
}
}
const handleCheck = (checked: boolean, sceneID: string) => {
setCheckedScenes({ ...checkedScenes, [sceneID]: checked });
};
const handleDeleteChecked = () => {
setDeletingScene(scenes.flat().filter((s) => checkedScenes[s.id]));
setIsMultiDelete(true);
};
const handleDeleteScene = (scene: GQL.SlimSceneDataFragment) => {
setDeletingScene([scene]);
setIsMultiDelete(false);
};
const renderFilesize = (filesize: string | null | undefined) => {
const { size: parsedSize, unit } = TextUtils.fileSize(
Number.parseInt(filesize ?? "0", 10)
);
return (
<FormattedNumber
value={parsedSize}
style="unit"
unit={unit}
unitDisplay="narrow"
maximumFractionDigits={2}
/>
);
};
return (
<div className={CLASSNAME}>
{deletingScene && (
<DeleteScenesDialog
selected={deletingScene}
onClose={onDeleteDialogClosed}
/>
)}
<h4>Duplicate Scenes</h4>
<Form.Group>
<Row noGutters>
<Form.Label>Search Accuracy</Form.Label>
<Col xs={2}>
<Form.Control
as="select"
onChange={(e) =>
setQuery({
distance:
e.currentTarget.value === "0"
? undefined
: e.currentTarget.value,
page: undefined,
})
}
defaultValue={distance ?? 0}
className="ml-4"
>
<option value={0}>Exact</option>
<option value={4}>High</option>
<option value={8}>Medium</option>
<option value={10}>Low</option>
</Form.Control>
</Col>
</Row>
<Form.Text>
Levels below &ldquo;Exact&rdquo; can take longer to calculate. False
positives might also be returned on lower accuracy levels.
</Form.Text>
</Form.Group>
<div className="d-flex mb-2">
<h6 className="mr-auto align-self-center">
{scenes.length} sets of duplicates found.
</h6>
{checkCount > 0 && (
<Button
className="edit-button"
variant="danger"
onClick={handleDeleteChecked}
>
Delete {checkCount} scene{checkCount > 1 && "s"}
</Button>
)}
<Pagination
itemsPerPage={pageSize}
currentPage={currentPage}
totalItems={scenes.length}
onChangePage={(newPage) =>
setQuery({ page: newPage === 1 ? undefined : newPage })
}
/>
<Form.Control
as="select"
className="w-auto ml-2 btn-secondary"
defaultValue={pageSize}
onChange={(e) =>
setQuery({
size:
e.currentTarget.value === "20"
? undefined
: e.currentTarget.value,
})
}
>
<option value={10}>10</option>
<option value={20}>20</option>
<option value={40}>40</option>
<option value={60}>60</option>
<option value={80}>80</option>
</Form.Control>
</div>
<Table striped className={`${CLASSNAME}-table`}>
<colgroup>
<col className={`${CLASSNAME}-checkbox`} />
<col className={`${CLASSNAME}-sprite`} />
<col className={`${CLASSNAME}-title`} />
<col className={`${CLASSNAME}-duration`} />
<col className={`${CLASSNAME}-filesize`} />
<col className={`${CLASSNAME}-resolution`} />
<col className={`${CLASSNAME}-bitrate`} />
<col className={`${CLASSNAME}-codec`} />
<col className={`${CLASSNAME}-operations`} />
</colgroup>
<thead>
<tr>
<th> </th>
<th> </th>
<th>Title</th>
<th>Duration</th>
<th>Filesize</th>
<th>Resolution</th>
<th>Bitrate</th>
<th>Codec</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{filteredScenes.map((group) =>
group.map((scene, i) => (
<tr className={i === 0 ? "duplicate-group" : ""} key={scene.id}>
<td>
<Form.Check
onChange={(e) =>
handleCheck(e.currentTarget.checked, scene.id)
}
/>
</td>
<td>
<HoverPopover
content={
<img src={scene.paths.sprite ?? ""} alt="" width={600} />
}
placement="right"
>
<img src={scene.paths.sprite ?? ""} alt="" width={100} />
</HoverPopover>
</td>
<td className="text-left">
<Link to={`/scenes/${scene.id}`}>
{scene.title ?? TextUtils.fileNameFromPath(scene.path)}
</Link>
</td>
<td>
{scene.file.duration &&
TextUtils.secondsToTimestamp(scene.file.duration)}
</td>
<td>{renderFilesize(scene.file.size)}</td>
<td>{`${scene.file.width}x${scene.file.height}`}</td>
<td>
<FormattedNumber
value={(scene.file.bitrate ?? 0) / 1000000}
maximumFractionDigits={2}
/>
&nbsp;mbps
</td>
<td>{scene.file.video_codec}</td>
<td>
<Button
className="edit-button"
variant="danger"
onClick={() => handleDeleteScene(scene)}
>
Delete
</Button>
</td>
</tr>
))
)}
</tbody>
</Table>
{scenes.length === 0 && (
<h4 className="text-center mt-4">
No duplicates found. Make sure the phash task has been run.
</h4>
)}
</div>
);
};