mirror of
https://github.com/zoriya/flood.git
synced 2025-12-05 23:06:20 +00:00
Add custom scripts for rss download
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,22 +260,22 @@ 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)),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (itemsToDownload.length === 0) {
|
if (itemsToDownload.length === 0) {
|
||||||
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,28 +299,29 @@ 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),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
this.db.update({type: 'matchedTorrents'}, {$push: {urls: {$each: addedURLs}}}, {upsert: true});
|
this.db.update({type: 'matchedTorrents'}, {$push: {urls: {$each: addedURLs}}}, {upsert: true});
|
||||||
|
|
||||||
this.services?.notificationService.addNotification(
|
this.services?.notificationService.addNotification(
|
||||||
itemsToDownload.map((item) => ({
|
itemsToDownload.map((item) => ({
|
||||||
id: 'notification.feed.torrent.added',
|
id: 'notification.feed.torrent.added',
|
||||||
data: {
|
data: {
|
||||||
title: item.matchTitle,
|
title: item.matchTitle,
|
||||||
feedLabel,
|
feedLabel,
|
||||||
ruleLabel: item.ruleLabel,
|
ruleLabel: item.ruleLabel,
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
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> {
|
||||||
|
|||||||
@@ -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,36 +55,58 @@ 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(
|
||||||
const torrentUrls = getTorrentUrlsFromFeedItem(feedItem);
|
feedItems.map(async (feedItem) => {
|
||||||
const isAlreadyDownloaded = matchedItems.some((matchedItem) =>
|
await Promise.all(
|
||||||
torrentUrls.every((url) => matchedItem.urls.includes(url)),
|
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 (!isAlreadyDownloaded && torrentUrls[0] != null) {
|
if (isMatched && !isExcluded && scriptMatch) {
|
||||||
matchedItems.push({
|
const torrentUrls = getTorrentUrlsFromFeedItem(feedItem);
|
||||||
urls: torrentUrls as [string, ...string[]],
|
const isAlreadyDownloaded = matchedItems.some((matchedItem) =>
|
||||||
tags: rule.tags,
|
torrentUrls.every((url) => matchedItem.urls.includes(url)),
|
||||||
matchTitle: feedItem.title as string,
|
);
|
||||||
ruleID: rule._id,
|
|
||||||
ruleLabel: rule.label,
|
|
||||||
destination: rule.destination,
|
|
||||||
start: rule.startOnLoad,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return matchedItems;
|
if (!isAlreadyDownloaded && torrentUrls[0] != null) {
|
||||||
}, []);
|
matchedItems.push({
|
||||||
|
urls: torrentUrls as [string, ...string[]],
|
||||||
|
tags: rule.tags,
|
||||||
|
matchTitle: feedItem.title as string,
|
||||||
|
ruleID: rule._id,
|
||||||
|
ruleLabel: rule.label,
|
||||||
|
destination: rule.destination,
|
||||||
|
start: rule.startOnLoad,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return matchedItems;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user