Change createURL to execute
parent
839016daa3
commit
1d7e06da4c
|
@ -1,10 +1,13 @@
|
|||
"use strict";
|
||||
|
||||
import { AxiosResponse } from 'axios';
|
||||
// Public modules from npm
|
||||
import validator from 'class-validator';
|
||||
|
||||
// Module from files
|
||||
import { IQuery, TCategory, TQueryInterface } from "../../interfaces.js";
|
||||
import { GenericAxiosError, UnexpectedResponseContentType } from '../errors.js';
|
||||
import { Result } from '../result.js';
|
||||
import LatestSearchQuery, { TLatestOrder } from './latest-search-query.js';
|
||||
import ThreadSearchQuery, { TThreadOrder } from './thread-search-query.js';
|
||||
|
||||
|
@ -28,6 +31,8 @@ import ThreadSearchQuery, { TThreadOrder } from './thread-search-query.js';
|
|||
* `views`: Order based on the number of visits. Replacement: `replies`.
|
||||
*/
|
||||
type THandiworkOrder = "date" | "likes" | "relevance" | "replies" | "title" | "views";
|
||||
type TLatestResult = Result<GenericAxiosError | UnexpectedResponseContentType, string>;
|
||||
type TThreadResult = Result<GenericAxiosError, AxiosResponse<any>>;
|
||||
|
||||
export default class HandiworkSearchQuery implements IQuery {
|
||||
|
||||
|
@ -38,6 +43,7 @@ export default class HandiworkSearchQuery implements IQuery {
|
|||
//#endregion Private fields
|
||||
|
||||
//#region Properties
|
||||
|
||||
/**
|
||||
* Keywords to use in the search.
|
||||
*/
|
||||
|
@ -69,9 +75,11 @@ export default class HandiworkSearchQuery implements IQuery {
|
|||
})
|
||||
public page: number = 1;
|
||||
itype: TQueryInterface = "HandiworkSearchQuery";
|
||||
|
||||
//#endregion Properties
|
||||
|
||||
//#region Public methods
|
||||
|
||||
/**
|
||||
* Select what kind of search should be
|
||||
* performed based on the properties of
|
||||
|
@ -90,13 +98,11 @@ export default class HandiworkSearchQuery implements IQuery {
|
|||
return DEFAULT_SEARCH_TYPE;
|
||||
}
|
||||
|
||||
public validate(): boolean {
|
||||
return validator.validateSync(this).length === 0;
|
||||
}
|
||||
public validate(): boolean { return validator.validateSync(this).length === 0; }
|
||||
|
||||
public createURL(): URL {
|
||||
public async execute(): Promise<TLatestResult | TThreadResult> {
|
||||
// Local variables
|
||||
let url = null;
|
||||
let response: TLatestResult | TThreadResult = null;
|
||||
|
||||
// Check if the query is valid
|
||||
if (!this.validate()) {
|
||||
|
@ -104,10 +110,10 @@ export default class HandiworkSearchQuery implements IQuery {
|
|||
}
|
||||
|
||||
// Convert the query
|
||||
if (this.selectSearchType() === "latest") url = this.cast<LatestSearchQuery>("LatestSearchQuery").createURL();
|
||||
else url = this.cast<ThreadSearchQuery>("ThreadSearchQuery").createURL();
|
||||
if (this.selectSearchType() === "latest") response = await this.cast<LatestSearchQuery>("LatestSearchQuery").execute();
|
||||
else response = await this.cast<ThreadSearchQuery>("ThreadSearchQuery").execute();
|
||||
|
||||
return url;
|
||||
return response;
|
||||
}
|
||||
|
||||
public cast<T extends IQuery>(type: TQueryInterface): T {
|
||||
|
@ -122,9 +128,11 @@ export default class HandiworkSearchQuery implements IQuery {
|
|||
// Cast the result to T
|
||||
return returnValue as T;
|
||||
}
|
||||
|
||||
//#endregion Public methods
|
||||
|
||||
//#region Private methods
|
||||
|
||||
private castToLatest(): LatestSearchQuery {
|
||||
// Cast the basic query object and copy common values
|
||||
const query: LatestSearchQuery = new LatestSearchQuery;
|
||||
|
@ -165,5 +173,6 @@ export default class HandiworkSearchQuery implements IQuery {
|
|||
|
||||
return query;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
|
@ -7,6 +7,7 @@ import validator from 'class-validator';
|
|||
import { urls } from "../../constants/url.js";
|
||||
import PrefixParser from '../prefix-parser.js';
|
||||
import { IQuery, TCategory, TQueryInterface } from "../../interfaces.js";
|
||||
import { fetchHTML } from '../../network-helper.js';
|
||||
|
||||
// Type definitions
|
||||
export type TLatestOrder = "date" | "likes" | "views" | "title" | "rating";
|
||||
|
@ -18,11 +19,14 @@ type TDate = 365 | 180 | 90 | 30 | 14 | 7 | 3 | 1;
|
|||
export default class LatestSearchQuery implements IQuery {
|
||||
|
||||
//#region Private fields
|
||||
|
||||
private static MAX_TAGS = 5;
|
||||
private static MIN_PAGE = 1;
|
||||
|
||||
//#endregion Private fields
|
||||
|
||||
//#region Properties
|
||||
|
||||
public category: TCategory = 'games';
|
||||
/**
|
||||
* Ordering type.
|
||||
|
@ -52,20 +56,47 @@ export default class LatestSearchQuery implements IQuery {
|
|||
})
|
||||
public page = LatestSearchQuery.MIN_PAGE;
|
||||
itype: TQueryInterface = "LatestSearchQuery";
|
||||
|
||||
//#endregion Properties
|
||||
|
||||
//#region Public methods
|
||||
|
||||
public validate(): boolean {
|
||||
return validator.validateSync(this).length === 0;
|
||||
}
|
||||
public validate(): boolean { return validator.validateSync(this).length === 0; }
|
||||
|
||||
public createURL(): URL {
|
||||
public async execute() {
|
||||
// Check if the query is valid
|
||||
if (!this.validate()) {
|
||||
throw new Error(`Invalid query: ${validator.validateSync(this).join("\n")}`);
|
||||
}
|
||||
|
||||
// Prepare the URL
|
||||
const url = this.prepareGETurl();
|
||||
const decoded = decodeURIComponent(url.toString());
|
||||
|
||||
// Fetch the result
|
||||
return await fetchHTML(decoded);
|
||||
}
|
||||
|
||||
public findNearestDate(d: Date): TDate {
|
||||
// Find the difference between today and the passed date
|
||||
const diff = this.dateDiffInDays(new Date(), d);
|
||||
|
||||
// Find the closest valid value in the array
|
||||
const closest = [365, 180, 90, 30, 14, 7, 3, 1].reduce(function (prev, curr) {
|
||||
return (Math.abs(curr - diff) < Math.abs(prev - diff) ? curr : prev);
|
||||
});
|
||||
|
||||
return closest as TDate;
|
||||
}
|
||||
|
||||
//#endregion Public methods
|
||||
|
||||
//#region Private methods
|
||||
|
||||
/**
|
||||
* Prepare the URL by filling out the GET parameters with the data in the query.
|
||||
*/
|
||||
private prepareGETurl(): URL {
|
||||
// Create the URL
|
||||
const url = new URL(urls.F95_LATEST_PHP);
|
||||
url.searchParams.set("cmd", "list");
|
||||
|
@ -92,20 +123,6 @@ export default class LatestSearchQuery implements IQuery {
|
|||
return url;
|
||||
}
|
||||
|
||||
public findNearestDate(d: Date): TDate {
|
||||
// Find the difference between today and the passed date
|
||||
const diff = this.dateDiffInDays(new Date(), d);
|
||||
|
||||
// Find the closest valid value in the array
|
||||
const closest = [365, 180, 90, 30, 14, 7, 3, 1].reduce(function (prev, curr) {
|
||||
return (Math.abs(curr - diff) < Math.abs(prev - diff) ? curr : prev);
|
||||
});
|
||||
|
||||
return closest as TDate;
|
||||
}
|
||||
//#endregion Public methods
|
||||
|
||||
//#region Private methods
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
@ -118,5 +135,7 @@ export default class LatestSearchQuery implements IQuery {
|
|||
|
||||
return Math.floor((utc2 - utc1) / MS_PER_DAY);
|
||||
}
|
||||
|
||||
//#endregion Private methodss
|
||||
|
||||
}
|
|
@ -7,6 +7,11 @@ import validator from 'class-validator';
|
|||
import { IQuery, TCategory, TQueryInterface } from "../../interfaces.js";
|
||||
import { urls } from "../../constants/url.js";
|
||||
import PrefixParser from "./../prefix-parser.js";
|
||||
import { fetchPOSTResponse } from '../../network-helper.js';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { GenericAxiosError } from '../errors.js';
|
||||
import { Result } from '../result.js';
|
||||
import Shared from '../../shared.js';
|
||||
|
||||
// Type definitions
|
||||
export type TThreadOrder = "relevance" | "date" | "last_update" | "replies";
|
||||
|
@ -14,10 +19,13 @@ export type TThreadOrder = "relevance" | "date" | "last_update" | "replies";
|
|||
export default class ThreadSearchQuery implements IQuery {
|
||||
|
||||
//#region Private fields
|
||||
|
||||
static MIN_PAGE = 1;
|
||||
|
||||
//#endregion Private fields
|
||||
|
||||
//#region Properties
|
||||
|
||||
/**
|
||||
* Keywords to use in the search.
|
||||
*/
|
||||
|
@ -57,78 +65,89 @@ export default class ThreadSearchQuery implements IQuery {
|
|||
})
|
||||
public page: number = 1;
|
||||
itype: TQueryInterface = "ThreadSearchQuery";
|
||||
|
||||
//#endregion Properties
|
||||
|
||||
//#region Public methods
|
||||
|
||||
public validate(): boolean {
|
||||
return validator.validateSync(this).length === 0;
|
||||
}
|
||||
public validate(): boolean { return validator.validateSync(this).length === 0; }
|
||||
|
||||
public createURL(): URL {
|
||||
public async execute(): Promise<Result<GenericAxiosError, AxiosResponse<any>>> {
|
||||
// Check if the query is valid
|
||||
if (!this.validate()) {
|
||||
throw new Error(`Invalid query: ${validator.validateSync(this).join("\n")}`);
|
||||
}
|
||||
|
||||
// Create the URL
|
||||
const url = new URL(urls.F95_SEARCH_URL);
|
||||
// Define the POST parameters
|
||||
const params = this.preparePOSTParameters();
|
||||
|
||||
// Specifiy if only the title should be searched
|
||||
if (this.onlyTitles) url.searchParams.set("c[title_only]", "1");
|
||||
// Return the POST response
|
||||
return await fetchPOSTResponse(urls.F95_SEARCH_URL, params);
|
||||
}
|
||||
|
||||
//#endregion Public methods
|
||||
|
||||
//#region Private methods
|
||||
|
||||
/**
|
||||
* Prepare the parameters for post request with the data in the query.
|
||||
*/
|
||||
private preparePOSTParameters(): { [s: string]: string } {
|
||||
// Local variables
|
||||
const params = {};
|
||||
|
||||
// Ad the session token
|
||||
params["_xfToken"] = Shared.session.token;
|
||||
|
||||
// Specify if only the title should be searched
|
||||
if (this.onlyTitles) params["c[title_only]"] = "1";
|
||||
|
||||
// Add keywords
|
||||
const encodedKeywords = this.keywords ?? "*";
|
||||
url.searchParams.set("q", encodedKeywords);
|
||||
params["keywords"] = this.keywords ?? "*";
|
||||
|
||||
// Specify the scope of the search (only "threads/post")
|
||||
url.searchParams.set("t", "post");
|
||||
params["search_type"] = "post";
|
||||
|
||||
// Set the dates
|
||||
if (this.newerThan) {
|
||||
const date = this.convertShortDate(this.newerThan);
|
||||
url.searchParams.set("c[newer_than]", date);
|
||||
params["c[newer_than]"] = date;
|
||||
}
|
||||
|
||||
if (this.olderThan) {
|
||||
const date = this.convertShortDate(this.olderThan);
|
||||
url.searchParams.set("c[older_than]", date);
|
||||
params["c[older_than]"] = date;
|
||||
}
|
||||
|
||||
// Set included and excluded tags
|
||||
// The tags are first joined with a comma, then encoded to URI
|
||||
const includedTags = encodeURIComponent(this.includedTags.join(","));
|
||||
const excludedTags = encodeURIComponent(this.excludedTags.join(","));
|
||||
if (includedTags) url.searchParams.set("c[tags]", includedTags);
|
||||
if (excludedTags) url.searchParams.set("c[excludeTags]", excludedTags);
|
||||
// Set included and excluded tags (joined with a comma)
|
||||
if (this.includedTags) params["c[tags]"] = this.includedTags.join(",");
|
||||
if (this.excludedTags) params["c[excludeTags]"] = this.excludedTags.join(",");
|
||||
|
||||
// Set minimum reply number
|
||||
if (this.minimumReplies > 0) url.searchParams.set("c[min_reply_count]", this.minimumReplies.toString());
|
||||
if (this.minimumReplies > 0) params["c[min_reply_count]"] = this.minimumReplies.toString();
|
||||
|
||||
// Add prefixes
|
||||
const parser = new PrefixParser();
|
||||
const ids = parser.prefixesToIDs(this.includedPrefixes);
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const name = `c[prefixes][${i}]`;
|
||||
url.searchParams.set(name, ids[i].toString());
|
||||
params[name] = ids[i].toString();
|
||||
}
|
||||
|
||||
// Set the category
|
||||
url.searchParams.set("c[child_nodes]", "1"); // Always set
|
||||
params["c[child_nodes]"] = "1"; // Always set
|
||||
if (this.category) {
|
||||
const catID = this.categoryToID(this.category).toString();
|
||||
url.searchParams.set("c[nodes][0]", catID);
|
||||
params["c[nodes][0]"] = catID;
|
||||
}
|
||||
|
||||
// Set the other values
|
||||
url.searchParams.set("o", this.order.toString());
|
||||
url.searchParams.set("page", this.page.toString());
|
||||
params["o"] = this.order.toString();
|
||||
params["page"] = this.page.toString();
|
||||
|
||||
return url;
|
||||
return params;
|
||||
}
|
||||
//#endregion Public methods
|
||||
|
||||
//#region Private methods
|
||||
/**
|
||||
* Convert a date in the YYYY-MM-DD format taking into account the time zone.
|
||||
*/
|
||||
|
@ -152,6 +171,7 @@ export default class ThreadSearchQuery implements IQuery {
|
|||
|
||||
return catMap[category as string];
|
||||
}
|
||||
|
||||
//#endregion Private methods
|
||||
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
export const urls = {
|
||||
F95_BASE_URL: "https://f95zone.to",
|
||||
F95_SEARCH_URL: "https://f95zone.to/search/105286576/",
|
||||
F95_SEARCH_URL: "https://f95zone.to/search/search/",
|
||||
F95_LATEST_UPDATES: "https://f95zone.to/latest",
|
||||
F95_THREADS: "https://f95zone.to/threads/",
|
||||
F95_LOGIN_URL: "https://f95zone.to/login/login",
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
// Modules from file
|
||||
import { fetchGETResponse } from "../network-helper.js";
|
||||
import LatestSearchQuery from "../classes/query/latest-search-query.js";
|
||||
import { urls as f95url } from "../constants/url.js";
|
||||
|
||||
|
@ -23,11 +22,8 @@ export default async function fetchLatestHandiworkURLs(query: LatestSearchQuery,
|
|||
let noMorePages = false;
|
||||
|
||||
do {
|
||||
// Prepare the URL
|
||||
const url = query.createURL().toString();
|
||||
|
||||
// Fetch the response (application/json)
|
||||
const response = await fetchGETResponse(url);
|
||||
const response = await query.execute();
|
||||
|
||||
// Save the URLs
|
||||
if (response.isSuccess()) {
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
import cheerio from "cheerio";
|
||||
|
||||
// Modules from file
|
||||
import { fetchHTML } from "../network-helper.js";
|
||||
import shared from "../shared.js";
|
||||
import { selectors as f95Selector } from "../constants/css-selector.js";
|
||||
import { urls as f95urls } from "../constants/url.js";
|
||||
import ThreadSearchQuery from "../classes/query/thread-search-query.js";
|
||||
|
||||
//#region Public methods
|
||||
|
||||
/**
|
||||
* Gets the URLs of the handiwork' threads that match the passed parameters.
|
||||
* You *must* be logged.
|
||||
|
@ -21,42 +21,37 @@ import ThreadSearchQuery from "../classes/query/thread-search-query.js";
|
|||
* @returns {Promise<String[]>} URLs of the handiworks
|
||||
*/
|
||||
export default async function fetchThreadHandiworkURLs(query: ThreadSearchQuery, limit: number = 30): Promise<string[]> {
|
||||
// Get the query
|
||||
const url = decodeURI(query.createURL().href);
|
||||
// Execute the query
|
||||
const response = await query.execute();
|
||||
|
||||
// Fetch the results from F95 and return the handiwork urls
|
||||
return await fetchResultURLs(url, limit);
|
||||
if (response.isSuccess()) return await fetchResultURLs(response.value.data as string, limit);
|
||||
else throw response.value
|
||||
}
|
||||
|
||||
//#endregion Public methods
|
||||
|
||||
//#region Private methods
|
||||
|
||||
/**
|
||||
* Gets the URLs of the threads resulting from the F95Zone search.
|
||||
* @param {number} limit
|
||||
* Maximum number of items to get. Default: 30
|
||||
* @return {Promise<String[]>} List of URLs
|
||||
*/
|
||||
async function fetchResultURLs(url: string, limit: number = 30): Promise<string[]> {
|
||||
shared.logger.trace(`Fetching ${url}...`);
|
||||
async function fetchResultURLs(html: string, limit: number = 30): Promise<string[]> {
|
||||
// Prepare cheerio
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// Fetch HTML and prepare Cheerio
|
||||
const html = await fetchHTML(url);
|
||||
// Here we get all the DIV that are the body of the various query results
|
||||
const results = $("body").find(f95Selector.GS_RESULT_BODY);
|
||||
|
||||
if (html.isSuccess()) {
|
||||
const $ = cheerio.load(html.value);
|
||||
// Than we extract the URLs
|
||||
const urls = results.slice(0, limit).map((idx, el) => {
|
||||
const elementSelector = $(el);
|
||||
return extractLinkFromResult(elementSelector);
|
||||
}).get();
|
||||
|
||||
// Here we get all the DIV that are the body of the various query results
|
||||
const results = $("body").find(f95Selector.GS_RESULT_BODY);
|
||||
|
||||
// Than we extract the URLs
|
||||
const urls = results.slice(0, limit).map((idx, el) => {
|
||||
const elementSelector = $(el);
|
||||
return extractLinkFromResult(elementSelector);
|
||||
}).get();
|
||||
|
||||
return urls;
|
||||
} else throw html.value;
|
||||
return urls;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -75,4 +70,5 @@ function extractLinkFromResult(selector: cheerio.Cheerio): string {
|
|||
// Compose and return the URL
|
||||
return new URL(partialLink, f95urls.F95_BASE_URL).toString();
|
||||
}
|
||||
|
||||
//#endregion Private methods
|
|
@ -288,8 +288,9 @@ export interface IQuery {
|
|||
*/
|
||||
validate(): boolean,
|
||||
/**
|
||||
* From the query values it generates the corresponding URL for the platform.
|
||||
* Search with the data in the query and returns the result.
|
||||
*
|
||||
* If the query is invalid it throws an exception.
|
||||
*/
|
||||
createURL(): URL,
|
||||
execute(): any,
|
||||
}
|
Loading…
Reference in New Issue