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';
 | 
					import validator from 'class-validator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Modules from file
 | 
					// Modules from file
 | 
				
			||||||
import { urls } from "../constants/url.js";
 | 
					import { urls } from "../../constants/url.js";
 | 
				
			||||||
import PrefixParser from './prefix-parser.js';
 | 
					import PrefixParser from '../prefix-parser.js';
 | 
				
			||||||
import { IQuery, TCategory } from "../interfaces";
 | 
					import { IQuery, TCategory, TQueryInterface } from "../../interfaces";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Type definitions
 | 
					// 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;
 | 
					type TDate = 365 | 180 | 90 | 30 | 14 | 7 | 3 | 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
| 
						 | 
					@ -27,7 +27,7 @@ export default class LatestSearchQuery implements IQuery {
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * Default: `date`.
 | 
					     * Default: `date`.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public order: TOrder = 'date';
 | 
					    public order: TLatestOrder = 'date';
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Date limit in days, to be understood as "less than".
 | 
					     * Date limit in days, to be understood as "less than".
 | 
				
			||||||
     * Use `1` to indicate "today" or `null` to indicate "anytime".
 | 
					     * 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"
 | 
					        message: "Too many tags: $value instead of $constraint1"
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    public includedTags: string[] = [];
 | 
					    public includedTags: string[] = [];
 | 
				
			||||||
 | 
					 | 
				
			||||||
    public includedPrefixes: string[] = [];
 | 
					    public includedPrefixes: string[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @validator.IsInt({
 | 
					    @validator.IsInt({
 | 
				
			||||||
| 
						 | 
					@ -50,24 +49,19 @@ export default class LatestSearchQuery implements IQuery {
 | 
				
			||||||
        message: "The minimum $property value must be $constraint1, received $value"
 | 
					        message: "The minimum $property value must be $constraint1, received $value"
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    public page = LatestSearchQuery.MIN_PAGE;
 | 
					    public page = LatestSearchQuery.MIN_PAGE;
 | 
				
			||||||
 | 
					    itype: TQueryInterface = "LatestSearchQuery";
 | 
				
			||||||
    //#endregion Properties
 | 
					    //#endregion Properties
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    //#region Public methods
 | 
					    //#region Public methods
 | 
				
			||||||
    /**
 | 
					
 | 
				
			||||||
     * Verify that the query values are valid.
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public validate(): boolean {
 | 
					    public validate(): boolean {
 | 
				
			||||||
        return validator.validateSync(this).length === 0;
 | 
					        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 {
 | 
					    public createURL(): URL {
 | 
				
			||||||
        // Check if the query is valid
 | 
					        // Check if the query is valid
 | 
				
			||||||
        if (!this.validate()) {
 | 
					        if (!this.validate()) {
 | 
				
			||||||
            throw new Error("Invalid query")
 | 
					            throw new Error(`Invalid query: ${validator.validateSync(this).join("\n")}`);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Create the URL
 | 
					        // Create the URL
 | 
				
			||||||
| 
						 | 
					@ -94,5 +88,32 @@ export default class LatestSearchQuery implements IQuery {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return url;
 | 
					        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
 | 
					    //#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