Add thread class mapping
							parent
							
								
									33cfc3a0a6
								
							
						
					
					
						commit
						36f6075e32
					
				| 
						 | 
					@ -0,0 +1,227 @@
 | 
				
			||||||
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Public modules from npm
 | 
				
			||||||
 | 
					import cheerio from "cheerio";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Modules from files
 | 
				
			||||||
 | 
					import Post from "./post";
 | 
				
			||||||
 | 
					import PlatformUser from "./platform-user";
 | 
				
			||||||
 | 
					import { TRating } from "../interfaces";
 | 
				
			||||||
 | 
					import { urls } from "../constants/url";
 | 
				
			||||||
 | 
					import { THREAD } from "../constants/css-selector";
 | 
				
			||||||
 | 
					import { fetchHTML, fetchPOSTResponse } from "../network-helper";
 | 
				
			||||||
 | 
					import Shared from "../shared";
 | 
				
			||||||
 | 
					import { GenericAxiosError, UnexpectedResponseContentType } from "./errors";
 | 
				
			||||||
 | 
					import { Result } from "./result";
 | 
				
			||||||
 | 
					import { getJSONLD, TJsonLD } from "../scrape-data/json-ld";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Represents a generic F95Zone platform thread.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export default class Thread {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //#region Fields
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private _id: number;
 | 
				
			||||||
 | 
					    private _url: string;
 | 
				
			||||||
 | 
					    private _title: string;
 | 
				
			||||||
 | 
					    private _tags: string[];
 | 
				
			||||||
 | 
					    private _prefixes: string[];
 | 
				
			||||||
 | 
					    private _posts: Post[];
 | 
				
			||||||
 | 
					    private _rating: TRating;
 | 
				
			||||||
 | 
					    private _owner: PlatformUser;
 | 
				
			||||||
 | 
					    private _creation: Date;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //#endregion Fields
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //#region Getters
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Unique ID of the thread on the platform.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public get id() { return this._id; }
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * URL of the thread.
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * It may vary depending on any versions of the contained product.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public get url() { return this._url; }
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Thread title.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public get title() { return this._title; };
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Tags associated with the thread.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public get tags() { return this._tags; }
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Prefixes associated with the thread
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public get prefixes() { return this._prefixes; }
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * List of posts belonging to the thread.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public get posts() { return this._posts; }
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Rating assigned to the thread.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public get rating() { return this._rating; }
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Owner of the thread.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public get owner() { return this._owner; }
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Creation date of the thread.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public get creation() { return this._creation; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //#endregion Getters
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Initializes an object for mapping a thread. 
 | 
				
			||||||
 | 
					     * 
 | 
				
			||||||
 | 
					     * The unique ID of the thread must be specified.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    constructor(id: number) { this._id = id; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //#region Private methods
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Set the number of posts to display for the current thread.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private async setMaximumPostsForPage(n: 20 | 40 | 60 | 100): Promise<void> {
 | 
				
			||||||
 | 
					        // Prepare the parameters to send via POST request
 | 
				
			||||||
 | 
					        const params = {
 | 
				
			||||||
 | 
					            "_xfResponseType": "json",
 | 
				
			||||||
 | 
					            "_xfRequestUri": `/account/dpp-update?content_type=thread&content_id=${this.id}`,
 | 
				
			||||||
 | 
					            "_xfToken": Shared.session.token,
 | 
				
			||||||
 | 
					            "_xfWithData": "1",
 | 
				
			||||||
 | 
					            "content_id": this.id.toString(),
 | 
				
			||||||
 | 
					            "content_type": "thread",
 | 
				
			||||||
 | 
					            "dpp_custom_config[posts]": n.toString(),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Send POST request
 | 
				
			||||||
 | 
					        const response = await fetchPOSTResponse(urls.F95_POSTS_NUMBER, params);
 | 
				
			||||||
 | 
					        if (response.isFailure()) throw response.value;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Gets all posts on a page.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private async parsePostsInPage(html: string): Promise<Post[]> {
 | 
				
			||||||
 | 
					        // Load the HTML
 | 
				
			||||||
 | 
					        const $ = cheerio.load(html);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Start parsing the posts
 | 
				
			||||||
 | 
					        const postPromises = $(THREAD.POSTS_IN_PAGE)
 | 
				
			||||||
 | 
					            .toArray()
 | 
				
			||||||
 | 
					            .map(async (idx, el) => {
 | 
				
			||||||
 | 
					                // Parse post data
 | 
				
			||||||
 | 
					                const p = new Post();
 | 
				
			||||||
 | 
					                await p.fetchData($(el));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return p;
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Wait for the post to be fetched
 | 
				
			||||||
 | 
					        return await Promise.all(postPromises);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Gets all posts in the thread.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private async fetchPosts(pages: number): Promise<Post[]> {
 | 
				
			||||||
 | 
					        // Local variables
 | 
				
			||||||
 | 
					        type TFetchResult = Promise<Result<GenericAxiosError | UnexpectedResponseContentType, string>>;
 | 
				
			||||||
 | 
					        const htmlPromiseList: TFetchResult[] = [];
 | 
				
			||||||
 | 
					        const fetchedPosts: Post[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Set the maximum number of post to 100
 | 
				
			||||||
 | 
					        await this.setMaximumPostsForPage(100);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Fetch posts for every page in the thread
 | 
				
			||||||
 | 
					        for (let i = 1; i <= pages; i++) {
 | 
				
			||||||
 | 
					            // Prepare the URL
 | 
				
			||||||
 | 
					            const url = new URL(`page-${i}`, urls.F95_BASE_URL).toString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Fetch the HTML source
 | 
				
			||||||
 | 
					            const htmlResponse = fetchHTML(url);
 | 
				
			||||||
 | 
					            htmlPromiseList.push(htmlResponse);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Wait for all the pages to load
 | 
				
			||||||
 | 
					        const responses = await Promise.all(htmlPromiseList);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Scrape the pages
 | 
				
			||||||
 | 
					        for (const response of responses) {
 | 
				
			||||||
 | 
					            if (response.isSuccess()) {
 | 
				
			||||||
 | 
					                // Parse the posts
 | 
				
			||||||
 | 
					                const posts = await this.parsePostsInPage(response.value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                fetchedPosts.push(...posts);
 | 
				
			||||||
 | 
					            } else throw response.value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Sorts the list of posts
 | 
				
			||||||
 | 
					        return fetchedPosts.sort((a, b) => (a.id > b.id) ? 1 : ((b.id > a.id) ? -1 : 0));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * It processes the rating of the thread 
 | 
				
			||||||
 | 
					     * starting from the data contained in the JSON+LD tag.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private parseRating(data: TJsonLD): TRating {
 | 
				
			||||||
 | 
					        const ratingTree = data["aggregateRating"] as TJsonLD;
 | 
				
			||||||
 | 
					        const rating: TRating = {
 | 
				
			||||||
 | 
					            average: parseFloat(ratingTree["ratingValue"] as string),
 | 
				
			||||||
 | 
					            best: parseInt(ratingTree["bestRating"] as string),
 | 
				
			||||||
 | 
					            count: parseInt(ratingTree["ratingCount"] as string),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return rating;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //#endregion Private methods
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //#region Public methods
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Gets information about this thread.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public async fetch() {
 | 
				
			||||||
 | 
					        // Prepare the url
 | 
				
			||||||
 | 
					        this._url = new URL(this.id.toString(), urls.F95_BASE_URL).toString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Fetch the HTML source
 | 
				
			||||||
 | 
					        const htmlResponse = await fetchHTML(this.url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (htmlResponse.isSuccess()) {
 | 
				
			||||||
 | 
					            // Load the HTML
 | 
				
			||||||
 | 
					            const $ = cheerio.load(htmlResponse.value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Fetch data from selectors
 | 
				
			||||||
 | 
					            const creationDatetime = $(THREAD.CREATION).attr("datetime");
 | 
				
			||||||
 | 
					            const ownerID = $(THREAD.OWNER_ID).attr("data-user-id");
 | 
				
			||||||
 | 
					            const tagArray = $(THREAD.TAGS).toArray();
 | 
				
			||||||
 | 
					            const prefixArray = $(THREAD.PREFIXES).toArray();
 | 
				
			||||||
 | 
					            const JSONLD = getJSONLD($("body"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Parse the thread's data
 | 
				
			||||||
 | 
					            this._title = $(THREAD.TITLE).text();
 | 
				
			||||||
 | 
					            this._creation = new Date(creationDatetime);
 | 
				
			||||||
 | 
					            this._tags = tagArray.map(el => $(el).text().trim());
 | 
				
			||||||
 | 
					            this._prefixes = prefixArray.map(el => $(el).text().trim());
 | 
				
			||||||
 | 
					            this._owner = new PlatformUser(parseInt(ownerID));
 | 
				
			||||||
 | 
					            this._rating = this.parseRating(JSONLD);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Parse all the posts
 | 
				
			||||||
 | 
					            const pages = parseInt($(THREAD.LAST_PAGE).first().text());
 | 
				
			||||||
 | 
					            this._posts = await this.fetchPosts(pages);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        } else throw htmlResponse.value;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    //#endregion Public methods
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue