From c77ee2eb449bb4ca1726cc5846eb85a34590f453 Mon Sep 17 00:00:00 2001 From: Brama Udi Date: Wed, 24 Jul 2024 22:05:21 +0700 Subject: [PATCH] services: add facebook support (#403) * feat: add facebook support * chore: fix fail check * chore: minor fix * chore: add service in README.md * chore: cleaning post-merge code * facebook: add shared link pattern * chore: clean up removing unnecessarily code * fix: facebook shared link pattern * matchActionDecider: redirect to facebook video instead of rendering * facebook: pass sourceUrl in object * url: fix botched lint * fix: facebook shared link pattern with clean up * test: change facebook test response to redirect --------- Co-authored-by: dumbmoron --- README.md | 2 + src/modules/processing/match.js | 6 ++ src/modules/processing/matchActionDecider.js | 1 + src/modules/processing/services/facebook.js | 62 +++++++++++++++++++ src/modules/processing/servicesConfig.json | 13 ++++ .../processing/servicesPatternTesters.js | 10 ++- src/modules/processing/url.js | 10 +++ src/util/tests.json | 57 +++++++++++++++++ 8 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 src/modules/processing/services/facebook.js diff --git a/README.md b/README.md index d2bb064c..9d6bb9a9 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ this list is not final and keeps expanding over time. if support for a service y | bilibili.com & bilibili.tv | ✅ | ✅ | ✅ | ➖ | ➖ | | dailymotion | ✅ | ✅ | ✅ | ✅ | ✅ | | instagram posts & reels | ✅ | ✅ | ✅ | ➖ | ➖ | +| facebook videos | ✅ | ❌ | ❌ | ➖ | ➖ | | loom | ✅ | ❌ | ✅ | ✅ | ➖ | | ok video | ✅ | ❌ | ✅ | ✅ | ✅ | | pinterest | ✅ | ✅ | ✅ | ➖ | ➖ | @@ -45,6 +46,7 @@ this list is not final and keeps expanding over time. if support for a service y | service | notes or features | | :-------- | :----- | | instagram | supports reels, photos, and videos. lets you pick what to save from multi-media posts. | +| facebook | supports public accessible videos content only. | | pinterest | supports photos, gifs, videos and stories. | | reddit | supports gifs and videos. | | rutube | supports yappy & private links. | diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js index 3e38c4db..59bc2dd9 100644 --- a/src/modules/processing/match.js +++ b/src/modules/processing/match.js @@ -25,6 +25,7 @@ import twitch from "./services/twitch.js"; import rutube from "./services/rutube.js"; import dailymotion from "./services/dailymotion.js"; import loom from "./services/loom.js"; +import facebook from "./services/facebook.js"; let freebind; @@ -192,6 +193,11 @@ export default async function(host, patternMatch, lang, obj) { r = await loom({ id: patternMatch.id }); + case "facebook": + r = await facebook({ + ...patternMatch, + sourceUrl: url.href + }); break; default: return createResponse("error", { diff --git a/src/modules/processing/matchActionDecider.js b/src/modules/processing/matchActionDecider.js index 74f0f8c7..74f0ec49 100644 --- a/src/modules/processing/matchActionDecider.js +++ b/src/modules/processing/matchActionDecider.js @@ -130,6 +130,7 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di params = { type: "bridge" }; break; + case "facebook": case "vine": case "instagram": case "tumblr": diff --git a/src/modules/processing/services/facebook.js b/src/modules/processing/services/facebook.js new file mode 100644 index 00000000..45d31b5f --- /dev/null +++ b/src/modules/processing/services/facebook.js @@ -0,0 +1,62 @@ +import { genericUserAgent } from "../../config.js"; + +const headers = { + 'User-Agent': genericUserAgent, + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + 'Accept-Encoding': 'gzip, deflate, br', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'none', +} + +function resolveUrl(url) { + return fetch(url, { headers }) + .then(r => { + if (r.headers.get('location')) { + return decodeURIComponent(r.headers.get('location')) + } + if (r.headers.get('link')) { + const linkMatch = r.headers.get('link').match(/<(.*?)\/>/) + return decodeURIComponent(linkMatch[1]) + } + return false + }) + .catch(() => false) +} + +export default async function({ sourceUrl, shortLink, username, id }) { + const isShortLink = !!shortLink?.length + const isSharedLink = !!sourceUrl.match(/\/share\/\w\//)?.length + + let url = isShortLink + ? `https://fb.watch/${shortLink}` + : `https://web.facebook.com/${username}/videos/${id}` + + if (isShortLink) url = await resolveUrl(url) + if (isSharedLink) url = sourceUrl + + const html = await fetch(url, { headers }) + .then(r => r.text()) + .catch(() => false) + + if (!html) return { error: 'ErrorCouldntFetch' }; + + const urls = [] + const hd = html.match('"browser_native_hd_url":(".*?")') + const sd = html.match('"browser_native_sd_url":(".*?")') + + if (hd?.[1]) urls.push(JSON.parse(hd[1])) + if (sd?.[1]) urls.push(JSON.parse(sd[1])) + + if (!urls.length) { + return { error: 'ErrorEmptyDownload' }; + } + + let filename = `facebook_${id || shortLink}.mp4` + + return { + urls: urls[0], + filename, + audioFilename: `${filename.slice(0, -4)}_audio`, + }; +} \ No newline at end of file diff --git a/src/modules/processing/servicesConfig.json b/src/modules/processing/servicesConfig.json index d727b9a5..b6c62cf2 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -118,6 +118,19 @@ "alias": "loom videos", "patterns": ["share/:id"], "enabled": true + }, + "facebook": { + "alias": "facebook videos", + "altDomains": ["fb.watch"], + "subdomains": ["web"], + "patterns": [ + "_shortLink/:shortLink", + ":username/videos/:caption/:id", + ":username/videos/:id", + "reel/:id", + "share/:shortLink/:id" + ], + "enabled": true } } } diff --git a/src/modules/processing/servicesPatternTesters.js b/src/modules/processing/servicesPatternTesters.js index ddeea31f..0fed8723 100644 --- a/src/modules/processing/servicesPatternTesters.js +++ b/src/modules/processing/servicesPatternTesters.js @@ -1,5 +1,5 @@ export const testers = { - "bilibili": (patternMatch) => + "bilibili": (patternMatch) => patternMatch.comId?.length <= 12 || patternMatch.comShortLink?.length <= 16 || patternMatch.tvId?.length <= 24, @@ -27,7 +27,7 @@ export const testers = { patternMatch.id?.length === 32 || patternMatch.yappyId?.length === 32, "soundcloud": (patternMatch) => - (patternMatch.author?.length <= 255 && patternMatch.song?.length <= 255) + (patternMatch.author?.length <= 255 && patternMatch.song?.length <= 255) || patternMatch.shortLink?.length <= 32, "streamable": (patternMatch) => @@ -58,4 +58,10 @@ export const testers = { "youtube": (patternMatch) => patternMatch.id?.length <= 11, + + "facebook": (patternMatch) => + patternMatch.shortLink?.length <= 11 + || patternMatch.username?.length <= 30 + || patternMatch.caption?.length <= 255 + || patternMatch.id?.length <= 20, } diff --git a/src/modules/processing/url.js b/src/modules/processing/url.js index 111f1f6f..180ec6ee 100644 --- a/src/modules/processing/url.js +++ b/src/modules/processing/url.js @@ -64,7 +64,17 @@ function aliasURL(url) { if (url.hostname === 'dai.ly' && parts.length === 2) { url = new URL(`https://dailymotion.com/video/${parts[1]}`) } + + case "facebook": + case "fb": + if (url.searchParams.get('v')) { + url = new URL(`https://web.facebook.com/user/videos/${url.searchParams.get('v')}`) + } + if (url.hostname === 'fb.watch') { + url = new URL(`https://web.facebook.com/_shortLink/${parts[1]}`) + } break; + case "ddinstagram": if (services.instagram.altDomains.includes(host.domain) && [null, 'd', 'g'].includes(host.subdomain)) { url.hostname = 'instagram.com'; diff --git a/src/util/tests.json b/src/util/tests.json index d3e5ec07..89e02daf 100644 --- a/src/util/tests.json +++ b/src/util/tests.json @@ -1160,5 +1160,62 @@ "code": 400, "status": "error" } + }], + "facebook": [{ + "name": "direct video with username and id", + "url": "https://web.facebook.com/100048111287134/videos/1157798148685638/", + "params": {}, + "expected": { + "code": 200, + "status": "redirect" + } + }, { + "name": "direct video with id as query param", + "url": "https://web.facebook.com/watch/?v=883839773514682&ref=sharing", + "params": {}, + "expected": { + "code": 200, + "status": "redirect" + } + }, { + "name": "direct video with caption", + "url": "https://web.facebook.com/wood57/videos/𝐒𝐞𝐛𝐚𝐬𝐤𝐨𝐦-𝐟𝐮𝐥𝐥/883839773514682", + "params": {}, + "expected": { + "code": 200, + "status": "redirect" + } + }, { + "name": "shortlink video", + "url": "https://fb.watch/r1K6XHMfGT/", + "params": {}, + "expected": { + "code": 200, + "status": "redirect" + } + }, { + "name": "reel video", + "url": "https://web.facebook.com/reel/730293269054758", + "params": {}, + "expected": { + "code": 200, + "status": "redirect" + } + }, { + "name": "shared video link", + "url": "https://www.facebook.com/share/v/NEf87jbPTvFE8LsL/", + "params": {}, + "expected": { + "code": 200, + "status": "redirect" + } + }, { + "name": "shared video link v2", + "url": "https://web.facebook.com/share/r/JFZfPVgLkiJQmWrr/", + "params": {}, + "expected": { + "code": 200, + "status": "redirect" + } }] }