diff --git a/src/modules/processing/match.js b/src/modules/processing/match.js index 6f715ef7..1a46d0cd 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 snapchat from "./services/snapchat.js"; export default async function(host, patternMatch, url, lang, obj) { assert(url instanceof URL); @@ -158,6 +159,15 @@ export default async function(host, patternMatch, url, 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 apiJSON(0, { t: errorUnsupported(lang) }); } diff --git a/src/modules/processing/matchActionDecider.js b/src/modules/processing/matchActionDecider.js index 2d8840a3..622f2bbc 100644 --- a/src/modules/processing/matchActionDecider.js +++ b/src/modules/processing/matchActionDecider.js @@ -120,6 +120,7 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di case "tumblr": case "pinterest": case "streamable": + case "snapchat": responseType = 1; break; } diff --git a/src/modules/processing/services/snapchat.js b/src/modules/processing/services/snapchat.js new file mode 100644 index 00000000..eadbd2ae --- /dev/null +++ b/src/modules/processing/services/snapchat.js @@ -0,0 +1,56 @@ +import { genericUserAgent } from "../../config.js"; + +const SPOTLIGHT_VIDEO_REGEX = //; + +export default async function(obj) { + let link; + if (obj.url.hostname === 't.snapchat.com' && obj.shortLink) { + link = await fetch(`https://t.snapchat.com/${obj.shortLink}`, { redirect: "manual" }).then((r) => { + if (r.status === 303 && r.headers.get("location").startsWith("https://www.snapchat.com/")) { + return r.headers.get("location").split('?', 1)[0] + } + }).catch(() => {}); + } + + if (!link && obj.username && obj.storyId) { + link = `https://www.snapchat.com/add/${obj.username}/${obj.storyId}` + } else if (!link && obj.spotlightId) { + link = `https://www.snapchat.com/spotlight/${obj.spotlightId}` + } else if (link?.startsWith('https://www.snapchat.com/download')) { + return { error: 'ErrorCouldntFetch' }; + } + + const path = new URL(link).pathname; + + if (path.startsWith('/spotlight/')) { + const html = await fetch(link, { + headers: { "user-agent": genericUserAgent } + }).then((r) => { return r.text() }).catch(() => { return false }); + if (!html) return { error: 'ErrorCouldntFetch' }; + + const id = path.split('/')[2]; + const videoURL = html.match(SPOTLIGHT_VIDEO_REGEX)?.[1]; + if (videoURL) return { + urls: videoURL, + filename: `snapchat_${id}.mp4`, + audioFilename: `snapchat_${id}_audio` + } + } else if (path.startsWith('/add/')) { + const html = await fetch(link, { + headers: { "user-agent": genericUserAgent } + }).then((r) => { return r.text() }).catch(() => { return false }); + if (!html) return { error: 'ErrorCouldntFetch' }; + + const id = path.split('/')[3]; + const storyVideoRegex = new RegExp(`"snapId":{"value":"${id}"},"snapMediaType":1,"snapUrls":{"mediaUrl":"(https:\\/\\/bolt-gcdn\\.sc-cdn\\.net\\/3\/[^"]+)","mediaPreviewUrl"`); + const videoURL = html.match(storyVideoRegex)?.[1]; + if (videoURL) return { + urls: videoURL, + filename: `snapchat_${id}.mp4`, + audioFilename: `snapchat_${id}_audio` + } + } + + + return { error: 'ErrorCouldntFetch' }; +} diff --git a/src/modules/processing/servicesConfig.json b/src/modules/processing/servicesConfig.json index 633fa2a6..afaee500 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -116,6 +116,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"], + "enabled": true } } } diff --git a/src/modules/processing/servicesPatternTesters.js b/src/modules/processing/servicesPatternTesters.js index f4dee15b..3d75c4f0 100644 --- a/src/modules/processing/servicesPatternTesters.js +++ b/src/modules/processing/servicesPatternTesters.js @@ -25,6 +25,11 @@ export const testers = { (patternMatch.author?.length <= 255 && patternMatch.song?.length <= 255) || patternMatch.shortLink?.length <= 32, + "snapchat": (patternMatch) => + (patternMatch.username?.length <= 32 && patternMatch.storyId?.length <= 255) + || patternMatch.spotlightId?.length <= 255 + || patternMatch.shortLink?.length <= 16, + "streamable": (patternMatch) => patternMatch.id?.length === 6, diff --git a/src/test/tests.json b/src/test/tests.json index bd8b33c5..2f654552 100644 --- a/src/test/tests.json +++ b/src/test/tests.json @@ -1115,5 +1115,22 @@ "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" + } }] -} +} \ No newline at end of file