From 12c36f7ced9d2e67e0b04583981465caf5f0a537 Mon Sep 17 00:00:00 2001 From: xhaeffer Date: Fri, 19 Apr 2024 01:32:59 +0700 Subject: [PATCH] first commit, add facebook --- src/modules/processing/match.js | 4 ++ src/modules/processing/services/facebook.js | 71 +++++++++++++++++++ src/modules/processing/servicesConfig.json | 14 +++- .../processing/servicesPatternTesters.js | 24 +++---- src/modules/processing/url.js | 10 +++ src/test/tests.json | 43 ++++++++++- 6 files changed, 151 insertions(+), 15 deletions(-) create mode 100644 src/modules/processing/services/facebook.js diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js index 6f715ef7..1ca9cb91 100644 --- a/src/modules/processing/match.js +++ b/src/modules/processing/match.js @@ -25,6 +25,7 @@ import streamable from "./services/streamable.js"; import twitch from "./services/twitch.js"; import rutube from "./services/rutube.js"; import dailymotion from "./services/dailymotion.js"; +import facebook from "./services/facebook.js"; export default async function(host, patternMatch, url, lang, obj) { assert(url instanceof URL); @@ -157,6 +158,9 @@ export default async function(host, patternMatch, url, lang, obj) { break; case "dailymotion": r = await dailymotion(patternMatch); + break; + case "facebook": + r = await facebook(patternMatch); break; default: return apiJSON(0, { t: errorUnsupported(lang) }); diff --git a/src/modules/processing/services/facebook.js b/src/modules/processing/services/facebook.js new file mode 100644 index 00000000..a6809f4d --- /dev/null +++ b/src/modules/processing/services/facebook.js @@ -0,0 +1,71 @@ +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 ({ shortLink, username, id }) { + const isShortLink = !!shortLink?.length + + let url = isShortLink + ? `https://fb.watch/${shortLink}` + : `https://web.facebook.com/${username}/videos/${id}` + + if (isShortLink) { + url = await resolveUrl(url) + } + + 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?.length) { + urls.push(JSON.parse(`["${hd[1]}"]`)[0]) + } + if (sd?.length) { + urls.push(JSON.parse(`["${sd[1]}"]`)[0]) + } + + if (!urls.length) { + return { error: 'ErrorEmptyDownload' }; + } + + let filename = `facebook_${id}.mp4` + if (isShortLink) { + filename = `facebook_${shortLink}.mp4` + } else if (username?.length && username !== 'user') { + filename = `facebook_${username}_${id}.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 633fa2a6..d4afb839 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -116,6 +116,18 @@ "alias": "dailymotion videos", "patterns": ["video/:id"], "enabled": true - } + }, + "facebook": { + "alias": "facebook videos", + "altDomains": ["fb.watch"], + "subdomains": ["web"], + "patterns": [ + "_shortLink/:shortLink", + ":username/videos/:caption/:id", + ":username/videos/:id", + "reel/:id" + ], + "enabled": true + } } } diff --git a/src/modules/processing/servicesPatternTesters.js b/src/modules/processing/servicesPatternTesters.js index f4dee15b..a6525b66 100644 --- a/src/modules/processing/servicesPatternTesters.js +++ b/src/modules/processing/servicesPatternTesters.js @@ -1,56 +1,54 @@ export const testers = { - "bilibili": (patternMatch) => + "bilibili": (patternMatch) => patternMatch.comId?.length <= 12 || patternMatch.comShortLink?.length <= 16 || patternMatch.tvId?.length <= 24, "dailymotion": (patternMatch) => patternMatch.id?.length <= 32, - "instagram": (patternMatch) => patternMatch.postId?.length <= 12 || (patternMatch.username?.length <= 30 && patternMatch.storyId?.length <= 24), - + "ok": (patternMatch) => patternMatch.id?.length <= 16, "pinterest": (patternMatch) => patternMatch.id?.length <= 128 || patternMatch.shortLink?.length <= 32, - "reddit": (patternMatch) => patternMatch.sub?.length <= 22 && patternMatch.id?.length <= 10, - "rutube": (patternMatch) => patternMatch.id?.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) => patternMatch.id?.length === 6, - + "tiktok": (patternMatch) => patternMatch.postId?.length <= 21 || patternMatch.id?.length <= 13, "tumblr": (patternMatch) => patternMatch.id?.length < 21 || (patternMatch.id?.length < 21 && patternMatch.user?.length <= 32), - "twitch": (patternMatch) => patternMatch.channel && patternMatch.clip?.length <= 100, - "twitter": (patternMatch) => patternMatch.id?.length < 20, - "vimeo": (patternMatch) => patternMatch.id?.length <= 11 && (!patternMatch.password || patternMatch.password.length < 16), - "vine": (patternMatch) => patternMatch.id?.length <= 12, - "vk": (patternMatch) => patternMatch.userId?.length <= 10 && patternMatch.videoId?.length <= 10, "youtube": (patternMatch) => patternMatch.id?.length <= 11, -} + + "facebook": (patternMatch) => + patternMatch.shortLink?.length <= 11 + || patternMatch.username?.length <= 30 + || patternMatch.caption?.length <= 255 + || patternMatch.id?.length <= 20, +} \ No newline at end of file diff --git a/src/modules/processing/url.js b/src/modules/processing/url.js index b272ff80..70061b58 100644 --- a/src/modules/processing/url.js +++ b/src/modules/processing/url.js @@ -64,6 +64,16 @@ export 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; } return url diff --git a/src/test/tests.json b/src/test/tests.json index bd8b33c5..be502186 100644 --- a/src/test/tests.json +++ b/src/test/tests.json @@ -1115,5 +1115,46 @@ "code": 200, "status": "stream" } - }] + }], + "facebook": [{ + "name": "direct video with username and id", + "url": "https://web.facebook.com/100048111287134/videos/1157798148685638/", + "params": {}, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "direct video with id as query param", + "url": "https://web.facebook.com/watch/?v=883839773514682&ref=sharing", + "params": {}, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "direct video with caption", + "url": "https://web.facebook.com/wood57/videos/𝐒𝐞𝐛𝐚𝐬𝐤𝐨𝐦-𝐟𝐮𝐥𝐥/883839773514682", + "params": {}, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "shortlink video", + "url": "https://fb.watch/r1K6XHMfGT/", + "params": {}, + "expected": { + "code": 200, + "status": "stream" + } + }, { + "name": "reel video", + "url": "https://web.facebook.com/reel/730293269054758", + "params": {}, + "expected": { + "code": 200, + "status": "stream" + } + }] }