diff --git a/README.md b/README.md index 5bf8ae8e3..54eacafe1 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ this list is not final and keeps expanding over time. if support for a service y | snapchat | ✅ | ✅ | ✅ | ➖ | ➖ | | soundcloud | ➖ | ✅ | ➖ | ✅ | ✅ | | streamable | ✅ | ✅ | ✅ | ➖ | ➖ | +| threads posts | ✅ | ✅ | ✅ | ➖ | ➖ | | tiktok | ✅ | ✅ | ✅ | ❌ | ❌ | | tumblr | ✅ | ✅ | ✅ | ➖ | ➖ | | twitch clips | ✅ | ✅ | ✅ | ✅ | ✅ | @@ -68,6 +69,7 @@ this list is not final and keeps expanding over time. if support for a service y | snapchat | supports spotlights and stories. lets you pick what to save from stories. | | rutube | supports yappy & private links. | | soundcloud | supports private links. | +| threads | supports photos and videos. lets you pick what to save from multi-media posts. | | tiktok | supports videos with or without watermark, images from slideshow without watermark, and full (original) audios. | | twitter/x | lets you pick what to save from multi-media posts. may not be 100% reliable due to current management. | | vimeo | audio downloads are only available for dash. | diff --git a/api/src/processing/match-action.js b/api/src/processing/match-action.js index 31d12e7fa..28e6c62ac 100644 --- a/api/src/processing/match-action.js +++ b/api/src/processing/match-action.js @@ -79,6 +79,7 @@ export default function({ r, host, audioFormat, isAudioOnly, isAudioMuted, disab switch (host) { case "instagram": case "twitter": + case "threads": case "snapchat": case "bsky": params = { picker: r.picker }; @@ -151,6 +152,7 @@ export default function({ r, host, audioFormat, isAudioOnly, isAudioMuted, disab case "streamable": case "snapchat": case "loom": + case "threads": case "twitch": responseType = "redirect"; break; diff --git a/api/src/processing/match.js b/api/src/processing/match.js index ffb92c23f..63c2df98b 100644 --- a/api/src/processing/match.js +++ b/api/src/processing/match.js @@ -27,6 +27,7 @@ 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 threads from "./services/threads.js"; import facebook from "./services/facebook.js"; import bluesky from "./services/bluesky.js"; @@ -230,6 +231,15 @@ export default async function({ host, patternMatch, params }) { }); break; + case "threads": + r = await threads({ + ...patternMatch, + quality: params.videoQuality, + alwaysProxy: params.alwaysProxy, + dispatcher + }); + break; + case "facebook": r = await facebook({ ...patternMatch diff --git a/api/src/processing/service-config.js b/api/src/processing/service-config.js index 8d8bf4ac9..c48000d30 100644 --- a/api/src/processing/service-config.js +++ b/api/src/processing/service-config.js @@ -108,6 +108,10 @@ export const services = { "s/:id" ], }, + threads: { + patterns: [":user/post/:id"], + tld: "net", + }, tiktok: { patterns: [ ":user/video/:postId", diff --git a/api/src/processing/service-patterns.js b/api/src/processing/service-patterns.js index 2105a563c..723ab8a1c 100644 --- a/api/src/processing/service-patterns.js +++ b/api/src/processing/service-patterns.js @@ -38,6 +38,9 @@ export const testers = { "streamable": pattern => pattern.id?.length === 6, + "threads": pattern => + pattern.user?.length <= 33 && pattern.id?.length <= 32, + "tiktok": pattern => pattern.postId?.length <= 21 || pattern.id?.length <= 13, diff --git a/api/src/processing/services/threads.js b/api/src/processing/services/threads.js new file mode 100644 index 000000000..0cac4d485 --- /dev/null +++ b/api/src/processing/services/threads.js @@ -0,0 +1,121 @@ +import { createStream } from "../../stream/manage.js"; +import { getCookie, updateCookie } from "../cookie/manager.js"; + +const commonHeaders = { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.7", + "Cache-Control": "no-cache", + "Dnt": "1", + "Priority": "u=0, i", + "Sec-Ch-Ua": '"Not/A)Brand";v="8", "Chromium";v="126", "Brave";v="126"', + "Sec-Ch-Ua-Mobile": "?0", + "Sec-Ch-Ua-Model": '""', + "Sec-Ch-Ua-Platform": '"Windows"', + "Sec-Ch-Ua-Platform-Version": '"15.0.0"', + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "same-origin", + "Sec-Gpc": "1", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", +}; + +const DATA_REGEX = /