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