|
@@ -0,0 +1,159 @@
|
|
|
|
|
+import { type Handler } from "aws-lambda";
|
|
|
|
|
+import { createRestAPIClient, mastodon } from "masto";
|
|
|
|
|
+import { CronJob } from "cron";
|
|
|
|
|
+
|
|
|
|
|
+import Scraper from "../../utils/scraper";
|
|
|
|
|
+import ScraperMethods from "../../enums/scraper-methods";
|
|
|
|
|
+import Emojis from "../../enums/emojis";
|
|
|
|
|
+import LogLevels from "../../enums/log-levels";
|
|
|
|
|
+import config from "../../config";
|
|
|
|
|
+
|
|
|
|
|
+interface IStreamConfig {
|
|
|
|
|
+ location: string;
|
|
|
|
|
+ url: string;
|
|
|
|
|
+ hashtags: string[];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export default class VideoStreams {
|
|
|
|
|
+ private readonly _name: string = "Video Streams";
|
|
|
|
|
+ private readonly _mastodonClient: mastodon.rest.Client;
|
|
|
|
|
+ private readonly _scraper: Scraper;
|
|
|
|
|
+ private readonly _selector: string = "#movie_player";
|
|
|
|
|
+ private readonly _clickSelector: string = ".ytp-large-play-button";
|
|
|
|
|
+
|
|
|
|
|
+ constructor(accessToken = config.MASTODON_TEST_ACCESS_TOKEN) {
|
|
|
|
|
+ accessToken = config.DEVELOP ? config.MASTODON_TEST_ACCESS_TOKEN : accessToken;
|
|
|
|
|
+ this._mastodonClient = createRestAPIClient({ url: config.MASTODON_URL, accessToken });
|
|
|
|
|
+ this._scraper = new Scraper();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private parseStreams(): IStreamConfig[] {
|
|
|
|
|
+ const streamsString = config.VIDEO_STREAMS;
|
|
|
|
|
+ if (!streamsString || streamsString.trim() === "") {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const streams: IStreamConfig[] = [];
|
|
|
|
|
+ const streamPairs = streamsString.split(";");
|
|
|
|
|
+
|
|
|
|
|
+ streamPairs.forEach((streamPair) => {
|
|
|
|
|
+ const [location, url, hashTags] = streamPair.split("|");
|
|
|
|
|
+ if (location && url) {
|
|
|
|
|
+ streams.push({
|
|
|
|
|
+ location: location.trim(),
|
|
|
|
|
+ url: url.trim(),
|
|
|
|
|
+ hashtags: hashTags?.split(",").map((h) => h.trim()).filter((h) => h !== "") ?? []
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return streams;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private base64ToFile(base64: string, fileName: string): File {
|
|
|
|
|
+ const buffer = Buffer.from(base64, "base64");
|
|
|
|
|
+ return new File([buffer], fileName, { type: "image/png" });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private buildMessage(stream: IStreamConfig): string {
|
|
|
|
|
+ let message = `${Emojis.VIDEO_CAMERA} Transmisión en vivo desde ${stream.location}`;
|
|
|
|
|
+ message += `\n\n${Emojis.LINK} ${stream.url}`;
|
|
|
|
|
+
|
|
|
|
|
+ if (stream.hashtags.length > 0) {
|
|
|
|
|
+ message += `\n\n${stream.hashtags.map((hashtag) => `#${hashtag}`).join(" ")}`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return message;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private async publish(stream: IStreamConfig, screenshotBase64: string): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const media = await this._mastodonClient.v2.media.create({
|
|
|
|
|
+ file: this.base64ToFile(screenshotBase64, `${stream.location.replace(/\s+/g, "_")}.png`),
|
|
|
|
|
+ description: `Transmisión en vivo desde ${stream.location}`
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const message = this.buildMessage(stream);
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`\n${this._name} | Sending\n`, message);
|
|
|
|
|
+
|
|
|
|
|
+ await this._mastodonClient.v1.statuses.create({ status: message, mediaIds: [media.id] });
|
|
|
|
|
+ } catch (err: any) {
|
|
|
|
|
+ console.error(`${this._name} | Error publishing stream ${stream.location}`);
|
|
|
|
|
+ console.error(err.message);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public async run(event?: any, context?: any): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const streams = this.parseStreams();
|
|
|
|
|
+
|
|
|
|
|
+ if (streams.length === 0) {
|
|
|
|
|
+ console.log(`${this._name} | No streams configured`);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`${this._name} | Found ${streams.length} stream(s)`);
|
|
|
|
|
+
|
|
|
|
|
+ for (const stream of streams) {
|
|
|
|
|
+ console.log(`${this._name} | Capturing ${stream.location}`);
|
|
|
|
|
+
|
|
|
|
|
+ const response = await this._scraper.scrape({
|
|
|
|
|
+ url: stream.url,
|
|
|
|
|
+ scraperMethod: ScraperMethods.PUPPETEER,
|
|
|
|
|
+ screenshotSelector: this._selector,
|
|
|
|
|
+ screenshotDelay: config.VIDEO_STREAMS_SCREENSHOT_DELAY,
|
|
|
|
|
+ clickSelector: config.VIDEO_STREAMS_CLICK_SELECTOR
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const screenshotBase64 = response?.data?.data?.screenshot;
|
|
|
|
|
+
|
|
|
|
|
+ if (!screenshotBase64 || typeof screenshotBase64 !== "string") {
|
|
|
|
|
+ console.log(`${this._name} | No screenshot obtained for ${stream.location}`);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await this.publish(stream, screenshotBase64);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`${this._name} | Finished`);
|
|
|
|
|
+ } catch (err: any) {
|
|
|
|
|
+ console.log(`${this._name} | An error has occurred\n`);
|
|
|
|
|
+ console.error(err.message);
|
|
|
|
|
+ if (config.LOG_LEVEL === LogLevels.DEBUG) {
|
|
|
|
|
+ if (event !== undefined) {
|
|
|
|
|
+ console.debug("\nEvent\n");
|
|
|
|
|
+ console.debug(event);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (context !== undefined) {
|
|
|
|
|
+ console.debug("\nContext\n");
|
|
|
|
|
+ console.debug(context);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public getHandler(): Handler {
|
|
|
|
|
+ return async (event, context) => {
|
|
|
|
|
+ await this.run(event, context);
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+try {
|
|
|
|
|
+ const videoStreams = new VideoStreams(config.DEVELOP ? config.MASTODON_TEST_ACCESS_TOKEN : config.MASTODON_KEY_VIDEO_STREAMS);
|
|
|
|
|
+ new CronJob(
|
|
|
|
|
+ "0 0 * * * *",
|
|
|
|
|
+ () => videoStreams.run(),
|
|
|
|
|
+ null,
|
|
|
|
|
+ true,
|
|
|
|
|
+ config.DEFAULT_TIMEZONE
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (config.DEVELOP) {
|
|
|
|
|
+ videoStreams.run();
|
|
|
|
|
+ }
|
|
|
|
|
+} catch (error) {
|
|
|
|
|
+ console.error(error);
|
|
|
|
|
+}
|