From ab1b07fe44a72715b5e573410f7e7a5eaf5c1fdc Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sun, 7 Jul 2024 15:14:12 +0000 Subject: [PATCH 01/25] rutube: pick closest quality to requested quality --- src/modules/processing/services/rutube.js | 24 ++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/modules/processing/services/rutube.js b/src/modules/processing/services/rutube.js index a8d0abbe..325e9ccc 100644 --- a/src/modules/processing/services/rutube.js +++ b/src/modules/processing/services/rutube.js @@ -10,6 +10,8 @@ async function requestJSON(url) { } catch {} } +const delta = (a, b) => Math.abs(a - b); + export default async function(obj) { if (obj.yappyId) { const yappy = await requestJSON( @@ -25,7 +27,7 @@ export default async function(obj) { } } - const quality = obj.quality === "max" ? "9000" : obj.quality; + const quality = Number(obj.quality) || 9000; const requestURL = new URL(`https://rutube.ru/api/play/options/${obj.id}/?no_404=true&referer&pver=v2`); if (obj.key) requestURL.searchParams.set('p', obj.key); @@ -45,12 +47,16 @@ export default async function(obj) { if (!m3u8) return { error: 'ErrorCouldntFetch' }; - m3u8 = HLS.parse(m3u8).variants.sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth)); + m3u8 = HLS.parse(m3u8).variants; - let bestQuality = m3u8[0]; - if (Number(quality) < bestQuality.resolution.height) { - bestQuality = m3u8.find((i) => (Number(quality) === i.resolution.height)); - } + const matchingQuality = m3u8.reduce((prev, next) => { + const diff = { + prev: delta(quality, prev.resolution.height), + next: delta(quality, next.resolution.height) + }; + + return diff.prev < diff.next ? prev : next; + }); const fileMetadata = { title: cleanString(play.title.trim()), @@ -58,15 +64,15 @@ export default async function(obj) { } return { - urls: bestQuality.uri, + urls: matchingQuality.uri, isM3U8: true, filenameAttributes: { service: "rutube", id: obj.id, title: fileMetadata.title, author: fileMetadata.artist, - resolution: `${bestQuality.resolution.width}x${bestQuality.resolution.height}`, - qualityLabel: `${bestQuality.resolution.height}p`, + resolution: `${matchingQuality.resolution.width}x${matchingQuality.resolution.height}`, + qualityLabel: `${matchingQuality.resolution.height}p`, extension: "mp4" }, fileMetadata: fileMetadata From 87783a4c862e980d0c964943e909c1f21aed2d8a Mon Sep 17 00:00:00 2001 From: jj Date: Sun, 7 Jul 2024 17:23:42 +0200 Subject: [PATCH 02/25] CONTRIBUTING: replace "message" with "title" when talking about amends --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dd2140eb..2668242b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,7 @@ the scope is not strictly defined, you can write whatever you find most fitting if you think a change deserves further explanation, we encourage you to write a short explanation in the commit message ([example](https://github.com/imputnet/cobalt/commit/d2e5b6542f71f3809ba94d56c26f382b5cb62762)), which will save both you and us time having to enquire about the change, and you explaining the reason behind it. -if your contribution has uninformative commit messages, you may be asked to interactively rebase your branch and amend each commit to include a meaningful message. +if your contribution has uninformative commit titles, you may be asked to interactively rebase your branch and amend each commit to include a meaningful title. ### clean commit history if your branch is out of date and/or has some merge conflicts with the `current` branch, you should **rebase** it instead of merging. this prevents meaningless merge commits from being included in your branch, which would then end up in the cobalt git history. From cfce04bbd00e36ca222aed1d0b7d997ed791c726 Mon Sep 17 00:00:00 2001 From: hyperdefined Date: Mon, 8 Jul 2024 18:03:03 -0400 Subject: [PATCH 03/25] tests: fix broken twitter links (#604) --- src/util/tests.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/util/tests.json b/src/util/tests.json index 982d99cb..ba79970c 100644 --- a/src/util/tests.json +++ b/src/util/tests.json @@ -1,7 +1,7 @@ { "twitter": [{ "name": "regular video", - "url": "https://twitter.com/TwitterSpaces/status/1526955853743546372?s=20", + "url": "https://twitter.com/X/status/1697304622749086011", "params": { "aFormat": "mp3", "isAudioOnly": false, @@ -14,7 +14,7 @@ }, { "name": "video with mobile web mediaviewer", - "url": "https://twitter.com/XSpaces/status/1526955853743546372/mediaViewer?currentTweet=1526955853743546372¤tTweetUser=XSpaces¤tTweet=1526955853743546372¤tTweetUser=XSpaces", + "url": "https://twitter.com/X/status/1697304622749086011/mediaViewer?currentTweet=1697304622749086011¤tTweetUser=X¤tTweet=1697304622749086011¤tTweetUser=X", "params": {}, "expected": { "code": 200, @@ -34,7 +34,7 @@ } }, { "name": "mixed media (image + gif)", - "url": "https://twitter.com/Twitter/status/1580661436132757506?s=20", + "url": "https://twitter.com/sky_mj26/status/1807756010712428565", "params": { "aFormat": "mp3", "isAudioOnly": false, From bcb8ab101f170fb870bc74528f6f39b33b0763e7 Mon Sep 17 00:00:00 2001 From: ihatespawn <168680471+ihatespawn@users.noreply.github.com> Date: Tue, 9 Jul 2024 15:17:09 +0200 Subject: [PATCH 04/25] tests: fix broken links, correct expected responses (#618) --- src/util/tests.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/util/tests.json b/src/util/tests.json index ba79970c..d3e5ec07 100644 --- a/src/util/tests.json +++ b/src/util/tests.json @@ -650,23 +650,23 @@ } }, { "name": "1080p dash parcel", - "url": "https://vimeo.com/774694040", + "url": "https://vimeo.com/967252742", "params": { "vQuality": "1440" }, "expected": { "code": 200, - "status": "stream" + "status": "redirect" } }, { "name": "720p dash parcel", - "url": "https://vimeo.com/774694040", + "url": "https://vimeo.com/967252742", "params": { "vQuality": "360" }, "expected": { "code": 200, - "status": "stream" + "status": "redirect" } }, { "name": "private video", @@ -947,7 +947,7 @@ }], "streamable": [{ "name": "regular video", - "url": "https://streamable.com/03r3c2", + "url": "https://streamable.com/p9cln4", "params": {}, "expected": { "code": 200, @@ -963,7 +963,7 @@ } }, { "name": "regular video (isAudioOnly)", - "url": "https://streamable.com/03r3c2", + "url": "https://streamable.com/p9cln4", "params": { "isAudioOnly": true }, @@ -973,7 +973,7 @@ } }, { "name": "regular video (isAudioMuted)", - "url": "https://streamable.com/03r3c2", + "url": "https://streamable.com/p9cln4", "params": { "isAudioMuted": true }, @@ -1042,8 +1042,8 @@ "url": "https://rutube.ru/video/b521653b4f71ece57b8ff54e57ca9b82/", "params": {}, "expected": { - "code": 200, - "status": "stream" + "code": 400, + "status": "error" } }, { "name": "vertical video", @@ -1055,7 +1055,7 @@ } }, { "name": "yappy", - "url": "https://rutube.ru/yappy/f1771e86294748eea8ecb2ac88e55740/", + "url": "https://rutube.ru/yappy/a06b1bf53bce403b9a069107f23c47eb/", "params": {}, "expected": { "code": 200, @@ -1157,8 +1157,8 @@ "isAudioOnly": true }, "expected": { - "code": 200, - "status": "stream" + "code": 400, + "status": "error" } }] } From 33fa653ee54e0cade6090a85e5081ba3374b6b97 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Tue, 9 Jul 2024 13:55:29 +0000 Subject: [PATCH 05/25] package: bump version to 7.14.6 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9c5434d9..6e4ea4f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cobalt", - "version": "7.14.5", + "version": "7.14.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cobalt", - "version": "7.14.5", + "version": "7.14.6", "license": "AGPL-3.0", "dependencies": { "content-disposition-header": "0.6.0", diff --git a/package.json b/package.json index b2224a50..75c8228c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cobalt", "description": "save what you love", - "version": "7.14.5", + "version": "7.14.6", "author": "imput", "exports": "./src/cobalt.js", "type": "module", From 404cad711f5f0da12919e1198650a54762c99048 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Wed, 10 Jul 2024 14:13:56 +0000 Subject: [PATCH 06/25] youtube: bump youtubei.js to v10.1.0 --- package-lock.json | 22 +++++++-------- package.json | 2 +- src/modules/processing/services/youtube.js | 33 ++++++++++++++-------- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6e4ea4f3..4917de2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "set-cookie-parser": "2.6.0", "undici": "^5.19.1", "url-pattern": "1.0.3", - "youtubei.js": "^9.3.0" + "youtubei.js": "^10.1.0" }, "engines": { "node": ">=18" @@ -73,9 +73,9 @@ } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "bin": { "acorn": "bin/acorn" }, @@ -683,9 +683,9 @@ } }, "node_modules/jintr": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jintr/-/jintr-1.1.0.tgz", - "integrity": "sha512-Tu9wk3BpN2v+kb8yT6YBtue+/nbjeLFv4vvVC4PJ7oCidHKbifWhvORrAbQfxVIQZG+67am/mDagpiGSVtvrZg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jintr/-/jintr-2.0.0.tgz", + "integrity": "sha512-RiVlevxttZ4eHEYB2dXKXDXluzHfRuw0DJQGsYuKCc5IvZj5/GbOakeqVX+Bar/G9kTty9xDJREcxukurkmYLA==", "funding": [ "https://github.com/sponsors/LuanRT" ], @@ -1123,14 +1123,14 @@ } }, "node_modules/youtubei.js": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-9.4.0.tgz", - "integrity": "sha512-8plCOZD2WabqWSEgZU3RjzigIIeR7sF028EERJENYrC9xO/6awpLMZfeoE1gNrNEbKcA+bzbMvonqlvBdxGdKg==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-10.1.0.tgz", + "integrity": "sha512-MokZMAnpWH11VYvWuW6qjPiiPmgRl5rfDgPQOpif9qXcVHoVw1hi8ePuRSD0AZSZ+uvWGe8rvas2dzp+Jv5JKQ==", "funding": [ "https://github.com/sponsors/LuanRT" ], "dependencies": { - "jintr": "^1.1.0", + "jintr": "^2.0.0", "tslib": "^2.5.0", "undici": "^5.19.1" } diff --git a/package.json b/package.json index 75c8228c..bcba1894 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "set-cookie-parser": "2.6.0", "undici": "^5.19.1", "url-pattern": "1.0.3", - "youtubei.js": "^9.3.0" + "youtubei.js": "^10.1.0" }, "optionalDependencies": { "freebind": "^0.2.2" diff --git a/src/modules/processing/services/youtube.js b/src/modules/processing/services/youtube.js index 1c86b4ed..b3a405e7 100644 --- a/src/modules/processing/services/youtube.js +++ b/src/modules/processing/services/youtube.js @@ -28,20 +28,24 @@ const transformSessionData = (cookie) => { if (!cookie) return; - const values = cookie.values(); + const values = { ...cookie.values() }; const REQUIRED_VALUES = [ 'access_token', 'refresh_token', - 'client_id', 'client_secret', - 'expires' + 'client_id', 'client_secret' ]; if (REQUIRED_VALUES.some(x => typeof values[x] !== 'string')) { return; } - return { - ...values, - expires: new Date(values.expires), - }; + + if (values.expires) { + values.expiry_date = values.expires; + delete values.expires; + } else if (!values.expiry_date) { + return; + } + + return values; } const cloneInnertube = async (customFetch) => { @@ -70,14 +74,19 @@ const cloneInnertube = async (customFetch) => { } if (session.logged_in) { - await session.oauth.refreshIfRequired(); - const oldExpiry = new Date(cookie.values().expires); - const newExpiry = session.oauth.credentials.expires; + if (session.oauth.shouldRefreshToken()) { + await session.oauth.refreshAccessToken(); + } + + const cookieValues = cookie.values(); + const oldExpiry = new Date(cookieValues.expiry_date); + const newExpiry = new Date(session.oauth.oauth2_tokens.expiry_date); if (oldExpiry.getTime() !== newExpiry.getTime()) { updateCookieValues(cookie, { - ...session.oauth.credentials, - expires: session.oauth.credentials.expires.toISOString() + ...session.oauth.client_id, + ...session.oauth.oauth2_tokens, + expiry_date: newExpiry.toISOString() }); } } From 20c409cdb308bcdd7e3e5ddffa97cc026ee2ab47 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Wed, 10 Jul 2024 15:14:28 +0000 Subject: [PATCH 07/25] generate-youtube-tokens: update response format --- src/util/generate-youtube-tokens.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/util/generate-youtube-tokens.js b/src/util/generate-youtube-tokens.js index 924afb13..ab877f96 100644 --- a/src/util/generate-youtube-tokens.js +++ b/src/util/generate-youtube-tokens.js @@ -20,9 +20,9 @@ tube.session.once( ); tube.session.once('auth-error', (err) => bail('An error occurred:', err)); -tube.session.once('auth', ({ status, credentials, ...rest }) => { - if (status !== 'SUCCESS') { - bail('something went wrong', rest); +tube.session.once('auth', ({ credentials }) => { + if (!credentials.access_token) { + bail('something went wrong'); } console.log( From 2f4e43f78f84ca53458c7e14d9a0a96fd5b1f7c1 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Thu, 11 Jul 2024 07:56:13 +0000 Subject: [PATCH 08/25] youtube: client_id/client_secret is optional in session data --- src/modules/processing/services/youtube.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/modules/processing/services/youtube.js b/src/modules/processing/services/youtube.js index b3a405e7..7beca2b4 100644 --- a/src/modules/processing/services/youtube.js +++ b/src/modules/processing/services/youtube.js @@ -29,10 +29,7 @@ const transformSessionData = (cookie) => { return; const values = { ...cookie.values() }; - const REQUIRED_VALUES = [ - 'access_token', 'refresh_token', - 'client_id', 'client_secret' - ]; + const REQUIRED_VALUES = [ 'access_token', 'refresh_token' ]; if (REQUIRED_VALUES.some(x => typeof values[x] !== 'string')) { return; From d68ce2f490a8cfaf35499150844b81c29e516a43 Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Fri, 12 Jul 2024 00:01:18 +0000 Subject: [PATCH 09/25] stream/types: only use `nice` if parsed `processingPriority` is a number for some reason, isNaN(true) -> false, which is technically correct, but what the fuck... --- src/modules/stream/types.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index af4aa2e5..071cb6d0 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -25,7 +25,7 @@ function killProcess(p) { } function getCommand(args) { - if (!isNaN(env.processingPriority)) { + if (typeof env.processingPriority === 'number' && !isNaN(env.processingPriority)) { return ['nice', ['-n', env.processingPriority.toString(), ffmpeg, ...args]] } return [ffmpeg, args] From 31e1fa5c5ce47bb603d3286e3d6eb77cbad0783a Mon Sep 17 00:00:00 2001 From: dumbmoron Date: Sat, 20 Jul 2024 12:53:59 +0200 Subject: [PATCH 10/25] run-an-instance: remove slash from end of CORS_URL example it's somewhat misleading, since this specifies the origin (https://html.spec.whatwg.org/multipage/browsers.html#concept-origin-tuple) and not a full URL --- docs/run-an-instance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/run-an-instance.md b/docs/run-an-instance.md index a440d11d..1faafb91 100644 --- a/docs/run-an-instance.md +++ b/docs/run-an-instance.md @@ -57,7 +57,7 @@ sudo service nscd start | `API_URL` | ➖ | `https://api.cobalt.tools/` | changes url from which api server is accessible.
***REQUIRED TO RUN THE API***. | | `API_NAME` | `unknown` | `ams-1` | api server name that is shown in `/api/serverInfo`. | | `CORS_WILDCARD` | `1` | `0` | toggles cross-origin resource sharing.
`0`: disabled. `1`: enabled. | -| `CORS_URL` | not used | `https://cobalt.tools/` | cross-origin resource sharing url. api will be available only from this url if `CORS_WILDCARD` is set to `0`. | +| `CORS_URL` | not used | `https://cobalt.tools` | cross-origin resource sharing url. api will be available only from this url if `CORS_WILDCARD` is set to `0`. | | `COOKIE_PATH` | not used | `/cookies.json` | path for cookie file relative to main folder. | | `PROCESSING_PRIORITY` | not used | `10` | changes `nice` value* for ffmpeg subprocess. available only on unix systems. | | `FREEBIND_CIDR` | ➖ | `2001:db8::/32` | IPv6 prefix used for randomly assigning addresses to cobalt requests. only supported on linux systems. see below for more info. | From c77ee2eb449bb4ca1726cc5846eb85a34590f453 Mon Sep 17 00:00:00 2001 From: Brama Udi Date: Wed, 24 Jul 2024 22:05:21 +0700 Subject: [PATCH 11/25] 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" + } }] } From 4080cd45812240c0aee621ae9784aea4eaed15ea Mon Sep 17 00:00:00 2001 From: Snazzah <7025343+Snazzah@users.noreply.github.com> Date: Wed, 24 Jul 2024 10:06:10 -0500 Subject: [PATCH 12/25] services: add snapchat support (#429) * feat: snapchat support * chore: remove redundancy * chore: a bit of better matching * chore: update readme * refactor(snapchat): refactor story matching to use pickers * fix: small fix to directly linked stories * fix(snapchat): fix filenames * chore: update readme * ref(snapchat): rewrite service, new test, split redirects into a util * fix(snapchat): small fixes * chore: deepscan error fixed * fix: remove debug logging * fix(snapchat): fix merge, clean up code with new utils * fix(snapchat): update with suggested changes --------- Signed-off-by: Snazzah <7025343+Snazzah@users.noreply.github.com> Co-authored-by: jj --- README.md | 2 + src/modules/processing/match.js | 9 ++ src/modules/processing/matchActionDecider.js | 2 + src/modules/processing/services/snapchat.js | 96 +++++++++++++++++++ src/modules/processing/servicesConfig.json | 6 ++ .../processing/servicesPatternTesters.js | 5 + src/modules/sub/utils.js | 7 ++ src/util/tests.json | 27 +++++- 8 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 src/modules/processing/services/snapchat.js diff --git a/README.md b/README.md index 9d6bb9a9..d5fe8164 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ this list is not final and keeps expanding over time. if support for a service y | pinterest | ✅ | ✅ | ✅ | ➖ | ➖ | | reddit | ✅ | ✅ | ✅ | ❌ | ❌ | | rutube | ✅ | ✅ | ✅ | ✅ | ✅ | +| snapchat stories & spotlights | ✅ | ✅ | ✅ | ➖ | ➖ | | soundcloud | ➖ | ✅ | ➖ | ✅ | ✅ | | streamable | ✅ | ✅ | ✅ | ➖ | ➖ | | tiktok | ✅ | ✅ | ✅ | ❌ | ❌ | @@ -49,6 +50,7 @@ this list is not final and keeps expanding over time. if support for a service y | facebook | supports public accessible videos content only. | | pinterest | supports photos, gifs, videos and stories. | | reddit | supports gifs and videos. | +| snapchat | supports spotlights and stories. lets you pick what to save from stories. | | rutube | supports yappy & private links. | | soundcloud | supports private links. | | tiktok | supports videos with or without watermark, images from slideshow without watermark, and full (original) audios. | diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js index 59bc2dd9..3c16a75b 100644 --- a/src/modules/processing/match.js +++ b/src/modules/processing/match.js @@ -24,6 +24,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 snapchat from "./services/snapchat.js"; import loom from "./services/loom.js"; import facebook from "./services/facebook.js"; @@ -189,6 +190,14 @@ export default async function(host, patternMatch, lang, obj) { case "dailymotion": r = await dailymotion(patternMatch); break; + case "snapchat": + r = await snapchat({ + url, + username: patternMatch.username, + storyId: patternMatch.storyId, + spotlightId: patternMatch.spotlightId, + shortLink: patternMatch.shortLink || false + }); case "loom": r = await loom({ id: patternMatch.id diff --git a/src/modules/processing/matchActionDecider.js b/src/modules/processing/matchActionDecider.js index 74f0ec49..7643d491 100644 --- a/src/modules/processing/matchActionDecider.js +++ b/src/modules/processing/matchActionDecider.js @@ -73,6 +73,7 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di switch (host) { case "instagram": case "twitter": + case "snapchat": params = { picker: r.picker }; break; case "tiktok": @@ -136,6 +137,7 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di case "tumblr": case "pinterest": case "streamable": + case "snapchat": case "loom": responseType = "redirect"; break; diff --git a/src/modules/processing/services/snapchat.js b/src/modules/processing/services/snapchat.js new file mode 100644 index 00000000..44a2b84e --- /dev/null +++ b/src/modules/processing/services/snapchat.js @@ -0,0 +1,96 @@ +import { genericUserAgent } from "../../config.js"; +import { getRedirectingURL } from "../../sub/utils.js"; +import { extract, normalizeURL } from "../url.js"; + +const SPOTLIGHT_VIDEO_REGEX = //; +const NEXT_DATA_REGEX = /