Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add facebook support #403

Open
wants to merge 6 commits into
base: current
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ this list is not final and keeps expanding over time. if support for a service y
| :-------- | :-----------: | :--------: | :--------: | :------: | :-------------: |
| bilibili.com & bilibili.tv | ✅ | ✅ | ✅ | ➖ | ➖ |
| dailymotion | ✅ | ✅ | ✅ | ✅ | ✅ |
| facebook videos | ✅ | ❌ | ❌ | ➖ | ➖ |
| instagram posts & stories | ✅ | ✅ | ✅ | ➖ | ➖ |
| instagram reels | ✅ | ✅ | ✅ | ➖ | ➖ |
| ok video | ✅ | ❌ | ❌ | ✅ | ✅ |
Expand All @@ -41,6 +42,7 @@ this list is not final and keeps expanding over time. if support for a service y
### additional notes or features (per service)
| service | notes or features |
| :-------- | :----- |
| facebook | supports public accessible videos content only. |
| instagram | supports photos, videos, and stories. lets you pick what to save from multi-media posts. |
| pinterest | supports videos and stories. |
| reddit | supports gifs and videos. |
Expand Down
4 changes: 4 additions & 0 deletions src/modules/processing/match.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -161,6 +162,9 @@ export default async function(host, patternMatch, url, lang, obj) {
case "dailymotion":
r = await dailymotion(patternMatch);
break;
case "facebook":
r = await facebook(url.href, patternMatch);
break;
default:
return apiJSON(0, { t: errorUnsupported(lang) });
}
Expand Down
76 changes: 76 additions & 0 deletions src/modules/processing/services/facebook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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\/v\//)?.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?.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`,
};
}
13 changes: 13 additions & 0 deletions src/modules/processing/servicesConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,19 @@
"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",
"share/v/:id"
],
"enabled": true
}
}
}
14 changes: 10 additions & 4 deletions src/modules/processing/servicesPatternTesters.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const testers = {
"bilibili": (patternMatch) =>
"bilibili": (patternMatch) =>
patternMatch.comId?.length <= 12 || patternMatch.comShortLink?.length <= 16
|| patternMatch.tvId?.length <= 24,

Expand All @@ -8,7 +8,7 @@ export const testers = {
"instagram": (patternMatch) =>
patternMatch.postId?.length <= 12
|| (patternMatch.username?.length <= 30 && patternMatch.storyId?.length <= 24),

"ok": (patternMatch) =>
patternMatch.id?.length <= 16,

Expand All @@ -23,12 +23,12 @@ 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) =>
patternMatch.id?.length === 6,

"tiktok": (patternMatch) =>
patternMatch.postId?.length <= 21 || patternMatch.id?.length <= 13,

Expand All @@ -54,4 +54,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,
}
14 changes: 12 additions & 2 deletions src/modules/processing/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ export function aliasURL(url) {
** but we only care about the 1st segment of the path */
url = new URL(`https://youtube.com/watch?v=${
encodeURIComponent(parts[1])
}`)
}`)
}
break;

case "pin":
if (url.hostname === 'pin.it' && parts.length === 2) {
url = new URL(`https://pinterest.com/url_shortener/${
encodeURIComponent(parts[1])
}`)
}`)
}
break;

Expand Down Expand Up @@ -64,7 +64,17 @@ 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;

case "ddinstagram":
if (services.instagram.altDomains.includes(host.domain) && [null, 'd', 'g'].includes(host.subdomain)) {
url.hostname = 'instagram.com';
Expand Down
49 changes: 49 additions & 0 deletions src/test/tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -1155,5 +1155,54 @@
"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"
}
}, {
"name": "shared video link",
"url": "https://www.facebook.com/share/v/NEf87jbPTvFE8LsL/",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}]
}