feat: add usenet support (#543)

* feat(autobrr): implement usenet support

* feat(sonarr): implement usenet support

* feat(radarr): implement usenet support

* feat(announce): implement usenet support

* announce: cast a line

* feat(release): prevent unknown protocol transfer

* release: lines for days.

* feat: add newznab and sabnzbd support

* feat: add category to sabnzbd

* feat(newznab): map categories

* feat(newznab): map categories

---------

Co-authored-by: ze0s <43699394+zze0s@users.noreply.github.com>
Co-authored-by: ze0s <ze0s@riseup.net>
This commit is contained in:
Kyle Sanderson 2023-03-04 11:27:18 -08:00 committed by GitHub
parent b2d93d50c5
commit 13a74f7cc8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1588 additions and 37 deletions

View file

@ -6,6 +6,7 @@ export interface radioFieldsetOption {
label: string;
description: string;
value: string;
type?: string;
}
interface props {
@ -75,7 +76,7 @@ function RadioFieldsetWide({ name, legend, options }: props) {
)}
aria-hidden="true"
/>
<div className="ml-3 flex flex-col">
<div className="ml-3 flex flex-col w-full">
<RadioGroup.Label
as="span"
className={classNames(
@ -83,7 +84,10 @@ function RadioFieldsetWide({ name, legend, options }: props) {
checked ? "font-bold" : "font-medium"
)}
>
{setting.label}
<div className="flex justify-between">
{setting.label}
{setting.type && <span className="rounded bg-orange-500 text-orange-900 px-1 ml-2 text-sm">{setting.type}</span>}
</div>
</RadioGroup.Label>
<RadioGroup.Description
as="span"

View file

@ -217,6 +217,7 @@ export interface RadioFieldsetOption {
label: string;
description: string;
value: ActionType;
type?: string;
}
export const DownloadClientTypeOptions: RadioFieldsetOption[] = [
@ -274,6 +275,12 @@ export const DownloadClientTypeOptions: RadioFieldsetOption[] = [
label: "Readarr",
description: "Send to Readarr and let it decide",
value: "READARR"
},
{
label: "Sabnzbd",
description: "Add nzbs directly to Sabnzbd",
value: "SABNZBD",
type: "nzb"
}
];
@ -288,7 +295,8 @@ export const DownloadClientTypeNameMap: Record<DownloadClientType | string, stri
"SONARR": "Sonarr",
"LIDARR": "Lidarr",
"WHISPARR": "Whisparr",
"READARR": "Readarr"
"READARR": "Readarr",
"SABNZBD": "Sabnzbd"
};
export const ActionTypeOptions: RadioFieldsetOption[] = [
@ -306,7 +314,8 @@ export const ActionTypeOptions: RadioFieldsetOption[] = [
{ label: "Sonarr", description: "Send to Sonarr and let it decide", value: "SONARR" },
{ label: "Lidarr", description: "Send to Lidarr and let it decide", value: "LIDARR" },
{ label: "Whisparr", description: "Send to Whisparr and let it decide", value: "WHISPARR" },
{ label: "Readarr", description: "Send to Readarr and let it decide", value: "READARR" }
{ label: "Readarr", description: "Send to Readarr and let it decide", value: "READARR" },
{ label: "Sabnzbd", description: "Add to Sabnzbd", value: "SABNZBD" }
];
export const ActionTypeNameMap = {
@ -324,7 +333,8 @@ export const ActionTypeNameMap = {
"SONARR": "Sonarr",
"LIDARR": "Lidarr",
"WHISPARR": "Whisparr",
"READARR": "Readarr"
"READARR": "Readarr",
"SABNZBD": "Sabnzbd"
};
export const ActionContentLayoutOptions: SelectGenericOption<ActionContentLayout>[] = [

View file

@ -240,22 +240,71 @@ function FormFieldsTransmission() {
);
}
function FormFieldsSabnzbd() {
const {
values: { port, tls, settings }
} = useFormikContext<InitialValues>();
return (
<div className="flex flex-col space-y-4 px-1 py-6 sm:py-0 sm:space-y-0">
<TextFieldWide
name="host"
label="Host"
help="Eg. ip:port"
// tooltip={<div><p>See guides for how to connect to qBittorrent for various server types in our docs.</p><br /><p>Dedicated servers:</p><a href='https://autobrr.com/configuration/download-clients/dedicated#qbittorrent' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/configuration/download-clients/dedicated#qbittorrent</a><p>Shared seedbox providers:</p><a href='https://autobrr.com/configuration/download-clients/shared-seedboxes#qbittorrent' className='text-blue-400 visited:text-blue-400' target='_blank'>https://autobrr.com/configuration/download-clients/shared-seedboxes#qbittorrent</a></div>}
/>
{port > 0 && (
<NumberFieldWide
name="port"
label="Port"
help="port for Sabnzbd"
/>
)}
<SwitchGroupWide name="tls" label="TLS" />
{tls && (
<SwitchGroupWide
name="tls_skip_verify"
label="Skip TLS verification (insecure)"
/>
)}
{/*<TextFieldWide name="username" label="Username" />*/}
{/*<PasswordFieldWide name="password" label="Password" />*/}
<PasswordFieldWide name="settings.apikey" label="API key" />
<SwitchGroupWide name="settings.basic.auth" label="Basic auth" />
{settings.basic?.auth === true && (
<>
<TextFieldWide name="settings.basic.username" label="Username" />
<PasswordFieldWide name="settings.basic.password" label="Password" />
</>
)}
</div>
);
}
export interface componentMapType {
[key: string]: React.ReactElement;
}
export const componentMap: componentMapType = {
DELUGE_V1: <FormFieldsDeluge/>,
DELUGE_V2: <FormFieldsDeluge/>,
QBITTORRENT: <FormFieldsQbit/>,
DELUGE_V1: <FormFieldsDeluge />,
DELUGE_V2: <FormFieldsDeluge />,
QBITTORRENT: <FormFieldsQbit />,
RTORRENT: <FormFieldsRTorrent />,
TRANSMISSION: <FormFieldsTransmission/>,
TRANSMISSION: <FormFieldsTransmission />,
PORLA: <FormFieldsPorla />,
RADARR: <FormFieldsArr/>,
SONARR: <FormFieldsArr/>,
LIDARR: <FormFieldsArr/>,
WHISPARR: <FormFieldsArr/>,
READARR: <FormFieldsArr/>
RADARR: <FormFieldsArr />,
SONARR: <FormFieldsArr />,
LIDARR: <FormFieldsArr />,
WHISPARR: <FormFieldsArr />,
READARR: <FormFieldsArr />,
SABNZBD: <FormFieldsSabnzbd />
};
function FormFieldsRulesBasic() {

View file

@ -203,6 +203,30 @@ function FormFieldsTorznab() {
);
}
function FormFieldsNewznab() {
const {
values: { interval }
} = useFormikContext<InitialValues>();
return (
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
<TextFieldWide
name="url"
label="URL"
help="Newznab url"
/>
<PasswordFieldWide name="api_key" label="API key" />
{interval < 15 && <WarningLabel />}
<NumberFieldWide name="interval" label="Refresh interval" help="Minutes. Recommended 15-30. Too low and risk ban."/>
<NumberFieldWide name="timeout" label="Refresh timeout" help="Seconds to wait before cancelling refresh."/>
<NumberFieldWide name="max_age" label="Max age" help="Seconds. Will not grab older than this value."/>
</div>
);
}
function FormFieldsRSS() {
const {
values: { interval }
@ -230,5 +254,6 @@ function FormFieldsRSS() {
const componentMap: componentMapType = {
TORZNAB: <FormFieldsTorznab />,
NEWZNAB: <FormFieldsNewznab />,
RSS: <FormFieldsRSS />
};

View file

@ -100,7 +100,7 @@ const IrcSettingFields = (ind: IndexerDefinition, indexer: string) => {
}
};
const FeedSettingFields = (ind: IndexerDefinition, indexer: string) => {
const TorznabFeedSettingFields = (ind: IndexerDefinition, indexer: string) => {
if (indexer !== "") {
return (
<Fragment>
@ -139,6 +139,37 @@ const FeedSettingFields = (ind: IndexerDefinition, indexer: string) => {
}
};
const NewznabFeedSettingFields = (ind: IndexerDefinition, indexer: string) => {
if (indexer !== "") {
return (
<Fragment>
{ind && ind.newznab && ind.newznab.settings && (
<div className="">
<div className="px-4 space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Newznab</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-200">
Newznab feed
</p>
</div>
<TextFieldWide name="name" label="Name" defaultValue="" />
{ind.newznab.settings.map((f: IndexerSetting, idx: number) => {
switch (f.type) {
case "text":
return <TextFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} validate={validateField(f)} />;
case "secret":
return <PasswordFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} validate={validateField(f)} />;
}
return null;
})}
</div>
)}
</Fragment>
);
}
};
const RSSFeedSettingFields = (ind: IndexerDefinition, indexer: string) => {
if (indexer !== "") {
return (
@ -274,6 +305,31 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
});
return;
} else if (formData.implementation === "newznab") {
formData.url = formData.feed.url;
const createFeed: FeedCreate = {
name: formData.name,
enabled: false,
type: "NEWZNAB",
url: formData.feed.newznab_url,
api_key: formData.feed.api_key,
interval: 30,
timeout: 60,
indexer_id: 0,
settings: formData.feed.settings
};
mutation.mutate(formData as Indexer, {
onSuccess: (indexer) => {
// @eslint-ignore
createFeed.indexer_id = indexer.id;
feedMutation.mutate(createFeed);
}
});
return;
} else if (formData.implementation === "rss") {
const createFeed: FeedCreate = {
name: formData.name,
@ -482,7 +538,8 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
</div>
{IrcSettingFields(indexer, values.identifier)}
{FeedSettingFields(indexer, values.identifier)}
{TorznabFeedSettingFields(indexer, values.identifier)}
{NewznabFeedSettingFields(indexer, values.identifier)}
{RSSFeedSettingFields(indexer, values.identifier)}
</div>

View file

@ -449,6 +449,26 @@ const TypeForm = ({ action, idx, clients }: TypeFormProps) => {
</div>
);
case "SABNZBD":
return (
<div>
<div className="mt-6 grid grid-cols-12 gap-6">
<DownloadClientSelect
name={`actions.${idx}.client_id`}
action={action}
clients={clients}
/>
<TextField
name={`actions.${idx}.category`}
label="Category"
columns={6}
placeholder="eg. category"
tooltip={<CustomTooltip anchorId={`actions.${idx}.category`} clickable={true}><p>Category must exist already.</p></CustomTooltip>} />
</div>
</div>
);
default:
return null;
}

View file

@ -19,6 +19,12 @@ const ImplementationBadgeTorznab = () => (
</span>
);
const ImplementationBadgeNewznab = () => (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-blue-200 dark:bg-blue-400 text-blue-800 dark:text-blue-800">
Newznab
</span>
);
const ImplementationBadgeRSS = () => (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-amber-200 dark:bg-amber-400 text-amber-800 dark:text-amber-800">
RSS
@ -28,6 +34,7 @@ const ImplementationBadgeRSS = () => (
export const ImplementationBadges: componentMapType = {
irc: <ImplementationBadgeIRC />,
torznab: <ImplementationBadgeTorznab />,
newznab: <ImplementationBadgeNewznab />,
rss: <ImplementationBadgeRSS />
};

View file

@ -1,15 +1,16 @@
type DownloadClientType =
"QBITTORRENT" |
"DELUGE_V1" |
"DELUGE_V2" |
"RTORRENT" |
"TRANSMISSION" |
"PORLA" |
"RADARR" |
"SONARR" |
"LIDARR" |
"WHISPARR" |
"READARR";
"QBITTORRENT" |
"DELUGE_V1" |
"DELUGE_V2" |
"RTORRENT" |
"TRANSMISSION" |
"PORLA" |
"RADARR" |
"SONARR" |
"LIDARR" |
"WHISPARR" |
"READARR" |
"SABNZBD";
// export enum DownloadClientTypeEnum {
// QBITTORRENT = "QBITTORRENT",

View file

@ -24,7 +24,7 @@ interface FeedSettings {
type FeedDownloadType = "MAGNET" | "TORRENT";
type FeedType = "TORZNAB" | "RSS";
type FeedType = "TORZNAB" | "NEWZNAB" | "RSS";
interface FeedCreate {
name: string;

View file

@ -24,6 +24,7 @@ interface IndexerDefinition {
settings: IndexerSetting[];
irc: IndexerIRC;
torznab: IndexerTorznab;
newznab?: IndexerTorznab;
rss: IndexerFeed;
parse: IndexerParse;
}