├── requirements.txt ├── LICENSE ├── README.md └── snapstory.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Siddharth Dushantha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SnapStory 2 | > A public SnapChat story downloader 3 | 4 | [![demo](https://user-images.githubusercontent.com/27065646/43774662-2caac5ee-9a4a-11e8-8188-45532bc5ab0b.png)](https://www.youtube.com/watch?v=35RC6NvCo8U) 5 | 6 | ## Installation 7 | 8 | ```bash 9 | # clone the repo 10 | $ git clone https://github.com/sdushantha/SnapStory.git 11 | 12 | # install the requirements 13 | $ pip3 install -r requirements.txt 14 | ``` 15 | 16 | ## Usage 17 | 18 | ``` 19 | usage: snapstory.py [-h] [-s] username 20 | 21 | A public SnapChat story downloader 22 | 23 | positional arguments: 24 | username The username or id of a public story 25 | 26 | optional arguments: 27 | -h, --help show this help message and exit 28 | -s, --single Download a single story 29 | ``` 30 | 31 | For this to work, you need to find a user with a **public** story which has an [emoji next to their username](https://user-images.githubusercontent.com/27065646/43775494-3380bdc6-9a4d-11e8-8d84-0aa9ee6ba275.jpg) or some event like *International Emoji Day*. The user **cant** be a company like *New York Times* or *BuzzFeed*. 32 | 33 | ## Related 34 | [SnapCode](https://github.com/sdushantha/SnapCode) - A simple command-line tool to download SnapChat codes. 35 | 36 | ## License 37 | MIT License 38 | 39 | Copyright (c) 2018 Siddharth Dushantha 40 | -------------------------------------------------------------------------------- /snapstory.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import urllib.request 3 | import sys 4 | import argparse 5 | import os 6 | from time import sleep 7 | 8 | 9 | 10 | def valid_story(username, singleStory=False): 11 | """ 12 | Checks if the given username or id 13 | has a public story 14 | """ 15 | if singleStory: 16 | url = "https://story.snapchat.com/s/s:{}" 17 | else: 18 | url = "https://story.snapchat.com/s/{}" 19 | 20 | url = url.format(username) 21 | r = requests.get(url) 22 | 23 | if r.status_code == 200: 24 | return True 25 | 26 | else: 27 | return False 28 | 29 | 30 | def download(username, singleStory=False): 31 | 32 | if singleStory: 33 | api = "https://storysharing.snapchat.com/v1/fetch/s:{}?request_origin=ORIGIN_WEB_PLAYER" 34 | else: 35 | api = "https://storysharing.snapchat.com/v1/fetch/{}?request_origin=ORIGIN_WEB_PLAYER" 36 | 37 | url = api.format(username) 38 | response = requests.get(url) 39 | 40 | data = response.json() 41 | print("\033[92m[+] Fetched data\033[0m") 42 | 43 | # Using dict.get() will return None when there are no snaps instead of throwing a KeyError 44 | story_arr = data.get("story").get("snaps") 45 | 46 | if story_arr: 47 | story_type = data.get("story").get("metadata").get("storyType") 48 | 49 | title = data.get("story").get("metadata").get("title") 50 | 51 | # TYPE_PUBLIC_USER_STORY = Story from a user 52 | # There are many different story types. 53 | if story_type == "TYPE_PUBLIC_USER_STORY": 54 | 55 | username = data["story"]["id"] 56 | 57 | print("\33[93m[!] Downloading from", 58 | title, str(data["story"]["metadata"]["emoji"]), 59 | "(\033[91m{}\33[93m)\33[0m".format(username)) 60 | else: 61 | print("\33[93m[!] Downloading from", title) 62 | 63 | # If dont do this the folder will be have a long 64 | # unidentifiable name. So we are using the title 65 | # as the "username" 66 | username = title.replace(" ", "_") 67 | 68 | # Making a directory with given username 69 | # to store the images of that user 70 | os.makedirs(username, exist_ok=True) 71 | 72 | for index, media in enumerate(story_arr): 73 | try: 74 | file_url = media["media"]["mediaUrl"] 75 | 76 | # We cant download images anymore. Its not in the JSON 77 | # response. But I just commented it out incase it comes 78 | # back. 79 | #if media["media"]["type"] == "IMAGE": 80 | #file_ext = ".jpg" 81 | #filetype = "IMAGE" 82 | 83 | if media["media"]["type"] == "VIDEO": 84 | file_ext = ".mp4" 85 | 86 | # This is name of the dir where these types 87 | # of files will be stored 88 | filetype = "VIDEO" 89 | 90 | elif media["media"]["type"] == "VIDEO_NO_SOUND": 91 | file_ext = ".mp4" 92 | filetype = "VIDEO_NO_SOUND" 93 | 94 | 95 | dir_name = username+"/"+filetype+"/" 96 | 97 | os.makedirs(dir_name, exist_ok=True) 98 | 99 | path = dir_name+str(media["id"])+file_ext 100 | 101 | if not os.path.exists(path): 102 | 103 | urllib.request.urlretrieve(file_url, path) 104 | print("\033[92m[+] Downloaded file {:d} of {:d}:\033[0m {:s}".format(index+1, len(story_arr), path.replace(dir_name, ""))) 105 | 106 | # We need a small pause or else we will get a ConnectionResetError 107 | sleep(0.3) 108 | 109 | else: 110 | print("\033[91m[!] File {:d} of {:d} already exists:\033[0m {:s}".format(index+1, len(story_arr), path.replace(dir_name, ""))) 111 | 112 | except KeyError as e: 113 | print("\033[91m[-] Could not get file data: \033[0m{:s}".format(str(e))) 114 | 115 | except KeyboardInterrupt: 116 | print("\033[91m[!] Download cancelled\033[0m") 117 | break 118 | 119 | else: 120 | print("\033[91m[!] No stories available\033[0m") 121 | 122 | 123 | def main(): 124 | parser = argparse.ArgumentParser(description = "A public SnapChat story downloader") 125 | parser.add_argument('username', action="store", 126 | help="The username or id of a public story") 127 | 128 | parser.add_argument('-s', '--single', action="store_true", 129 | help="Download a single story") 130 | 131 | args = parser.parse_args() 132 | 133 | if len(sys.argv) == 1: 134 | parser.print_help() 135 | 136 | elif args.single: 137 | if valid_story(args.username, singleStory=True): 138 | print("\033[92m[+] Valid story\033[0m") 139 | download(args.username, singleStory=True) 140 | 141 | else: 142 | print("\033[91m[-] Invalid story\033[0m") 143 | 144 | 145 | else: 146 | if valid_story(args.username): 147 | print("\033[92m[+] Valid story\033[0m") 148 | download(args.username) 149 | 150 | else: 151 | print("\033[91m[-] Invalid story\033[0m") 152 | 153 | if __name__=="__main__": 154 | main() 155 | --------------------------------------------------------------------------------