Add custom scripts for rss download

This commit is contained in:
2023-10-08 22:33:44 +02:00
parent b32abb6615
commit 875ae36d17
7 changed files with 102 additions and 61 deletions

View File

@@ -101,6 +101,9 @@ const DownloadRuleForm: FC<DownloadRuleFormProps> = ({
)} )}
</Textbox> </Textbox>
</FormRow> </FormRow>
<FormRow>
<Textbox id="script" label={i18n._('feeds.script')} defaultValue={rule.script} />
</FormRow>
<FormRow> <FormRow>
<FormRowItem> <FormRowItem>
<FilesystemBrowserTextbox <FilesystemBrowserTextbox

View File

@@ -17,6 +17,7 @@ const initialRule: AddRuleOptions = {
feedIDs: [], feedIDs: [],
match: '', match: '',
exclude: '', exclude: '',
script: '',
tags: [], tags: [],
destination: '', destination: '',
startOnLoad: false, startOnLoad: false,
@@ -36,7 +37,12 @@ const validatedFields = {
error: 'feeds.validation.must.specify.label', error: 'feeds.validation.must.specify.label',
}, },
match: { match: {
isValid: (value: string | undefined) => isNotEmpty(value) && isRegExValid(value), isValid: (value: string | undefined) => {
if (isNotEmpty(value)) {
return isRegExValid(value);
}
return true;
},
error: 'feeds.validation.invalid.regular.expression', error: 'feeds.validation.invalid.regular.expression',
}, },
exclude: { exclude: {
@@ -64,6 +70,7 @@ interface RuleFormData {
feedID: string; feedID: string;
label: string; label: string;
match: string; match: string;
script: string;
tags: string; tags: string;
isBasePath: boolean; isBasePath: boolean;
startOnLoad: boolean; startOnLoad: boolean;
@@ -140,6 +147,7 @@ const DownloadRulesTab: FC = () => {
field: formData.field, field: formData.field,
match: formData.match ?? initialRule.match, match: formData.match ?? initialRule.match,
exclude: formData.exclude ?? initialRule.exclude, exclude: formData.exclude ?? initialRule.exclude,
script: formData.script ?? initialRule.script,
destination: formData.destination ?? initialRule.destination, destination: formData.destination ?? initialRule.destination,
tags: formData.tags?.split(',') ?? initialRule.tags, tags: formData.tags?.split(',') ?? initialRule.tags,
startOnLoad: formData.startOnLoad ?? initialRule.startOnLoad, startOnLoad: formData.startOnLoad ?? initialRule.startOnLoad,

View File

@@ -102,6 +102,7 @@
"feeds.no.items.matching": "No items matching search term.", "feeds.no.items.matching": "No items matching search term.",
"feeds.no.rules.defined": "No rules defined.", "feeds.no.rules.defined": "No rules defined.",
"feeds.regEx": "RegEx", "feeds.regEx": "RegEx",
"feeds.script": "Script",
"feeds.search": "Search term", "feeds.search": "Search term",
"feeds.search.term": "Search term", "feeds.search.term": "Search term",
"feeds.select.feed": "Select Feed", "feeds.select.feed": "Select Feed",

View File

@@ -220,6 +220,7 @@ describe('PUT /api/feed-monitor/rules', () => {
feedIDs: [''], feedIDs: [''],
match: '', match: '',
exclude: '.*', exclude: '.*',
script: '',
destination: tempDirectory, destination: tempDirectory,
tags: ['FeedItem'], tags: ['FeedItem'],
startOnLoad: false, startOnLoad: false,

View File

@@ -70,6 +70,7 @@ class FeedService extends BaseService<Record<string, never>> {
field: rule.field, field: rule.field,
match: rule.match, match: rule.match,
exclude: rule.exclude, exclude: rule.exclude,
script: rule.script,
startOnLoad: rule.startOnLoad, startOnLoad: rule.startOnLoad,
isBasePath: rule.isBasePath, isBasePath: rule.isBasePath,
}); });
@@ -259,13 +260,12 @@ class FeedService extends BaseService<Record<string, never>> {
} }
handleNewItems = (feedReaderOptions: FeedReaderOptions, feedItems: Array<FeedItem>): void => { handleNewItems = (feedReaderOptions: FeedReaderOptions, feedItems: Array<FeedItem>): void => {
this.getPreviouslyMatchedUrls() this.getPreviouslyMatchedUrls().then(async (previouslyMatchedUrls) => {
.then((previouslyMatchedUrls) => {
const {feedID, feedLabel} = feedReaderOptions; const {feedID, feedLabel} = feedReaderOptions;
const applicableRules = this.rules[feedID]; const applicableRules = this.rules[feedID];
if (!applicableRules) return; if (!applicableRules) return;
const itemsMatchingRules = getFeedItemsMatchingRules(feedItems, applicableRules); const itemsMatchingRules = await getFeedItemsMatchingRules(feedItems, applicableRules);
const itemsToDownload = itemsMatchingRules.filter((item) => const itemsToDownload = itemsMatchingRules.filter((item) =>
item.urls.some((url) => !previouslyMatchedUrls.includes(url)), item.urls.some((url) => !previouslyMatchedUrls.includes(url)),
); );
@@ -274,7 +274,8 @@ class FeedService extends BaseService<Record<string, never>> {
return; return;
} }
Promise.all( try {
const ArrayOfURLArrays = await Promise.all(
itemsToDownload.map(async (item): Promise<Array<string>> => { itemsToDownload.map(async (item): Promise<Array<string>> => {
const {urls, destination, start, tags, ruleID} = item; const {urls, destination, start, tags, ruleID} = item;
@@ -298,7 +299,7 @@ class FeedService extends BaseService<Record<string, never>> {
return urls; return urls;
}), }),
).then((ArrayOfURLArrays) => { );
const addedURLs = ArrayOfURLArrays.reduce( const addedURLs = ArrayOfURLArrays.reduce(
(URLArray: Array<string>, urls: Array<string>) => URLArray.concat(urls), (URLArray: Array<string>, urls: Array<string>) => URLArray.concat(urls),
[], [],
@@ -317,9 +318,10 @@ class FeedService extends BaseService<Record<string, never>> {
})), })),
); );
this.services?.torrentService.fetchTorrentList(); this.services?.torrentService.fetchTorrentList();
} catch (e) {
console.error(e);
}
}); });
})
.catch(console.error);
}; };
async removeItem(id: string): Promise<void> { async removeItem(id: string): Promise<void> {

View File

@@ -1,3 +1,5 @@
import {spawn} from 'node:child_process';
import type {FeedItem} from 'feedsub'; import type {FeedItem} from 'feedsub';
import type {AddTorrentByURLOptions} from '../../shared/schema/api/torrents'; import type {AddTorrentByURLOptions} from '../../shared/schema/api/torrents';
@@ -53,17 +55,37 @@ export const getTorrentUrlsFromFeedItem = (feedItem: FeedItem): Array<string> =>
return []; return [];
}; };
export const getFeedItemsMatchingRules = ( const execAsync = (...command: string[]) => {
const p = spawn(command[0], command.slice(1));
return new Promise((resolveFunc) => {
p.stdout.on('data', (x) => {
process.stdout.write(x.toString());
});
p.stderr.on('data', (x) => {
process.stderr.write(x.toString());
});
p.on('exit', (code) => {
resolveFunc(code);
});
});
};
export const getFeedItemsMatchingRules = async (
feedItems: Array<FeedItem>, feedItems: Array<FeedItem>,
rules: Array<Rule>, rules: Array<Rule>,
): Array<PendingDownloadItems> => { ): Promise<Array<PendingDownloadItems>> => {
return feedItems.reduce((matchedItems: Array<PendingDownloadItems>, feedItem) => { const matchedItems: Array<PendingDownloadItems> = [];
rules.forEach((rule) => {
const matchField = rule.field ? (feedItem[rule.field] as string) : (feedItem.title as string);
const isMatched = new RegExp(rule.match, 'gi').test(matchField);
const isExcluded = rule.exclude !== '' && new RegExp(rule.exclude, 'gi').test(matchField);
if (isMatched && !isExcluded) { await Promise.all(
feedItems.map(async (feedItem) => {
await Promise.all(
rules.map(async (rule) => {
const matchField = rule.field ? (feedItem[rule.field] as string) : (feedItem.title as string);
const isMatched = rule.match === '' || new RegExp(rule.match, 'gi').test(matchField);
const isExcluded = rule.exclude !== '' && new RegExp(rule.exclude, 'gi').test(matchField);
const scriptMatch = rule.script === '' || (await execAsync(rule.script, matchField)) === 80;
if (isMatched && !isExcluded && scriptMatch) {
const torrentUrls = getTorrentUrlsFromFeedItem(feedItem); const torrentUrls = getTorrentUrlsFromFeedItem(feedItem);
const isAlreadyDownloaded = matchedItems.some((matchedItem) => const isAlreadyDownloaded = matchedItems.some((matchedItem) =>
torrentUrls.every((url) => matchedItem.urls.includes(url)), torrentUrls.every((url) => matchedItem.urls.includes(url)),
@@ -81,8 +103,10 @@ export const getFeedItemsMatchingRules = (
}); });
} }
} }
}); }),
);
}),
);
return matchedItems; return matchedItems;
}, []);
}; };

View File

@@ -27,6 +27,8 @@ export interface Rule {
match: string; match: string;
// Regular expression to exclude items. // Regular expression to exclude items.
exclude: string; exclude: string;
// Custom script to select if the item should be downloaded (exit with status 80 to download).
script: string;
// Destination path where matched items are downloaded to. // Destination path where matched items are downloaded to.
destination: string; destination: string;
// Tags to be added when items are queued for download. // Tags to be added when items are queued for download.