From 6c9601690b938bd7819703d21de81edcb0d8ceb5 Mon Sep 17 00:00:00 2001 From: wukko Date: Sun, 1 Sep 2024 14:34:44 +0600 Subject: [PATCH] api: add support for bluesky videos & clean up service patterns --- api/src/processing/match.js | 7 ++ api/src/processing/service-config.js | 8 +- api/src/processing/service-patterns.js | 107 +++++++++++++------------ api/src/processing/services/bluesky.js | 52 ++++++++++++ 4 files changed, 121 insertions(+), 53 deletions(-) create mode 100644 api/src/processing/services/bluesky.js diff --git a/api/src/processing/match.js b/api/src/processing/match.js index d6c72e7e..cd30ae70 100644 --- a/api/src/processing/match.js +++ b/api/src/processing/match.js @@ -26,6 +26,7 @@ import dailymotion from "./services/dailymotion.js"; import snapchat from "./services/snapchat.js"; import loom from "./services/loom.js"; import facebook from "./services/facebook.js"; +import bluesky from "./services/bluesky.js"; let freebind; @@ -234,6 +235,12 @@ export default async function(host, patternMatch, obj) { }); break; + case "bsky": + r = await bluesky({ + ...patternMatch + }); + break; + default: return createResponse("error", { code: "error.api.service.unsupported" diff --git a/api/src/processing/service-config.js b/api/src/processing/service-config.js index 816eaa1c..67542e97 100644 --- a/api/src/processing/service-config.js +++ b/api/src/processing/service-config.js @@ -1,7 +1,7 @@ import UrlPattern from "url-pattern"; export const audioIgnore = ["vk", "ok", "loom"]; -export const hlsExceptions = ["dailymotion", "vimeo", "rutube"]; +export const hlsExceptions = ["dailymotion", "vimeo", "rutube", "bsky"]; export const services = { bilibili: { @@ -13,6 +13,12 @@ export const services = { ], subdomains: ["m"], }, + bsky: { + patterns: [ + "profile/:user/post/:post" + ], + tld: "app", + }, dailymotion: { patterns: ["video/:id"], }, diff --git a/api/src/processing/service-patterns.js b/api/src/processing/service-patterns.js index e8d169d5..2105a563 100644 --- a/api/src/processing/service-patterns.js +++ b/api/src/processing/service-patterns.js @@ -1,73 +1,76 @@ export const testers = { - "bilibili": (patternMatch) => - patternMatch.comId?.length <= 12 || patternMatch.comShortLink?.length <= 16 - || patternMatch.tvId?.length <= 24, + "bilibili": pattern => + pattern.comId?.length <= 12 || pattern.comShortLink?.length <= 16 + || pattern.tvId?.length <= 24, - "dailymotion": (patternMatch) => patternMatch.id?.length <= 32, + "dailymotion": pattern => pattern.id?.length <= 32, - "instagram": (patternMatch) => - patternMatch.postId?.length <= 12 - || (patternMatch.username?.length <= 30 && patternMatch.storyId?.length <= 24), + "instagram": pattern => + pattern.postId?.length <= 12 + || (pattern.username?.length <= 30 && pattern.storyId?.length <= 24), - "loom": (patternMatch) => - patternMatch.id?.length <= 32, + "loom": pattern => + pattern.id?.length <= 32, - "ok": (patternMatch) => - patternMatch.id?.length <= 16, + "ok": pattern => + pattern.id?.length <= 16, - "pinterest": (patternMatch) => - patternMatch.id?.length <= 128 || patternMatch.shortLink?.length <= 32, + "pinterest": pattern => + pattern.id?.length <= 128 || pattern.shortLink?.length <= 32, - "reddit": (patternMatch) => - (patternMatch.sub?.length <= 22 && patternMatch.id?.length <= 10) - || (patternMatch.user?.length <= 22 && patternMatch.id?.length <= 10), + "reddit": pattern => + (pattern.sub?.length <= 22 && pattern.id?.length <= 10) + || (pattern.user?.length <= 22 && pattern.id?.length <= 10), - "rutube": (patternMatch) => - (patternMatch.id?.length === 32 && patternMatch.key?.length <= 32) || - patternMatch.id?.length === 32 || patternMatch.yappyId?.length === 32, + "rutube": pattern => + (pattern.id?.length === 32 && pattern.key?.length <= 32) || + pattern.id?.length === 32 || pattern.yappyId?.length === 32, - "soundcloud": (patternMatch) => - (patternMatch.author?.length <= 255 && patternMatch.song?.length <= 255) - || patternMatch.shortLink?.length <= 32, + "soundcloud": pattern => + (pattern.author?.length <= 255 && pattern.song?.length <= 255) + || pattern.shortLink?.length <= 32, - "snapchat": (patternMatch) => - (patternMatch.username?.length <= 32 && (!patternMatch.storyId || patternMatch.storyId?.length <= 255)) - || patternMatch.spotlightId?.length <= 255 - || patternMatch.shortLink?.length <= 16, + "snapchat": pattern => + (pattern.username?.length <= 32 && (!pattern.storyId || pattern.storyId?.length <= 255)) + || pattern.spotlightId?.length <= 255 + || pattern.shortLink?.length <= 16, - "streamable": (patternMatch) => - patternMatch.id?.length === 6, + "streamable": pattern => + pattern.id?.length === 6, - "tiktok": (patternMatch) => - patternMatch.postId?.length <= 21 || patternMatch.id?.length <= 13, + "tiktok": pattern => + pattern.postId?.length <= 21 || pattern.id?.length <= 13, - "tumblr": (patternMatch) => - patternMatch.id?.length < 21 - || (patternMatch.id?.length < 21 && patternMatch.user?.length <= 32), + "tumblr": pattern => + pattern.id?.length < 21 + || (pattern.id?.length < 21 && pattern.user?.length <= 32), - "twitch": (patternMatch) => - patternMatch.channel && patternMatch.clip?.length <= 100, + "twitch": pattern => + pattern.channel && pattern.clip?.length <= 100, - "twitter": (patternMatch) => - patternMatch.id?.length < 20, + "twitter": pattern => + pattern.id?.length < 20, - "vimeo": (patternMatch) => - patternMatch.id?.length <= 11 - && (!patternMatch.password || patternMatch.password.length < 16), + "vimeo": pattern => + pattern.id?.length <= 11 + && (!pattern.password || pattern.password.length < 16), - "vine": (patternMatch) => - patternMatch.id?.length <= 12, + "vine": pattern => + pattern.id?.length <= 12, - "vk": (patternMatch) => - patternMatch.userId?.length <= 10 && patternMatch.videoId?.length <= 10, + "vk": pattern => + pattern.userId?.length <= 10 && pattern.videoId?.length <= 10, - "youtube": (patternMatch) => - patternMatch.id?.length <= 11, + "youtube": pattern => + pattern.id?.length <= 11, - "facebook": (patternMatch) => - patternMatch.shortLink?.length <= 11 - || patternMatch.username?.length <= 30 - || patternMatch.caption?.length <= 255 - || patternMatch.id?.length <= 20 && !patternMatch.shareType - || patternMatch.id?.length <= 20 && patternMatch.shareType?.length === 1, + "facebook": pattern => + pattern.shortLink?.length <= 11 + || pattern.username?.length <= 30 + || pattern.caption?.length <= 255 + || pattern.id?.length <= 20 && !pattern.shareType + || pattern.id?.length <= 20 && pattern.shareType?.length === 1, + + "bsky": pattern => + pattern.user?.length <= 128 && pattern.post?.length <= 128, } diff --git a/api/src/processing/services/bluesky.js b/api/src/processing/services/bluesky.js new file mode 100644 index 00000000..6a553359 --- /dev/null +++ b/api/src/processing/services/bluesky.js @@ -0,0 +1,52 @@ +import HLS from "hls-parser"; +import { cobaltUserAgent } from "../../config.js"; + +const extractVideo = async ({ getPost, filename }) => { + const urlMasterHLS = getPost?.thread?.post?.embed?.playlist; + if (!urlMasterHLS) return { error: "fetch.empty" }; + + const masterHLS = await fetch(urlMasterHLS) + .then(r => r.text()) + .catch(); + if (!masterHLS) return { error: "fetch.fail" }; + + const video = HLS.parse(masterHLS) + ?.variants + ?.reduce((a, b) => a?.bandwidth > b?.bandwidth ? a : b); + + const videoURL = new URL(video.uri, urlMasterHLS).toString(); + + return { + urls: videoURL, + filename: `${filename}.mp4`, + audioFilename: `${filename}_audio`, + isM3U8: true, + } +} + +export default async function ({ user, post }) { + const apiEndpoint = new URL("https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?depth=0&parentHeight=0"); + apiEndpoint.searchParams.set( + "uri", + `at://${user}/app.bsky.feed.post/${post}` + ); + + const getPost = await fetch(apiEndpoint, { + headers: { + "user-agent": cobaltUserAgent + } + }) + .then(r => r.json()) + .catch(); + + if (!getPost || getPost?.error) return { error: "fetch.empty" }; + + const embedType = getPost?.thread?.post?.embed?.$type; + const filename = `bluesky_${user}_${post}`; + + if (embedType === "app.bsky.embed.video#view") { + return await extractVideo({ getPost, filename }); + } + + return { error: "fetch.empty" }; +}