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: snapchat support #429

Merged
merged 19 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,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 | ✅ | ✅ | ✅ | ❌ | ❌ |
Expand All @@ -46,6 +47,7 @@ this list is not final and keeps expanding over time. if support for a service y
| instagram | supports reels, photos, and videos. lets you pick what to save from multi-media posts. |
| 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. |
| soundcloud | supports private links. |
| 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. |
Expand Down
10 changes: 10 additions & 0 deletions src/modules/processing/match.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

let freebind;

Expand Down Expand Up @@ -185,6 +186,15 @@ 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
});
break;
default:
return createResponse("error", {
t: loc(lang, 'ErrorUnsupported')
Expand Down
2 changes: 2 additions & 0 deletions src/modules/processing/matchActionDecider.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,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":
Expand Down Expand Up @@ -127,6 +128,7 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
case "tumblr":
case "pinterest":
case "streamable":
case "snapchat":
responseType = "redirect";
break;
}
Expand Down
82 changes: 82 additions & 0 deletions src/modules/processing/services/snapchat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { genericUserAgent } from "../../config.js";
import { getRedirectingURL } from "../../sub/utils.js";
import { extract, normalizeURL } from "../url.js";

const SPOTLIGHT_VIDEO_REGEX = /<link data-react-helmet="true" rel="preload" href="(https:\/\/cf-st\.sc-cdn\.net\/d\/[\w.?=]+&amp;uc=\d+)" as="video"\/>/;
const NEXT_DATA_REGEX = /<script id="__NEXT_DATA__" type="application\/json">({.+})<\/script><\/body><\/html>$/;

async function getSpotlight(id) {
const html = await fetch(`https://www.snapchat.com/spotlight/${id}`, {
headers: { 'User-Agent': genericUserAgent }
}).then((r) => r.text()).catch(() => null);
if (!html) return { error: 'ErrorCouldntFetch' };

const videoURL = html.match(SPOTLIGHT_VIDEO_REGEX)?.[1];
if (videoURL) return {
urls: videoURL,
filename: `snapchat_${id}.mp4`,
audioFilename: `snapchat_${id}_audio`
}
}

async function getStory(username, storyId) {
const html = await fetch(`https://www.snapchat.com/add/${username}${storyId ? `/${storyId}` : ''}`, {
headers: { 'User-Agent': genericUserAgent }
}).then((r) => r.text()).catch(() => null);
if (!html) return { error: 'ErrorCouldntFetch' };
Snazzah marked this conversation as resolved.
Show resolved Hide resolved

const nextDataString = html.match(NEXT_DATA_REGEX)?.[1];
if (nextDataString) {
const data = JSON.parse(nextDataString);
const storyIdParam = data.query.profileParams[1];

if (storyIdParam && data.props.pageProps.story) {
const story = data.props.pageProps.story.snapList.find((snap) => snap.snapId.value === storyIdParam);
if (story) {
if (story.snapMediaType === 0)
return {
urls: story.snapUrls.mediaUrl,
isPhoto: true
}

Snazzah marked this conversation as resolved.
Show resolved Hide resolved
return {
urls: story.snapUrls.mediaUrl,
filename: `snapchat_${storyId}.mp4`,
audioFilename: `snapchat_${storyId}_audio`
}
}
}

const defaultStory = data.props.pageProps.curatedHighlights[0];
if (defaultStory) {
return {
picker: defaultStory.snapList.map((snap) => ({
type: snap.snapMediaType === 0 ? 'photo' : 'video',
url: snap.snapUrls.mediaUrl,
thumb: snap.snapUrls.mediaPreviewUrl.value
}))
}
}
}
}

export default async function(obj) {
let params = obj;
if (obj.url.hostname === 't.snapchat.com' && obj.shortLink) {
const link = await getRedirectingURL(`https://t.snapchat.com/${obj.shortLink}`);
Snazzah marked this conversation as resolved.
Show resolved Hide resolved
if (!link || !link.startsWith('https://www.snapchat.com/')) return { error: 'ErrorCouldntFetch' };
Snazzah marked this conversation as resolved.
Show resolved Hide resolved
const extractResult = extract(normalizeURL(link));
if (!extractResult || extractResult.host !== 'snapchat') return { error: 'ErrorCouldntFetch' };
Snazzah marked this conversation as resolved.
Show resolved Hide resolved
params = extractResult.patternMatch;
}

if (params.spotlightId) {
const result = await getSpotlight(params.spotlightId);
if (result) return result;
} else if (params.username) {
const result = await getStory(params.username, params.storyId);
if (result) return result;
}

return { error: 'ErrorCouldntFetch' };
}
6 changes: 6 additions & 0 deletions src/modules/processing/servicesConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@
"alias": "dailymotion videos",
"patterns": ["video/:id"],
"enabled": true
},
"snapchat": {
"alias": "snapchat stories & spotlights",
"subdomains": ["t", "story"],
"patterns": [":shortLink", "spotlight/:spotlightId", "add/:username/:storyId", "u/:username/:storyId", "add/:username", "u/:username"],
"enabled": true
}
}
}
5 changes: 5 additions & 0 deletions src/modules/processing/servicesPatternTesters.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export const testers = {
(patternMatch.author?.length <= 255 && patternMatch.song?.length <= 255)
|| patternMatch.shortLink?.length <= 32,

"snapchat": (patternMatch) =>
(patternMatch.username?.length <= 32 && (!patternMatch.storyId || patternMatch.storyId?.length <= 255))
|| patternMatch.spotlightId?.length <= 255
|| patternMatch.shortLink?.length <= 16,

"streamable": (patternMatch) =>
patternMatch.id?.length === 6,

Expand Down
6 changes: 6 additions & 0 deletions src/modules/sub/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,9 @@ export function cleanHTML(html) {
clean = clean.replace(/\n/g, '');
return clean
}
export function getRedirectingURL(url) {
return fetch(url, { redirect: 'manual' }).then((r) => {
if ([301, 302, 303].includes(r.status) && r.headers.has('location'))
return r.headers.get('location');
}).catch(() => null);
}
27 changes: 26 additions & 1 deletion src/test/tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -1123,5 +1123,30 @@
"code": 200,
"status": "stream"
}
}],
"snapchat": [{
"name": "spotlight",
"url": "https://www.snapchat.com/spotlight/W7_EDlXWTBiXAEEniNoMPwAAYdWxucG9pZmNqAY46j0a5AY46j0YbAAAAAQ",
"params": {},
"expected": {
"code": 200,
"status": "redirect"
}
}, {
"name": "shortlinked spotlight",
"url": "https://t.snapchat.com/4ZsiBLDi",
"params": {},
"expected": {
"code": 200,
"status": "redirect"
}
}, {
"name": "story",
"url": "https://www.snapchat.com/add/bazerkmakane",
"params": {},
"expected": {
"code": 200,
"status": "picker"
}
}]
}
}