From 9c2390d697f7440844a30c988f0f57a05bd66f7f Mon Sep 17 00:00:00 2001 From: Toil <62353659+ilyhalight@users.noreply.github.com> Date: Sat, 21 Dec 2024 00:02:56 +0300 Subject: [PATCH] feature: add artstation --- packages/ext/src/data/sites.ts | 7 ++ packages/ext/src/helpers/artstation.ts | 89 ++++++++++++++++++++ packages/ext/src/helpers/index.ts | 3 + packages/ext/src/types/helpers/artstation.ts | 55 ++++++++++++ packages/ext/src/types/service.ts | 1 + 5 files changed, 155 insertions(+) create mode 100644 packages/ext/src/helpers/artstation.ts create mode 100644 packages/ext/src/types/helpers/artstation.ts diff --git a/packages/ext/src/data/sites.ts b/packages/ext/src/data/sites.ts index 8c12251c..9ae1c2ba 100644 --- a/packages/ext/src/data/sites.ts +++ b/packages/ext/src/data/sites.ts @@ -459,6 +459,13 @@ export default [ selector: ".VideoLayersContainer", needExtraData: true, }, + { + host: ExtVideoService.artstation, + url: "https://www.artstation.com/learning/", + match: /^(www.)?artstation.com$/, + selector: ".vjs-v7", + needExtraData: true, + }, { host: CoreVideoService.custom, url: "stub", diff --git a/packages/ext/src/helpers/artstation.ts b/packages/ext/src/helpers/artstation.ts new file mode 100644 index 00000000..23989db5 --- /dev/null +++ b/packages/ext/src/helpers/artstation.ts @@ -0,0 +1,89 @@ +import { BaseHelper } from "./base"; +import type { MinimalVideoData } from "../types/client"; +import * as Artstation from "../types/helpers/artstation"; + +import type { VideoDataSubtitle } from "@vot.js/core/types/client"; +import { normalizeLang } from "@vot.js/shared/utils/utils"; +import Logger from "@vot.js/shared/utils/logger"; + +export default class ArtstationHelper extends BaseHelper { + API_ORIGIN = "https://www.artstation.com/api/v2/learning"; + + async getCourseInfo(courseId: string) { + try { + const res = await this.fetch( + `${this.API_ORIGIN}/courses/${courseId}/autoplay.json`, + ); + + return (await res.json()) as Artstation.Course; + } catch (err) { + Logger.error( + `Failed to get artstation course info by courseId: ${courseId}.`, + (err as Error).message, + ); + return false; + } + } + + async getVideoUrl(chapterId: number) { + try { + const res = await this.fetch( + `${this.API_ORIGIN}/quicksilver/video_url.json?chapter_id=${chapterId}`, + ); + + const data = (await res.json()) as Artstation.VideoUrlData; + return data.url.replace("qsep://", "https://"); + } catch (err) { + Logger.error( + `Failed to get artstation video url by chapterId: ${chapterId}.`, + (err as Error).message, + ); + return false; + } + } + + async getVideoData(videoId: string): Promise { + const [, courseId, , , chapterId] = videoId.split("/")?.[1]; + const courseInfo = await this.getCourseInfo(courseId); + if (!courseInfo) { + return undefined; + } + + const chapter = courseInfo.chapters.find( + (chapter) => chapter.hash_id === chapterId, + ); + if (!chapter) { + return undefined; + } + + const videoUrl = await this.getVideoUrl(chapter.id); + if (!videoUrl) { + return undefined; + } + + const { title, duration, subtitles: videoSubtitles } = chapter; + const subtitles: VideoDataSubtitle[] = videoSubtitles + .filter((subtitle) => subtitle.format === "vtt") + .map((subtitle) => ({ + language: normalizeLang(subtitle.locale), + source: "artstation", + format: "vtt", + url: subtitle.file_url, + })); + + // url returns a json containing a dash playlist (in base64) in the playlist field + return { + url: videoUrl, + title, + duration, + subtitles, + }; + } + + // eslint-disable-next-line @typescript-eslint/require-await + async getVideoId(url: URL): Promise { + return /courses\/(\w{3,5})\/([^/]+)\/chapters\/(\w{3,5})/.exec( + url.pathname, + )?.[0]; + } +} diff --git a/packages/ext/src/helpers/index.ts b/packages/ext/src/helpers/index.ts index daab547a..c467a14d 100644 --- a/packages/ext/src/helpers/index.ts +++ b/packages/ext/src/helpers/index.ts @@ -51,6 +51,7 @@ import CourseraHelper from "./coursera"; import CloudflareStreamHelper from "./cloudflarestream"; import DouyinHelper from "./douyin"; import LoomHelper from "./loom"; +import ArtstationHelper from "./artstation"; export * as MailRuHelper from "./mailru"; export * as WeverseHelper from "./weverse"; @@ -100,6 +101,7 @@ export * as CourseraHelper from "./coursera"; export * as CloudflareStreamHelper from "./cloudflarestream"; export * as DouyinHelper from "./douyin"; export * as LoomHelper from "./loom"; +export * as ArtstationHelper from "./artstation"; export const availableHelpers = { [CoreVideoService.mailru]: MailRuHelper, @@ -155,6 +157,7 @@ export const availableHelpers = { [ExtVideoService.udemy]: UdemyHelper, [ExtVideoService.coursera]: CourseraHelper, [ExtVideoService.douyin]: DouyinHelper, + [ExtVideoService.artstation]: ArtstationHelper, }; export type AvailableVideoHelpers = typeof availableHelpers; diff --git a/packages/ext/src/types/helpers/artstation.ts b/packages/ext/src/types/helpers/artstation.ts new file mode 100644 index 00000000..471ab791 --- /dev/null +++ b/packages/ext/src/types/helpers/artstation.ts @@ -0,0 +1,55 @@ +import { ISODate } from "@vot.js/shared/types/utils"; + +export type Subtitle = { + id: number; + locale: string; + file_url: string; + format: "str" | "vtt"; +}; + +export type Chapter = { + id: number; + hash_id: string; + slug: string; + title: string; + position: number; + free: boolean; + viewed: boolean; + duration: number; + subtitles: Subtitle[]; +}; + +export type Level = "beginner" | "intermediate"; +export type AutoPlayData = { + chapter_id: number; + playback_time: number; + drm_token: null; +}; + +export type Course = { + id: number; + hash_id: string; + slug: string; + title: string; + description: string; // with html + medium_cover_url: string; + larget_cover_url: string; + published_at: ISODate; + level: Level; + duration: number; + series_hash_id: null | string; + likes_count: number; + learners_count: number; + liked: boolean; + playlist: unknown[]; + instructors: unknown[]; + chapters: Chapter[]; + topics: unknown[]; + attached_files: unknown[]; + autoplay_data: AutoPlayData; +}; + +export type VideoUrlData = { + url: string; + type: "quicksilver"; +}; diff --git a/packages/ext/src/types/service.ts b/packages/ext/src/types/service.ts index cbad63f0..10a58e7f 100644 --- a/packages/ext/src/types/service.ts +++ b/packages/ext/src/types/service.ts @@ -10,6 +10,7 @@ export enum ExtVideoService { udemy = "udemy", coursera = "coursera", douyin = "douyin", + artstation = "artstation", } export type VideoService = CoreVideoService | ExtVideoService;