2025-01-07 12:25:19 +01:00
|
|
|
import { NoNewListingsWarning } from './errors.js';
|
|
|
|
|
import { setKnownListings, getKnownListings } from './services/storage/listingsStorage.js';
|
2023-03-13 13:42:43 +01:00
|
|
|
import * as notify from './notification/notify.js';
|
2024-12-17 12:38:28 +01:00
|
|
|
import Extractor from './services/extractor/extractor.js';
|
2023-03-13 13:42:43 +01:00
|
|
|
import urlModifier from './services/queryStringMutator.js';
|
2024-12-17 12:38:28 +01:00
|
|
|
|
2020-02-26 09:05:20 +01:00
|
|
|
class FredyRuntime {
|
2025-01-07 12:25:19 +01:00
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param providerConfig the config for the specific provider, we're going to query at the moment
|
|
|
|
|
* @param notificationConfig the config for all notifications
|
|
|
|
|
* @param providerId the id of the provider currently in use
|
|
|
|
|
* @param jobKey key of the job that is currently running (from within the config)
|
|
|
|
|
* @param similarityCache cache instance holding values to check for similarity of entries
|
|
|
|
|
*/
|
|
|
|
|
constructor(providerConfig, notificationConfig, providerId, jobKey, similarityCache) {
|
|
|
|
|
this._providerConfig = providerConfig;
|
|
|
|
|
this._notificationConfig = notificationConfig;
|
|
|
|
|
this._providerId = providerId;
|
|
|
|
|
this._jobKey = jobKey;
|
|
|
|
|
this._similarityCache = similarityCache;
|
|
|
|
|
}
|
2024-12-17 12:38:28 +01:00
|
|
|
|
2025-01-07 12:25:19 +01:00
|
|
|
execute() {
|
|
|
|
|
return (
|
|
|
|
|
//modify the url to make sure search order is correctly set
|
|
|
|
|
Promise.resolve(urlModifier(this._providerConfig.url, this._providerConfig.sortByDateParam))
|
|
|
|
|
//scraping the site and try finding new listings
|
|
|
|
|
.then(this._getListings.bind(this))
|
|
|
|
|
//bring them in a proper form (dictated by the provider)
|
|
|
|
|
.then(this._normalize.bind(this))
|
|
|
|
|
//filter listings with stuff tagged by the blacklist of the provider
|
|
|
|
|
.then(this._filter.bind(this))
|
|
|
|
|
//check if new listings available. if so proceed
|
|
|
|
|
.then(this._findNew.bind(this))
|
|
|
|
|
//store everything in db
|
|
|
|
|
.then(this._save.bind(this))
|
|
|
|
|
//check for similar listings. if found, remove them before notifying
|
|
|
|
|
.then(this._filterBySimilarListings.bind(this))
|
|
|
|
|
//notify the user using the configured notification adapter
|
|
|
|
|
.then(this._notify.bind(this))
|
|
|
|
|
//if an error occurred on the way, handle it here.
|
|
|
|
|
.catch(this._handleError.bind(this))
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-12-17 12:38:28 +01:00
|
|
|
|
2025-01-07 12:25:19 +01:00
|
|
|
_getListings(url) {
|
|
|
|
|
const extractor = new Extractor();
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
extractor
|
|
|
|
|
.execute(url, this._providerConfig.waitForSelector)
|
|
|
|
|
.then(() => {
|
|
|
|
|
const listings = extractor.parseResponseText(
|
|
|
|
|
this._providerConfig.crawlContainer,
|
|
|
|
|
this._providerConfig.crawlFields,
|
|
|
|
|
url,
|
|
|
|
|
);
|
|
|
|
|
resolve(listings == null ? [] : listings);
|
|
|
|
|
})
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
reject(err);
|
|
|
|
|
/* eslint-disable no-console */
|
|
|
|
|
console.error(err);
|
|
|
|
|
/* eslint-enable no-console */
|
2024-12-17 12:38:28 +01:00
|
|
|
});
|
2025-01-07 12:25:19 +01:00
|
|
|
});
|
|
|
|
|
}
|
2024-12-17 12:38:28 +01:00
|
|
|
|
2025-01-07 12:25:19 +01:00
|
|
|
_normalize(listings) {
|
|
|
|
|
return listings.map(this._providerConfig.normalize);
|
|
|
|
|
}
|
2024-12-17 12:38:28 +01:00
|
|
|
|
2025-01-07 12:25:19 +01:00
|
|
|
_filter(listings) {
|
|
|
|
|
//only return those where all the fields have been found
|
|
|
|
|
const keys = Object.keys(this._providerConfig.crawlFields);
|
|
|
|
|
const filteredListings = listings.filter((item) => keys.every((key) => key in item));
|
|
|
|
|
return filteredListings.filter(this._providerConfig.filter);
|
|
|
|
|
}
|
2024-12-17 12:38:28 +01:00
|
|
|
|
2025-01-07 12:25:19 +01:00
|
|
|
_findNew(listings) {
|
|
|
|
|
const newListings = listings.filter((o) => getKnownListings(this._jobKey, this._providerId)[o.id] == null);
|
|
|
|
|
if (newListings.length === 0) {
|
|
|
|
|
throw new NoNewListingsWarning();
|
2024-12-17 12:38:28 +01:00
|
|
|
}
|
2025-01-07 12:25:19 +01:00
|
|
|
return newListings;
|
|
|
|
|
}
|
2024-12-17 12:38:28 +01:00
|
|
|
|
2025-01-07 12:25:19 +01:00
|
|
|
_notify(newListings) {
|
|
|
|
|
if (newListings.length === 0) {
|
|
|
|
|
throw new NoNewListingsWarning();
|
2024-12-17 12:38:28 +01:00
|
|
|
}
|
2025-01-07 12:25:19 +01:00
|
|
|
const sendNotifications = notify.send(this._providerId, newListings, this._notificationConfig, this._jobKey);
|
|
|
|
|
return Promise.all(sendNotifications).then(() => newListings);
|
|
|
|
|
}
|
2024-12-17 12:38:28 +01:00
|
|
|
|
2025-01-07 12:25:19 +01:00
|
|
|
_save(newListings) {
|
|
|
|
|
const currentListings = getKnownListings(this._jobKey, this._providerId) || {};
|
|
|
|
|
newListings.forEach((listing) => {
|
|
|
|
|
currentListings[listing.id] = Date.now();
|
|
|
|
|
});
|
|
|
|
|
setKnownListings(this._jobKey, this._providerId, currentListings);
|
|
|
|
|
return newListings;
|
|
|
|
|
}
|
2024-12-17 12:38:28 +01:00
|
|
|
|
2025-01-07 12:25:19 +01:00
|
|
|
_filterBySimilarListings(listings) {
|
|
|
|
|
const filteredList = listings.filter((listing) => {
|
|
|
|
|
const similar = this._similarityCache.hasSimilarEntries(this._jobKey, listing.title);
|
|
|
|
|
if (similar) {
|
|
|
|
|
/* eslint-disable no-console */
|
|
|
|
|
console.debug(`Filtering similar entry for job with id ${this._jobKey} with title: `, listing.title);
|
|
|
|
|
/* eslint-enable no-console */
|
|
|
|
|
}
|
|
|
|
|
return !similar;
|
|
|
|
|
});
|
|
|
|
|
filteredList.forEach((filter) => this._similarityCache.addCacheEntry(this._jobKey, filter.title));
|
|
|
|
|
return filteredList;
|
|
|
|
|
}
|
2024-12-17 12:38:28 +01:00
|
|
|
|
2025-01-07 12:25:19 +01:00
|
|
|
_handleError(err) {
|
|
|
|
|
if (err.name !== 'NoNewListingsWarning') console.error(err);
|
|
|
|
|
}
|
2020-02-26 09:05:20 +01:00
|
|
|
}
|
2024-12-17 12:38:28 +01:00
|
|
|
|
2023-03-13 13:42:43 +01:00
|
|
|
export default FredyRuntime;
|