mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 16:59:12 +00:00
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:
parent
b2d93d50c5
commit
13a74f7cc8
29 changed files with 1588 additions and 37 deletions
|
@ -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"
|
||||
|
|
|
@ -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>[] = [
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 />
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
};
|
||||
|
||||
|
|
23
web/src/types/Download.d.ts
vendored
23
web/src/types/Download.d.ts
vendored
|
@ -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",
|
||||
|
|
2
web/src/types/Feed.d.ts
vendored
2
web/src/types/Feed.d.ts
vendored
|
@ -24,7 +24,7 @@ interface FeedSettings {
|
|||
|
||||
type FeedDownloadType = "MAGNET" | "TORRENT";
|
||||
|
||||
type FeedType = "TORZNAB" | "RSS";
|
||||
type FeedType = "TORZNAB" | "NEWZNAB" | "RSS";
|
||||
|
||||
interface FeedCreate {
|
||||
name: string;
|
||||
|
|
1
web/src/types/Indexer.d.ts
vendored
1
web/src/types/Indexer.d.ts
vendored
|
@ -24,6 +24,7 @@ interface IndexerDefinition {
|
|||
settings: IndexerSetting[];
|
||||
irc: IndexerIRC;
|
||||
torznab: IndexerTorznab;
|
||||
newznab?: IndexerTorznab;
|
||||
rss: IndexerFeed;
|
||||
parse: IndexerParse;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue