Moved to subfolder "query"
parent
2c9f8e2d0b
commit
3c34b63cb6
|
@ -1,114 +0,0 @@
|
|||
// Public modules from npm
|
||||
import validator from 'class-validator';
|
||||
|
||||
// Modules from file
|
||||
import { urls } from "../constants/url.js";
|
||||
import PrefixParser from './prefix-parser.js';
|
||||
import { TCategory } from "../interfaces";
|
||||
|
||||
// Type definitions
|
||||
type TSort = "date" | "likes" | "views" | "title" | "rating";
|
||||
type TDate = 365 | 180 | 90 | 30 | 14 | 7 | 3 | 1;
|
||||
|
||||
/**
|
||||
* Query used to search for specific threads on the platform.
|
||||
*/
|
||||
export default class HandiworkSearchQuery {
|
||||
|
||||
//#region Private fields
|
||||
private static MAX_TAGS = 5;
|
||||
private static MIN_PAGE = 1;
|
||||
//#endregion Private fields
|
||||
|
||||
//#region Properties
|
||||
/**
|
||||
* Category of items to search among.
|
||||
* Default: `games`
|
||||
*/
|
||||
public category: TCategory = 'games';
|
||||
/**
|
||||
* List of IDs of tags to be included in the search.
|
||||
* Max. 5 tags
|
||||
*/
|
||||
@validator.IsArray({
|
||||
message: "Expected an array, received $value"
|
||||
})
|
||||
@validator.ArrayMaxSize(HandiworkSearchQuery.MAX_TAGS, {
|
||||
message: "Too many tags: $value instead of $constraint1"
|
||||
})
|
||||
public tags: string[] = [];
|
||||
/**
|
||||
* List of IDs of prefixes to be included in the search.
|
||||
*/
|
||||
@validator.IsArray({
|
||||
message: "Expected an array, received $value"
|
||||
})
|
||||
public prefixes: string[] = [];
|
||||
/**
|
||||
* Sorting type. Default: `date`.
|
||||
*/
|
||||
public sort: TSort = 'date';
|
||||
/**
|
||||
* Date limit in days, to be understood as "less than".
|
||||
* Use `1` to indicate "today" or `null` to indicate "anytime".
|
||||
* Default: `null`
|
||||
*/
|
||||
public date: TDate = null;
|
||||
/**
|
||||
* Index of the page to be obtained.
|
||||
* Between 1 and infinity.
|
||||
* Default: 1.
|
||||
*/
|
||||
@validator.IsInt({
|
||||
message: "$property expect an integer, received $value"
|
||||
})
|
||||
@validator.Min(HandiworkSearchQuery.MIN_PAGE, {
|
||||
message: "The minimum $property value must be $constraint1, received $value"
|
||||
})
|
||||
public page = HandiworkSearchQuery.MIN_PAGE;
|
||||
//#endregion Properties
|
||||
|
||||
//#region Public methods
|
||||
/**
|
||||
* Verify that the query values are valid.
|
||||
*/
|
||||
public validate(): boolean {
|
||||
return validator.validateSync(this).length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* From the query values it generates the corresponding URL for the platform.
|
||||
* If the query is invalid it throws an exception.
|
||||
*/
|
||||
public createUrl(): URL {
|
||||
// Check if the query is valid
|
||||
if (!this.validate()) {
|
||||
throw new Error("Invalid query")
|
||||
}
|
||||
|
||||
// Create the URL
|
||||
const url = new URL(urls.F95_LATEST_PHP);
|
||||
url.searchParams.set("cmd", "list");
|
||||
|
||||
// Set the category
|
||||
url.searchParams.set("cat", this.category);
|
||||
|
||||
// Add tags and prefixes
|
||||
const parser = new PrefixParser();
|
||||
for (const tag of parser.prefixesToIDs(this.tags)) {
|
||||
url.searchParams.append("tags[]", tag.toString());
|
||||
}
|
||||
|
||||
for (const p of parser.prefixesToIDs(this.prefixes)) {
|
||||
url.searchParams.append("prefixes[]", p.toString());
|
||||
}
|
||||
|
||||
// Set the other values
|
||||
url.searchParams.set("sort", this.sort.toString());
|
||||
url.searchParams.set("page", this.page.toString());
|
||||
if(this.date) url.searchParams.set("date", this.date.toString());
|
||||
|
||||
return url;
|
||||
}
|
||||
//#endregion Public methods
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
"use strict";
|
||||
|
||||
// Public modules from npm
|
||||
import validator from 'class-validator';
|
||||
|
||||
// Module from files
|
||||
import { IQuery, TCategory, TQueryInterface } from "../../interfaces";
|
||||
import LatestSearchQuery, { TLatestOrder } from './latest-search-query';
|
||||
import ThreadSearchQuery, { TThreadOrder } from './thread-search-query';
|
||||
|
||||
// Type definitions
|
||||
/**
|
||||
* Method of sorting results. Try to unify the two types of
|
||||
* sorts in the "Latest" section and in the "Thread search"
|
||||
* section. Being dynamic research, if a sorting type is not
|
||||
* available, the replacement sort is chosen.
|
||||
*
|
||||
* `date`: Order based on the latest update
|
||||
*
|
||||
* `likes`: Order based on the number of likes received. Replacement: `replies`.
|
||||
*
|
||||
* `relevance`: Order based on the relevance of the result (or rating).
|
||||
*
|
||||
* `replies`: Order based on the number of answers to the thread. Replacement: `views`.
|
||||
*
|
||||
* `title`: Order based on the growing alphabetical order of the titles.
|
||||
*
|
||||
* `views`: Order based on the number of visits. Replacement: `replies`.
|
||||
*/
|
||||
type THandiworkOrder = "date" | "likes" | "relevance" | "replies" | "title" | "views";
|
||||
|
||||
export default class HandiworkSearchQuery implements IQuery {
|
||||
|
||||
//#region Private fields
|
||||
static MIN_PAGE = 1;
|
||||
//#endregion Private fields
|
||||
|
||||
//#region Properties
|
||||
/**
|
||||
* Keywords to use in the search.
|
||||
*/
|
||||
public keywords: string = "";
|
||||
/**
|
||||
* The results must be more recent than the date indicated.
|
||||
*/
|
||||
public newerThan: Date = null;
|
||||
/**
|
||||
* The results must be older than the date indicated.
|
||||
*/
|
||||
public olderThan: Date = null;
|
||||
public includedTags: string[] = [];
|
||||
/**
|
||||
* Tags to exclude from the search.
|
||||
*/
|
||||
public excludedTags: string[] = [];
|
||||
public includedPrefixes: string[];
|
||||
public category: TCategory;
|
||||
/**
|
||||
* Results presentation order.
|
||||
*/
|
||||
public order: THandiworkOrder = "relevance";
|
||||
@validator.IsInt({
|
||||
message: "$property expect an integer, received $value"
|
||||
})
|
||||
@validator.Min(HandiworkSearchQuery.MIN_PAGE, {
|
||||
message: "The minimum $property value must be $constraint1, received $value"
|
||||
})
|
||||
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
|
||||
* the query.
|
||||
*/
|
||||
public selectSearchType(): "latest" | "thread" {
|
||||
// Local variables
|
||||
const MAX_TAGS_LATEST_SEARCH = 5;
|
||||
const DEFAULT_SEARCH_TYPE = "latest";
|
||||
|
||||
// If the keywords are set or the number
|
||||
// of included tags is greather than 5,
|
||||
// we must perform a thread search
|
||||
if (this.keywords || this.includedTags.length > MAX_TAGS_LATEST_SEARCH) return "thread";
|
||||
|
||||
return DEFAULT_SEARCH_TYPE;
|
||||
}
|
||||
|
||||
public validate(): boolean {
|
||||
return validator.validateSync(this).length === 0;
|
||||
}
|
||||
|
||||
public createURL(): URL {
|
||||
// Local variables
|
||||
let query: LatestSearchQuery | ThreadSearchQuery = null;
|
||||
|
||||
// Check if the query is valid
|
||||
if (!this.validate()) {
|
||||
throw new Error(`Invalid query: ${validator.validateSync(this).join("\n")}`);
|
||||
}
|
||||
|
||||
// Convert the query
|
||||
if (this.selectSearchType() === "latest") query = this.cast<LatestSearchQuery>();
|
||||
else query = this.cast<ThreadSearchQuery>();
|
||||
|
||||
return query.createURL();
|
||||
}
|
||||
|
||||
public cast<T extends IQuery>(): T {
|
||||
// Local variables
|
||||
let returnValue = null;
|
||||
|
||||
// Cast the query
|
||||
const query:T = {} as IQuery as T;
|
||||
|
||||
// Convert the query
|
||||
if (query.itype === "LatestSearchQuery") returnValue = this.castToLatest();
|
||||
else if (query.itype === "ThreadSearchQuery") returnValue = this.castToThread();
|
||||
else returnValue = this as HandiworkSearchQuery;
|
||||
|
||||
// Cast the result to T
|
||||
return returnValue as T;
|
||||
}
|
||||
//#endregion Public methods
|
||||
|
||||
//#region Private methods
|
||||
private castToLatest(): LatestSearchQuery {
|
||||
// Cast the basic query object
|
||||
const query: LatestSearchQuery = this as IQuery as LatestSearchQuery;
|
||||
let orderFilter = this.order as string;
|
||||
query.itype = "LatestSearchQuery";
|
||||
|
||||
// Adapt order filter
|
||||
if (orderFilter === "relevance") orderFilter = "rating";
|
||||
else if (orderFilter === "replies") orderFilter = "views";
|
||||
query.order = orderFilter as TLatestOrder;
|
||||
|
||||
// Adapt date
|
||||
if (this.newerThan) query.date = query.findNearestDate(this.newerThan);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
private castToThread(): ThreadSearchQuery {
|
||||
// Cast the basic query object
|
||||
const query: ThreadSearchQuery = this as IQuery as ThreadSearchQuery;
|
||||
let orderFilter = this.order as string;
|
||||
|
||||
// Set common values
|
||||
query.excludedTags = this.excludedTags;
|
||||
query.newerThan = this.newerThan;
|
||||
query.olderThan = this.olderThan;
|
||||
query.onlyTitles = true;
|
||||
query.keywords = this.keywords;
|
||||
|
||||
// Adapt order filter
|
||||
if (orderFilter === "likes" || orderFilter === "title") orderFilter = "relevance";
|
||||
query.order = orderFilter as TThreadOrder;
|
||||
|
||||
return query;
|
||||
}
|
||||
//#endregion
|
||||
}
|
|
@ -2,12 +2,12 @@
|
|||
import validator from 'class-validator';
|
||||
|
||||
// Modules from file
|
||||
import { urls } from "../constants/url.js";
|
||||
import PrefixParser from './prefix-parser.js';
|
||||
import { IQuery, TCategory } from "../interfaces";
|
||||
import { urls } from "../../constants/url.js";
|
||||
import PrefixParser from '../prefix-parser.js';
|
||||
import { IQuery, TCategory, TQueryInterface } from "../../interfaces";
|
||||
|
||||
// Type definitions
|
||||
type TOrder = "date" | "likes" | "views" | "title" | "rating";
|
||||
export type TLatestOrder = "date" | "likes" | "views" | "title" | "rating";
|
||||
type TDate = 365 | 180 | 90 | 30 | 14 | 7 | 3 | 1;
|
||||
|
||||
/**
|
||||
|
@ -27,7 +27,7 @@ export default class LatestSearchQuery implements IQuery {
|
|||
*
|
||||
* Default: `date`.
|
||||
*/
|
||||
public order: TOrder = 'date';
|
||||
public order: TLatestOrder = 'date';
|
||||
/**
|
||||
* Date limit in days, to be understood as "less than".
|
||||
* Use `1` to indicate "today" or `null` to indicate "anytime".
|
||||
|
@ -40,7 +40,6 @@ export default class LatestSearchQuery implements IQuery {
|
|||
message: "Too many tags: $value instead of $constraint1"
|
||||
})
|
||||
public includedTags: string[] = [];
|
||||
|
||||
public includedPrefixes: string[] = [];
|
||||
|
||||
@validator.IsInt({
|
||||
|
@ -50,24 +49,19 @@ export default class LatestSearchQuery implements IQuery {
|
|||
message: "The minimum $property value must be $constraint1, received $value"
|
||||
})
|
||||
public page = LatestSearchQuery.MIN_PAGE;
|
||||
itype: TQueryInterface = "LatestSearchQuery";
|
||||
//#endregion Properties
|
||||
|
||||
//#region Public methods
|
||||
/**
|
||||
* Verify that the query values are valid.
|
||||
*/
|
||||
|
||||
public validate(): boolean {
|
||||
return validator.validateSync(this).length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* From the query values it generates the corresponding URL for the platform.
|
||||
* If the query is invalid it throws an exception.
|
||||
*/
|
||||
public createURL(): URL {
|
||||
// Check if the query is valid
|
||||
if (!this.validate()) {
|
||||
throw new Error("Invalid query")
|
||||
throw new Error(`Invalid query: ${validator.validateSync(this).join("\n")}`);
|
||||
}
|
||||
|
||||
// Create the URL
|
||||
|
@ -94,5 +88,32 @@ 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
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private dateDiffInDays(a: Date, b: Date) {
|
||||
const MS_PER_DAY = 1000 * 60 * 60 * 24;
|
||||
|
||||
// Discard the time and time-zone information.
|
||||
const utc1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
|
||||
const utc2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
|
||||
|
||||
return Math.floor((utc2 - utc1) / MS_PER_DAY);
|
||||
}
|
||||
//#endregion Private methodss
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
"use strict";
|
||||
|
||||
// Public modules from npm
|
||||
import validator from 'class-validator';
|
||||
|
||||
// Module from files
|
||||
import { IQuery, TCategory, TQueryInterface } from "../interfaces";
|
||||
import { urls } from "../constants/url";
|
||||
import PrefixParser from "./prefix-parser";
|
||||
|
||||
// Type definitions
|
||||
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.
|
||||
*/
|
||||
public keywords: string = "";
|
||||
/**
|
||||
* Indicates to search by checking only the thread titles and not the content.
|
||||
*/
|
||||
public onlyTitles: boolean = false;
|
||||
/**
|
||||
* The results must be more recent than the date indicated.
|
||||
*/
|
||||
public newerThan: Date = null;
|
||||
/**
|
||||
* The results must be older than the date indicated.
|
||||
*/
|
||||
public olderThan: Date = null;
|
||||
public includedTags: string[] = [];
|
||||
/**
|
||||
* Tags to exclude from the search.
|
||||
*/
|
||||
public excludedTags: string[] = [];
|
||||
/**
|
||||
* Minimum number of answers that the thread must possess.
|
||||
*/
|
||||
public minimumReplies: number = 0;
|
||||
public includedPrefixes: string[];
|
||||
public category: TCategory;
|
||||
/**
|
||||
* Results presentation order.
|
||||
*/
|
||||
public order: TThreadOrder = "relevance";
|
||||
@validator.IsInt({
|
||||
message: "$property expect an integer, received $value"
|
||||
})
|
||||
@validator.Min(ThreadSearchQuery.MIN_PAGE, {
|
||||
message: "The minimum $property value must be $constraint1, received $value"
|
||||
})
|
||||
public page: number = 1;
|
||||
itype: TQueryInterface = "ThreadSearchQuery";
|
||||
//#endregion Properties
|
||||
|
||||
//#region Public methods
|
||||
|
||||
public validate(): boolean {
|
||||
return validator.validateSync(this).length === 0;
|
||||
}
|
||||
|
||||
public createURL(): URL {
|
||||
// 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);
|
||||
|
||||
// Specifiy if only the title should be searched
|
||||
if (this.onlyTitles) url.searchParams.set("c[title_only]", "1");
|
||||
|
||||
// Add keywords
|
||||
const encodedKeywords = this.keywords ? encodeURIComponent(this.keywords) : "*";
|
||||
url.searchParams.set("q", encodedKeywords);
|
||||
|
||||
// Specify the scope of the search (only "threads/post")
|
||||
url.searchParams.set("t", "post");
|
||||
|
||||
// Set the dates
|
||||
if (this.newerThan) {
|
||||
const date = this.convertShortDate(this.newerThan);
|
||||
url.searchParams.set("c[newer_than]", date);
|
||||
}
|
||||
|
||||
if (this.olderThan) {
|
||||
const date = this.convertShortDate(this.olderThan);
|
||||
url.searchParams.set("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(","));
|
||||
url.searchParams.set("c[tags]", includedTags);
|
||||
url.searchParams.set("c[excludeTags]", excludedTags);
|
||||
|
||||
// Set minimum reply number
|
||||
url.searchParams.set("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());
|
||||
}
|
||||
|
||||
// Set the category
|
||||
const catID = this.categoryToID(this.category).toString();
|
||||
url.searchParams.set("c[child_nodes]", "1"); // Always set
|
||||
url.searchParams.set("c[nodes][0]", catID);
|
||||
|
||||
// Set the other values
|
||||
url.searchParams.set("o", this.order.toString());
|
||||
url.searchParams.set("page", this.page.toString());
|
||||
|
||||
return url;
|
||||
}
|
||||
//#endregion Public methods
|
||||
|
||||
//#region Private methods
|
||||
/**
|
||||
* Convert a date in the YYYY-MM-DD format taking into account the time zone.
|
||||
*/
|
||||
private convertShortDate(d: Date): string {
|
||||
const offset = d.getTimezoneOffset()
|
||||
d = new Date(d.getTime() - (offset * 60 * 1000))
|
||||
return d.toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the unique ID of the selected category.
|
||||
*/
|
||||
private categoryToID(category: TCategory): number {
|
||||
const catMap = {
|
||||
"games": 1,
|
||||
"mods": 41,
|
||||
"comics": 40,
|
||||
"animations": 94,
|
||||
"assets": 95,
|
||||
}
|
||||
|
||||
return catMap[category as string];
|
||||
}
|
||||
//#endregion Private methods
|
||||
|
||||
}
|
Loading…
Reference in New Issue