Skip to content

Commit

Permalink
Add scraper list page (stashapp#833)
Browse files Browse the repository at this point in the history
  • Loading branch information
WithoutPants authored Oct 13, 2020
1 parent 2b1637a commit 52929df
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 0 deletions.
1 change: 1 addition & 0 deletions ui/v2.5/src/components/Changelog/versions/v040.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
### ✨ New Features
* Add scrapers list setting page.
* Add support for individual images and manual creation of galleries.
* Add various fields to galleries.
* Add partial import from zip file.
Expand Down
7 changes: 7 additions & 0 deletions ui/v2.5/src/components/Settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { SettingsInterfacePanel } from "./SettingsInterfacePanel";
import { SettingsLogsPanel } from "./SettingsLogsPanel";
import { SettingsTasksPanel } from "./SettingsTasksPanel/SettingsTasksPanel";
import { SettingsPluginsPanel } from "./SettingsPluginsPanel";
import { SettingsScrapersPanel } from "./SettingsScrapersPanel";

export const Settings: React.FC = () => {
const location = useLocation();
Expand Down Expand Up @@ -35,6 +36,9 @@ export const Settings: React.FC = () => {
<Nav.Item>
<Nav.Link eventKey="tasks">Tasks</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="scrapers">Scrapers</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="plugins">Plugins</Nav.Link>
</Nav.Item>
Expand All @@ -58,6 +62,9 @@ export const Settings: React.FC = () => {
<Tab.Pane eventKey="tasks">
<SettingsTasksPanel />
</Tab.Pane>
<Tab.Pane eventKey="scrapers">
<SettingsScrapersPanel />
</Tab.Pane>
<Tab.Pane eventKey="plugins">
<SettingsPluginsPanel />
</Tab.Pane>
Expand Down
238 changes: 238 additions & 0 deletions ui/v2.5/src/components/Settings/SettingsScrapersPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import React, { useState } from "react";
import { Button } from "react-bootstrap";
import {
mutateReloadScrapers,
useListMovieScrapers,
useListPerformerScrapers,
useListSceneScrapers,
} from "src/core/StashService";
import { useToast } from "src/hooks";
import { TextUtils } from "src/utils";
import { Icon, LoadingIndicator } from "src/components/Shared";
import { ScrapeType } from "src/core/generated-graphql";

interface IURLList {
urls: string[];
}

const URLList: React.FC<IURLList> = ({ urls }) => {
const maxCollapsedItems = 5;
const [expanded, setExpanded] = useState<boolean>(false);

function linkSite(url: string) {
const u = new URL(url);
return `${u.protocol}//${u.host}`;
}

function renderLink(url?: string) {
if (url) {
const sanitised = TextUtils.sanitiseURL(url);
const siteURL = linkSite(sanitised!);

return (
<a
href={siteURL}
className="link"
target="_blank"
rel="noopener noreferrer"
>
{sanitised}
</a>
);
}
}

function getListItems() {
const items = urls.map((u) => <li key={u}>{renderLink(u)}</li>);

if (items.length > maxCollapsedItems) {
if (!expanded) {
items.length = maxCollapsedItems;
}

items.push(
<li key="expand/collapse">
<Button onClick={() => setExpanded(!expanded)} variant="link">
{expanded ? "less" : "more"}
</Button>
</li>
);
}

return items;
}

return <ul>{getListItems()}</ul>;
};

export const SettingsScrapersPanel: React.FC = () => {
const Toast = useToast();
const {
data: performerScrapers,
loading: loadingPerformers,
} = useListPerformerScrapers();
const {
data: sceneScrapers,
loading: loadingScenes,
} = useListSceneScrapers();
const {
data: movieScrapers,
loading: loadingMovies,
} = useListMovieScrapers();

async function onReloadScrapers() {
await mutateReloadScrapers().catch((e) => Toast.error(e));
}

function renderPerformerScrapeTypes(types: ScrapeType[]) {
const typeStrings = types
.filter((t) => t !== ScrapeType.Fragment)
.map((t) => {
switch (t) {
case ScrapeType.Name:
return "Search by name";
default:
return t;
}
});

return (
<ul>
{typeStrings.map((t) => (
<li key={t}>{t}</li>
))}
</ul>
);
}

function renderSceneScrapeTypes(types: ScrapeType[]) {
const typeStrings = types.map((t) => {
switch (t) {
case ScrapeType.Fragment:
return "Scene Metadata";
default:
return t;
}
});

return (
<ul>
{typeStrings.map((t) => (
<li key={t}>{t}</li>
))}
</ul>
);
}

function renderMovieScrapeTypes(types: ScrapeType[]) {
const typeStrings = types.map((t) => {
switch (t) {
case ScrapeType.Fragment:
return "Movie Metadata";
default:
return t;
}
});

return (
<ul>
{typeStrings.map((t) => (
<li key={t}>{t}</li>
))}
</ul>
);
}

function renderURLs(urls: string[]) {
return <URLList urls={urls} />;
}

function renderSceneScrapers() {
const elements = (sceneScrapers?.listSceneScrapers ?? []).map((scraper) => (
<tr key={scraper.id}>
<td>{scraper.name}</td>
<td>
{renderSceneScrapeTypes(scraper.scene?.supported_scrapes ?? [])}
</td>
<td>{renderURLs(scraper.scene?.urls ?? [])}</td>
</tr>
));

return renderTable("Scene scrapers", elements);
}

function renderPerformerScrapers() {
const elements = (performerScrapers?.listPerformerScrapers ?? []).map(
(scraper) => (
<tr key={scraper.id}>
<td>{scraper.name}</td>
<td>
{renderPerformerScrapeTypes(
scraper.performer?.supported_scrapes ?? []
)}
</td>
<td>{renderURLs(scraper.performer?.urls ?? [])}</td>
</tr>
)
);

return renderTable("Performer scrapers", elements);
}

function renderMovieScrapers() {
const elements = (movieScrapers?.listMovieScrapers ?? []).map((scraper) => (
<tr key={scraper.id}>
<td>{scraper.name}</td>
<td>
{renderMovieScrapeTypes(scraper.movie?.supported_scrapes ?? [])}
</td>
<td>{renderURLs(scraper.movie?.urls ?? [])}</td>
</tr>
));

return renderTable("Movie scrapers", elements);
}

function renderTable(title: string, elements: JSX.Element[]) {
if (elements.length > 0) {
return (
<div className="mb-2">
<h5>{title}</h5>
<table className="scraper-table">
<thead>
<tr>
<th>Name</th>
<th>Supported types</th>
<th>URLs</th>
</tr>
</thead>
<tbody>{elements}</tbody>
</table>
</div>
);
}
}

if (loadingScenes || loadingPerformers || loadingMovies)
return <LoadingIndicator />;

return (
<>
<h4>Scrapers</h4>
<div className="mb-3">
<Button onClick={() => onReloadScrapers()}>
<span className="fa-icon">
<Icon icon="sync-alt" />
</span>
<span>Reload scrapers</span>
</Button>
</div>

<div>
{renderSceneScrapers()}
{renderPerformerScrapers()}
{renderMovieScrapers()}
</div>
</>
);
};
30 changes: 30 additions & 0 deletions ui/v2.5/src/components/Settings/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,33 @@
#configuration-tabs-tabpane-tasks h5 {
margin-bottom: 1em;
}

.scraper-table {
display: block;
margin-bottom: 16px;
overflow: auto;
width: 100%;

tr {
border-top: 1px solid #181513;

&:nth-child(2n) {
background-color: #2c3b47;
}
}

th,
td {
border: 1px solid #181513;
padding: 6px 13px;
}

ul {
margin-bottom: 0;
padding-left: 0;
}

li {
list-style: none;
}
}
5 changes: 5 additions & 0 deletions ui/v2.5/src/core/StashService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,11 @@ export const queryStashBoxScene = (stashBoxIndex: number, sceneID: string) =>
export const mutateReloadScrapers = () =>
client.mutate<GQL.ReloadScrapersMutation>({
mutation: GQL.ReloadScrapersDocument,
refetchQueries: [
GQL.refetchListMovieScrapersQuery(),
GQL.refetchListPerformerScrapersQuery(),
GQL.refetchListSceneScrapersQuery(),
],
});

export const mutateReloadPlugins = () =>
Expand Down

0 comments on commit 52929df

Please sign in to comment.