├── USAGE.rst
├── docs
├── readme.rst
├── authors.rst
├── history.rst
├── contributing.rst
├── modules.rst
├── usage.rst
├── index.rst
├── snapchat_dl.rst
├── Makefile
├── make.bat
├── installation.rst
└── conf.py
├── snapchat_dl
├── version.py
├── __init__.py
├── downloader.py
├── app.py
├── cli.py
├── utils.py
└── snapchat_dl.py
├── .gitattributes
├── requirements.txt
├── tests
├── __init__.py
├── mock_data
│ ├── 23
│ │ └── .gitignore
│ ├── batch_file.txt
│ ├── user1
│ │ └── .gitignore
│ ├── user.1name
│ │ └── .gitignore
│ ├── invalidusername.json
│ ├── invalidusername-nostories.html
│ ├── api-error.html
│ └── invalidusername.html
├── test_downlaoder.py
├── test_utils.py
└── test_snapchat_dl.py
├── AUTHORS.rst
├── requirements_dev.txt
├── MANIFEST.in
├── setup.cfg
├── .editorconfig
├── .github
├── ISSUE_TEMPLATE.md
└── workflows
│ ├── continuous-integration-pip.yml
│ └── continuous-integration-publish.yml
├── .coveragerc
├── .pre-commit-config.yaml
├── LICENSE
├── setup.py
├── .gitignore
├── Makefile
├── CONTRIBUTING.md
└── README.md
/USAGE.rst:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/readme.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../README.rst
2 |
--------------------------------------------------------------------------------
/snapchat_dl/version.py:
--------------------------------------------------------------------------------
1 | __version__ = "2.0.1"
2 |
--------------------------------------------------------------------------------
/docs/authors.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../AUTHORS.rst
2 |
--------------------------------------------------------------------------------
/docs/history.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../HISTORY.rst
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | tests/mock_data/* linguist-vendored
2 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | loguru
2 | pyperclip
3 | requests
4 |
--------------------------------------------------------------------------------
/docs/contributing.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../CONTRIBUTING.rst
2 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | """Unit test package for snapchat_dl."""
2 |
--------------------------------------------------------------------------------
/tests/mock_data/batch_file.txt:
--------------------------------------------------------------------------------
1 | username1
2 | user.name2
3 | user_name
4 |
--------------------------------------------------------------------------------
/tests/mock_data/23/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore everything in this directory
2 | *
3 | # Except this file
4 | !.gitignore
5 |
--------------------------------------------------------------------------------
/docs/modules.rst:
--------------------------------------------------------------------------------
1 | snapchat_dl
2 | ===========
3 |
4 | .. toctree::
5 | :maxdepth: 4
6 |
7 | snapchat_dl
8 |
--------------------------------------------------------------------------------
/tests/mock_data/user1/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore everything in this directory
2 | *
3 | # Except this file
4 | !.gitignore
5 |
--------------------------------------------------------------------------------
/tests/mock_data/user.1name/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore everything in this directory
2 | *
3 | # Except this file
4 | !.gitignore
5 |
--------------------------------------------------------------------------------
/docs/usage.rst:
--------------------------------------------------------------------------------
1 | =====
2 | Usage
3 | =====
4 |
5 | To use Snapchat Downloader in a project::
6 |
7 | import snapchat_dl
8 |
--------------------------------------------------------------------------------
/AUTHORS.rst:
--------------------------------------------------------------------------------
1 | =======
2 | Credits
3 | =======
4 |
5 | Development Lead
6 | ----------------
7 |
8 | * Aakash Gajjar
9 |
10 | Contributors
11 | ------------
12 |
13 | None yet. Why not be the first?
14 |
--------------------------------------------------------------------------------
/snapchat_dl/__init__.py:
--------------------------------------------------------------------------------
1 | """Top-level package for Snapchat Downloader."""
2 |
3 | __author__ = """Aakash Gajjar"""
4 | __email__ = "skyqutip@gmail.com"
5 |
6 | from snapchat_dl.snapchat_dl import SnapchatDL
7 |
8 | __all__ = ["SnapchatDL"]
9 |
--------------------------------------------------------------------------------
/requirements_dev.txt:
--------------------------------------------------------------------------------
1 | bandit==1.7.9
2 | black==24.3.0
3 | blacken-docs==1.18.0
4 | bump2version==1.0.1
5 | coverage==7.5.4
6 | pre-commit==3.7.1
7 | pytest==8.2.2
8 | reorder_python_imports==3.13.0
9 | Sphinx==7.3.7
10 | watchdog==4.0.1
11 | wheel==0.43.0
12 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include AUTHORS.rst
2 | include CONTRIBUTING.rst
3 | include HISTORY.rst
4 | include LICENSE
5 | include README.rst
6 | include USAGE.rst
7 | include requirements.txt
8 |
9 | recursive-include tests *
10 | recursive-exclude * __pycache__
11 | recursive-exclude * *.py[co]
12 |
13 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif
14 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bumpversion]
2 | current_version = 0.2.5
3 | commit = True
4 | tag = True
5 |
6 | [bumpversion:file:snapchat_dl/__init__.py]
7 | search = __version__ = "{current_version}"
8 | replace = __version__ = "{new_version}"
9 |
10 | [bdist_wheel]
11 | universal = 1
12 |
13 | [flake8]
14 | exclude = docs
15 | max-line-length = 99
16 | extend-ignore =
17 | E203,
18 |
19 | [aliases]
20 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | indent_style = space
7 | indent_size = 4
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 | charset = utf-8
11 | end_of_line = lf
12 |
13 | [*.bat]
14 | indent_style = tab
15 | end_of_line = crlf
16 |
17 | [LICENSE]
18 | insert_final_newline = false
19 |
20 | [Makefile]
21 | indent_style = tab
22 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | Welcome to Snapchat Downloader's documentation!
2 | ===============================================
3 |
4 | .. toctree::
5 | :maxdepth: 2
6 | :caption: Contents:
7 |
8 | readme
9 | installation
10 | usage
11 | modules
12 | contributing
13 | authors
14 | history
15 |
16 | Indices and tables
17 | ==================
18 | * :ref:`genindex`
19 | * :ref:`modindex`
20 | * :ref:`search`
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | * Snapchat Downloader version:
2 | * Python version:
3 | * Operating System:
4 |
5 | ### Description
6 |
7 | Describe what you were trying to get done.
8 | Tell us what happened, what went wrong, and what you expected to happen.
9 |
10 | ### What I Did
11 |
12 | ```
13 | Paste the command(s) you ran and the output.
14 | If there was a crash, please include the traceback here.
15 | ```
16 |
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [report]
2 | exclude_lines =
3 | except FileExistsError as e:
4 | except KeyboardInterrupt:
5 | except requests.exceptions.ConnectTimeout:
6 | except requests.exceptions.RequestException as e:
7 | if self.quiet is False:
8 | raise FileExistsError
9 | raise NoStoriesAvailable
10 | return response
11 |
12 |
13 | [run]
14 | omit =
15 | snapchat_dl/app.py
16 | snapchat_dl/cli.py
17 | snapchat_dl/version.py
18 | setup.py
19 | .eggs/*
20 | venv/*
21 |
--------------------------------------------------------------------------------
/docs/snapchat_dl.rst:
--------------------------------------------------------------------------------
1 | snapchat\_dl package
2 | ====================
3 |
4 | Submodules
5 | ----------
6 |
7 | snapchat\_dl.cli module
8 | -----------------------
9 |
10 | .. automodule:: snapchat_dl.cli
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | snapchat\_dl.snapchat\_dl module
16 | --------------------------------
17 |
18 | .. automodule:: snapchat_dl.snapchat_dl
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 |
24 | Module contents
25 | ---------------
26 |
27 | .. automodule:: snapchat_dl
28 | :members:
29 | :undoc-members:
30 | :show-inheritance:
31 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = python -msphinx
7 | SPHINXPROJ = snapchat_dl
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | # See https://pre-commit.com for more information
2 | # See https://pre-commit.com/hooks.html for more hooks
3 | repos:
4 | - repo: https://github.com/pre-commit/pre-commit-hooks
5 | rev: v4.6.0
6 | hooks:
7 | - id: check-added-large-files
8 | - id: check-yaml
9 | - id: detect-private-key
10 | - id: end-of-file-fixer
11 | - id: requirements-txt-fixer
12 | - id: trailing-whitespace
13 | - repo: https://github.com/psf/black
14 | rev: 24.4.2
15 | hooks:
16 | - id: black
17 | - repo: https://github.com/asottile/blacken-docs
18 | rev: 1.18.0
19 | hooks:
20 | - id: blacken-docs
21 | additional_dependencies: [black==24.4.2]
22 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=python -msphinx
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 | set SPHINXPROJ=snapchat_dl
13 |
14 | if "%1" == "" goto help
15 |
16 | %SPHINXBUILD% >NUL 2>NUL
17 | if errorlevel 9009 (
18 | echo.
19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed,
20 | echo.then set the SPHINXBUILD environment variable to point to the full
21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the
22 | echo.Sphinx directory to PATH.
23 | echo.
24 | echo.If you don't have Sphinx installed, grab it from
25 | echo.http://sphinx-doc.org/
26 | exit /b 1
27 | )
28 |
29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
30 | goto end
31 |
32 | :help
33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
34 |
35 | :end
36 | popd
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020, Aakash Gajjar
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 |
--------------------------------------------------------------------------------
/.github/workflows/continuous-integration-pip.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | python-version: [3.11]
11 |
12 | steps:
13 | - uses: actions/cache@v3
14 | with:
15 | path: ~/.cache/pip
16 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
17 | restore-keys: |
18 | ${{ runner.os }}-pip-
19 | - uses: actions/checkout@v4
20 | - name: Set up Python ${{ matrix.python-version }}
21 | uses: actions/setup-python@v5
22 | with:
23 | python-version: ${{ matrix.python-version }}
24 | - name: Install dependencies
25 | run: |
26 | python -m pip install --upgrade pip
27 | pip install pytest
28 | pip install pytest-cov
29 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
30 | - name: Test with pytest
31 | run: |
32 | pytest --cov=./ --cov-report=xml
33 | - name: Upload coverage to Codecov
34 | uses: codecov/codecov-action@v4
35 |
--------------------------------------------------------------------------------
/docs/installation.rst:
--------------------------------------------------------------------------------
1 | .. highlight:: shell
2 |
3 | ============
4 | Installation
5 | ============
6 |
7 |
8 | Stable release
9 | --------------
10 |
11 | To install Snapchat Downloader, run this command in your terminal:
12 |
13 | .. code-block:: console
14 |
15 | $ pip install snapchat-dl
16 |
17 | This is the preferred method to install Snapchat Downloader, as it will always install the most recent stable release.
18 |
19 | If you don't have `pip`_ installed, this `Python installation guide`_ can guide
20 | you through the process.
21 |
22 | .. _pip: https://pip.pypa.io
23 | .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/
24 |
25 |
26 | From sources
27 | ------------
28 |
29 | The sources for Snapchat Downloader can be downloaded from the `Github repo`_.
30 |
31 | You can either clone the public repository:
32 |
33 | .. code-block:: console
34 |
35 | $ git clone git://github.com/skyme5/snapchat-dl
36 |
37 | Or download the `tarball`_:
38 |
39 | .. code-block:: console
40 |
41 | $ curl -OJL https://github.com/skyme5/snapchat-dl/tarball/master
42 |
43 | Once you have a copy of the source, you can install it with:
44 |
45 | .. code-block:: console
46 |
47 | $ python setup.py install
48 |
49 |
50 | .. _Github repo: https://github.com/skyme5/snapchat-dl
51 | .. _tarball: https://github.com/skyme5/snapchat-dl/tarball/master
52 |
--------------------------------------------------------------------------------
/tests/test_downlaoder.py:
--------------------------------------------------------------------------------
1 | """Tests for `snapchat_dl` package."""
2 |
3 | import os
4 | import shutil
5 | import unittest
6 |
7 | from requests.exceptions import HTTPError
8 |
9 | from snapchat_dl.downloader import download_url
10 |
11 |
12 | def teardown_module(module):
13 | shutil.rmtree(".test-data")
14 |
15 |
16 | class Test_downloader(unittest.TestCase):
17 | """Tests for `snapchat_dl.downloader.download_url` package."""
18 |
19 | def setUp(self):
20 | """Set up test fixtures."""
21 | self.test_url = (
22 | "https://sample-videos.com/video321/mp4/240/big_buck_bunny_240p_1mb.mp4"
23 | )
24 | self.test_url404 = "https://google.com/error.html"
25 |
26 | def test_download_url(self):
27 | """Test snapchat_dl download_url."""
28 | download_url(self.test_url, ".test-data/test_dl_23.mp4", sleep_interval=0)
29 |
30 | def test_empty_download(self):
31 | """Test snapchat_dl download_url."""
32 | open(".test-data/test_dl_23.mp4", "w").close()
33 | download_url(self.test_url, ".test-data/test_dl_23.mp4", sleep_interval=0)
34 |
35 | def test_download_url_raise(self):
36 | """Test snapchat_dl download_url with invalid url."""
37 | with self.assertRaises(HTTPError):
38 | download_url(
39 | self.test_url404, ".test-data/test_dl_23.mp4", sleep_interval=0
40 | )
41 |
--------------------------------------------------------------------------------
/.github/workflows/continuous-integration-publish.yml:
--------------------------------------------------------------------------------
1 | name: publish
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*.*.*"
7 |
8 | jobs:
9 | build:
10 | # Specifying a GitHub environment is optional, but strongly encouraged
11 | environment: publish
12 | permissions:
13 | # IMPORTANT: this permission is mandatory for trusted publishing
14 | id-token: write
15 | runs-on: ubuntu-latest
16 | strategy:
17 | matrix:
18 | python-version: [3.8]
19 |
20 | steps:
21 | - uses: actions/cache@v3
22 | with:
23 | path: ~/.cache/pip
24 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
25 | restore-keys: |
26 | ${{ runner.os }}-pip-
27 | - uses: actions/checkout@v4
28 | - name: Set up Python ${{ matrix.python-version }}
29 | uses: actions/setup-python@v5
30 | with:
31 | python-version: ${{ matrix.python-version }}
32 | - name: Install dependencies
33 | run: |
34 | python -m pip install --upgrade pip
35 | pip install setuptools
36 | pip install wheel
37 | pip install twine
38 | - name: Build
39 | run: |
40 | python setup.py sdist bdist_wheel
41 | - name: Publish package distributions to PyPI
42 | uses: pypa/gh-action-pypi-publish@release/v1
43 |
--------------------------------------------------------------------------------
/snapchat_dl/downloader.py:
--------------------------------------------------------------------------------
1 | """File Downlaoder for snapchat_dl."""
2 | import os
3 | import time
4 |
5 | import requests
6 | from loguru import logger
7 |
8 |
9 | def download_url(url: str, dest: str, sleep_interval: int):
10 | """Download URL to destionation path.
11 |
12 | Args:
13 | url (str): url to download
14 | dest (str): absolute path to destination
15 |
16 | Raises:
17 | response.raise_for_status: if response is 4** or 50*
18 | FileExistsError: if file is already downloaded
19 | """
20 | if len(os.path.dirname(dest)) > 0:
21 | os.makedirs(os.path.dirname(dest), exist_ok=True)
22 |
23 | """Rate limiting."""
24 | time.sleep(sleep_interval)
25 |
26 | try:
27 | response = requests.get(url, stream=True, timeout=10)
28 | except requests.exceptions.ConnectTimeout:
29 | response = requests.get(url, stream=True, timeout=10)
30 |
31 | if response.status_code != requests.codes.get("ok"):
32 | raise response.raise_for_status()
33 |
34 | if os.path.isfile(dest) and os.path.getsize(dest) == response.headers.get(
35 | "content-length"
36 | ):
37 | raise FileExistsError
38 |
39 | if os.path.isfile(dest) and os.path.getsize(dest) == 0:
40 | os.remove(dest)
41 | try:
42 | with open(dest, "xb") as handle:
43 | try:
44 | for data in response.iter_content(chunk_size=4194304):
45 | handle.write(data)
46 | handle.close()
47 | except requests.exceptions.RequestException as e:
48 | logger.error(e)
49 | except FileExistsError as e:
50 | pass
51 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """The setup script."""
3 | from setuptools import find_packages
4 | from setuptools import setup
5 |
6 |
7 | version = {}
8 | with open("snapchat_dl/version.py") as fp:
9 | exec(fp.read(), version)
10 |
11 | with open("README.md") as readme_file:
12 | readme = readme_file.read()
13 |
14 | requirements = list()
15 |
16 | with open("requirements.txt", "r") as file:
17 | requirements = [r for r in file.readlines() if len(r) > 0]
18 |
19 | test_requirements = ["pytest"].extend(requirements)
20 |
21 | setup(
22 | name="snapchat-dl",
23 | version=version["__version__"],
24 | description="Snapchat Public Stories Downloader.",
25 | long_description=readme,
26 | long_description_content_type="text/markdown",
27 | url="https://github.com/skyme5/snapchat-dl",
28 | author="Aakash Gajjar",
29 | author_email="skyqutip@gmail.com",
30 | entry_points={
31 | "console_scripts": [
32 | "snapchat-dl=snapchat_dl.app:main",
33 | ],
34 | },
35 | include_package_data=True,
36 | install_requires=requirements,
37 | test_suite="tests",
38 | tests_require=test_requirements,
39 | python_requires=">=3.5",
40 | keywords="snapchat-dl",
41 | license="MIT license",
42 | packages=find_packages(include=["snapchat_dl", "snapchat_dl.*"]),
43 | zip_safe=False,
44 | classifiers=[
45 | "Development Status :: 5 - Production/Stable",
46 | "Environment :: Console",
47 | "Intended Audience :: Developers",
48 | "Intended Audience :: End Users/Desktop",
49 | "License :: OSI Approved :: MIT License",
50 | "Natural Language :: English",
51 | "Programming Language :: Python :: 3",
52 | ],
53 | )
54 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 | tmp/
50 |
51 | # Translations
52 | *.mo
53 | *.pot
54 |
55 | # Django stuff:
56 | *.log
57 | local_settings.py
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # dotenv
85 | .env
86 |
87 | # virtualenv
88 | .venv
89 | venv/
90 | ENV/
91 |
92 | # Spyder project settings
93 | .spyderproject
94 | .spyproject
95 |
96 | # Rope project settings
97 | .ropeproject
98 |
99 | # mkdocs documentation
100 | /site
101 |
102 | # mypy
103 | .mypy_cache/
104 |
105 | # IDE settings
106 | .vscode/
107 | cmd.txt
108 |
109 | .test-data/
110 | *.jpg
111 | *.png
112 | *.mp4
113 | *.jpeg
114 |
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Tests for `snapchat_dl` package."""
3 | import unittest
4 | from argparse import Namespace
5 |
6 | from snapchat_dl.utils import search_usernames
7 | from snapchat_dl.utils import strf_time
8 | from snapchat_dl.utils import use_batch_file
9 | from snapchat_dl.utils import use_prefix_dir
10 | from snapchat_dl.utils import valid_username
11 |
12 |
13 | class Test_utils(unittest.TestCase):
14 | """Tests for `snapchat_dl` package."""
15 |
16 | def test_valid_username(self):
17 | """Test for invalid username."""
18 | self.assertFalse(valid_username("2323 2323"))
19 |
20 | def test_strf_time(self):
21 | """Test strf_time."""
22 | self.assertEqual(
23 | strf_time(978307200, "%Y-%m-%dT%H-%M-%S"),
24 | "2001-01-01T00-00-00",
25 | )
26 |
27 | def test_search_usernames(self):
28 | """Test usernames search in string."""
29 | string = """
30 | https://story.snapchat.com/s/in#invalidusername
31 | https://story.snapchat.com/s/username1
32 | https://story.snapchat.com/s/user.name2
33 | https://story.snapchat.com/s/user_name
34 | https://story.snapchat.com/@user_name
35 | """
36 | usernames = ["user.name2", "user_name", "username1"]
37 | self.assertListEqual(search_usernames(string), usernames)
38 |
39 | def test_use_batch_file(self):
40 | args = Namespace(batch_file="tests/mock_data/batch_file.txt")
41 | usernames = ["username1", "user.name2", "user_name"]
42 | self.assertListEqual(use_batch_file(args), usernames)
43 |
44 | def test_use_batch_file_err(self):
45 | args = Namespace(batch_file="tests/mock_data/batch_file_err.txt")
46 | with self.assertRaises(Exception):
47 | use_batch_file(args)
48 |
49 | def test_use_prefix_dir(self):
50 | args = Namespace(scan_prefix=True, save_prefix="tests/mock_data", quiet=False)
51 | usernames = ["user.1name", "user1"]
52 | self.assertListEqual(use_prefix_dir(args), usernames)
53 |
--------------------------------------------------------------------------------
/tests/mock_data/invalidusername.json:
--------------------------------------------------------------------------------
1 | {
2 | "story": {
3 | "id": "invalidusername",
4 | "metadata": {
5 | "storyType": "TYPE_PUBLIC_USER_STORY",
6 | "title": "invalidusername",
7 | "emoji": "⭐",
8 | "canonicalUrlSuffix": "story/invalidusername/invalidusername"
9 | },
10 | "snaps": [
11 | {
12 | "id": "1PgHW1XZSgWteDhyTiIFbgAAgcnlmYnB0ZWpiAXxms2FCAXxms17GAAAAAA",
13 | "media": {
14 | "type": "VIDEO",
15 | "mediaUrl": "https://sample-videos.com/video321/mp4/240/big_buck_bunny_240p_1mb.mp4",
16 | "mediaStreamingUrl": "https://sample-videos.com/video321/mp4/240/big_buck_bunny_240p_1mb.mp4",
17 | "mediaPreviewUrl": "https://sample-videos.com/video321/mp4/240/big_buck_bunny_240p_1mb.mp4"
18 | },
19 | "overlayImage": {
20 | "mediaUrl": "https://sample-videos.com/video321/mp4/240/big_buck_bunny_240p_1mb.mp4",
21 | "mediaStreamingUrl": "https://sample-videos.com/video321/mp4/240/big_buck_bunny_240p_1mb.mp4"
22 | },
23 | "captureTimeSecs": "1633810603"
24 | },
25 | {
26 | "id": "1PgHW1XZSgWteDhyTiIFbgAAgZWZqZ2t2ZGN4AXxpJwZoAXxpJwRfAAAAAA",
27 | "media": {
28 | "type": "IMAGE",
29 | "mediaUrl": "https://sample-videos.com/video321/mp4/240/big_buck_bunny_240p_1mb.mp4"
30 | },
31 | "captureTimeSecs": "1633851737"
32 | },
33 | {
34 | "id": "1PgHW1XZSgWteDhyTiIFbgAAgdGlyeXpodGx4AXxpR4jsAXxpR4XzAAAAAA",
35 | "media": {
36 | "type": "IMAGE",
37 | "mediaUrl": "https://sample-videos.com/video321/mp4/240/big_buck_bunny_240p_1mb.mp4"
38 | },
39 | "captureTimeSecs": "1633853867"
40 | },
41 | {
42 | "id": "1PgHW1XZSgWteDhyTiIFbgAAgdnJlc2hjeWd5AXxqhQjjAXxqhQaeAAAAAA",
43 | "media": {
44 | "type": "IMAGE",
45 | "mediaUrl": "https://sample-videos.com/video321/mp4/240/big_buck_bunny_240p_1mb.mp4"
46 | },
47 | "captureTimeSecs": "1633874675"
48 | }
49 | ]
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: clean clean-test clean-pyc clean-build docs help
2 | .DEFAULT_GOAL := help
3 |
4 | define BROWSER_PYSCRIPT
5 | import os, webbrowser, sys
6 |
7 | from urllib.request import pathname2url
8 |
9 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1])))
10 | endef
11 | export BROWSER_PYSCRIPT
12 |
13 | define PRINT_HELP_PYSCRIPT
14 | import re, sys
15 |
16 | for line in sys.stdin:
17 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line)
18 | if match:
19 | target, help = match.groups()
20 | print("%-20s %s" % (target, help))
21 | endef
22 | export PRINT_HELP_PYSCRIPT
23 |
24 | BROWSER := python -c "$$BROWSER_PYSCRIPT"
25 |
26 | help:
27 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
28 |
29 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
30 |
31 | clean-build: ## remove build artifacts
32 | rm -fr build/
33 | rm -fr dist/
34 | rm -fr .eggs/
35 | find . -name '*.egg-info' -exec rm -fr {} +
36 | find . -name '*.egg' -exec rm -rf {} +
37 |
38 | clean-pyc: ## remove Python file artifacts
39 | find . -name '*.pyc' -exec rm -f {} +
40 | find . -name '*.pyo' -exec rm -f {} +
41 | find . -name '*~' -exec rm -f {} +
42 | find . -name '__pycache__' -exec rm -fr {} +
43 |
44 | clean-test: ## remove test and coverage artifacts
45 | rm -fr .tox/
46 | rm -f .coverage
47 | rm -fr htmlcov/
48 | rm -fr .pytest_cache
49 | rm -fr .test-data
50 |
51 | lint: ## check style with flake8
52 | black snapchat_dl tests
53 |
54 | test: clean-test ## run tests quickly with the default Python
55 | pytest tests -s --cache-clear
56 |
57 | test-all: clean-test ## run tests on every Python version with tox
58 | pytest tests --cache-clear
59 |
60 | coverage: clean-test ## check code coverage quickly with the default Python
61 | coverage run -m pytest -v tests
62 | coverage report -m --skip-covered
63 | coverage html
64 | $(BROWSER) htmlcov/index.html
65 |
66 | docs-usage:
67 | python.exe snapchat_dl/cli.py --help > USAGE.rst
68 |
69 | docs: docs-usage## generate Sphinx HTML documentation, including API docs
70 | rm -f docs/snapchat_dl.rst
71 | rm -f docs/modules.rst
72 | sphinx-apidoc -o docs/ snapchat_dl
73 | $(MAKE) -C docs clean
74 | $(MAKE) -C docs html
75 | $(BROWSER) docs/_build/html/index.html
76 |
77 | servedocs: docs ## compile the docs watching for changes
78 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D .
79 |
80 | dist: clean ## builds source and wheel package
81 | python setup.py sdist
82 | python setup.py bdist_wheel
83 | ls -l dist
84 |
85 | install: clean ## install the package to the active Python's site-packages
86 | python setup.py install
87 |
--------------------------------------------------------------------------------
/tests/test_snapchat_dl.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Tests for `snapchat_dl` package."""
3 | import json
4 | import os
5 | import shutil
6 | import unittest
7 | from unittest import mock
8 |
9 | from snapchat_dl.snapchat_dl import SnapchatDL
10 | from snapchat_dl.utils import APIResponseError
11 | from snapchat_dl.utils import NoStoriesFound
12 | from snapchat_dl.utils import UserNotFoundError
13 |
14 |
15 | def teardown_module(module):
16 | shutil.rmtree(".test-data")
17 |
18 |
19 | class TestSnapchat_dl(unittest.TestCase):
20 | """Tests for `snapchat_dl` package."""
21 |
22 | def setUp(self):
23 | """Set up test fixtures."""
24 | self.snapchat_dl = SnapchatDL(
25 | limit_story=10,
26 | quiet=True,
27 | directory_prefix=".test-data",
28 | dump_json=True,
29 | )
30 | self.test_url = (
31 | "https://sample-videos.com/video321/mp4/240/big_buck_bunny_240p_1mb.mp4"
32 | )
33 | self.test_url404 = "https://google.com/error.html"
34 | self.username = "invalidusername"
35 | self.html = open(
36 | "tests/mock_data/invalidusername.html", "r", encoding="utf8"
37 | ).read()
38 | self.html_api_error = open(
39 | "tests/mock_data/api-error.html", "r", encoding="utf8"
40 | ).read()
41 | self.html_nostories = open(
42 | "tests/mock_data/invalidusername-nostories.html", "r", encoding="utf8"
43 | ).read()
44 |
45 | def test_class_init(self):
46 | """Test snapchat_dl init."""
47 | self.assertTrue(self.snapchat_dl)
48 |
49 | def test_invalid_username(self):
50 | """Test snapchat_dl Stories are not available."""
51 | with self.assertRaises(UserNotFoundError):
52 | self.snapchat_dl.download("use=rname")
53 |
54 | @mock.patch("snapchat_dl.snapchat_dl.SnapchatDL._api_response")
55 | def test_api_error(self, api_response):
56 | """Test snapchat_dl Download."""
57 | api_response.return_value = self.html_api_error
58 | with self.assertRaises(APIResponseError):
59 | self.snapchat_dl.download(self.username)
60 |
61 | @mock.patch("snapchat_dl.snapchat_dl.SnapchatDL._api_response")
62 | def test_get_stories(self, api_response):
63 | """Test snapchat_dl Download."""
64 | api_response.return_value = self.html
65 | self.snapchat_dl.download(self.username)
66 |
67 | @mock.patch("snapchat_dl.snapchat_dl.SnapchatDL._api_response")
68 | def test_no_stories(self, api_response):
69 | """Test snapchat_dl Download."""
70 | api_response.return_value = self.html_nostories
71 | with self.assertRaises(NoStoriesFound):
72 | self.snapchat_dl.download(self.username)
73 |
--------------------------------------------------------------------------------
/snapchat_dl/app.py:
--------------------------------------------------------------------------------
1 | """Commandline setup for snapchat_dl."""
2 | import sys
3 | import time
4 |
5 | import pyperclip
6 | from loguru import logger
7 |
8 | from snapchat_dl.cli import parse_arguments
9 | from snapchat_dl.snapchat_dl import SnapchatDL
10 | from snapchat_dl.utils import NoStoriesFound
11 | from snapchat_dl.utils import search_usernames
12 | from snapchat_dl.utils import use_batch_file
13 | from snapchat_dl.utils import use_prefix_dir
14 | from snapchat_dl.utils import UserNotFoundError
15 |
16 |
17 | def main():
18 | """Download user stories from Snapchat."""
19 | args = parse_arguments()
20 | usernames = args.username + use_batch_file(args) + use_prefix_dir(args)
21 |
22 | downlaoder = SnapchatDL(
23 | directory_prefix=args.save_prefix,
24 | max_workers=args.max_workers,
25 | limit_story=args.limit_story,
26 | sleep_interval=args.sleep_interval,
27 | quiet=args.quiet,
28 | dump_json=args.dump_json,
29 | )
30 |
31 | history = list()
32 |
33 | def download_users(users: list, respect_history=False):
34 | """Download user story from usernames.
35 |
36 | Args:
37 | users (list): List of usernames to download.
38 | respect_history (bool, optional): append username to history. Defaults to False.
39 | log_str (str, optional): Log log_str to terminal. Defaults to None.
40 | """
41 | for username in users:
42 | time.sleep(args.sleep_interval)
43 |
44 | if respect_history is True:
45 | if username not in history:
46 | history.append(username)
47 | try:
48 | downlaoder.download(username)
49 | except (NoStoriesFound, UserNotFoundError):
50 | pass
51 | else:
52 | try:
53 | downlaoder.download(username)
54 | except (NoStoriesFound, UserNotFoundError):
55 | pass
56 |
57 | try:
58 | download_users(usernames)
59 | if args.scan_clipboard is True:
60 | if args.quiet is False:
61 | logger.info("Listening for clipboard change")
62 |
63 | while True:
64 | usernames_clip = search_usernames(pyperclip.paste())
65 | if len(usernames_clip) > 0:
66 | download_users(usernames_clip, respect_history=True)
67 |
68 | time.sleep(1)
69 |
70 | if args.check_update is True:
71 | if args.quiet is False:
72 | logger.info(
73 | "Scheduling story updates for {} users".format(len(usernames))
74 | )
75 |
76 | while True:
77 | started_at = int(time.time())
78 | download_users(usernames)
79 | if started_at < args.interval:
80 | time.sleep(args.interval - started_at)
81 |
82 | except KeyboardInterrupt:
83 | exit(0)
84 |
85 |
86 | if __name__ == "__main__":
87 | sys.exit(main())
88 |
--------------------------------------------------------------------------------
/snapchat_dl/cli.py:
--------------------------------------------------------------------------------
1 | """Console script for snapchat_dl."""
2 | import argparse
3 | import os
4 | import sys
5 |
6 |
7 | def parse_arguments():
8 | """Console script for snapchat_dl."""
9 | parser = argparse.ArgumentParser(prog="snapchat-dl")
10 |
11 | parser.add_argument(
12 | "username",
13 | action="store",
14 | nargs="*",
15 | help="At least one or more usernames to download stories for.",
16 | )
17 |
18 | any_one_group = parser.add_mutually_exclusive_group()
19 | any_one_group.add_argument(
20 | "-c",
21 | "--scan-clipboard",
22 | action="store_true",
23 | help="Scan clipboard for story links"
24 | " ('https://story.snapchat.com//').",
25 | dest="scan_clipboard",
26 | )
27 |
28 | any_one_group.add_argument(
29 | "-u",
30 | "--check-for-update",
31 | action="store_true",
32 | help="Periodically check for new stories.",
33 | dest="check_update",
34 | )
35 |
36 | parser.add_argument(
37 | "-i",
38 | "--batch-file",
39 | action="store",
40 | default=None,
41 | help="Read usernames from batch file (one username per line).",
42 | metavar="BATCH_FILENAME",
43 | dest="batch_file",
44 | )
45 |
46 | parser.add_argument(
47 | "-P",
48 | "--directory-prefix",
49 | action="store",
50 | default=os.path.abspath(os.getcwd()),
51 | help="Location to store downloaded media.",
52 | metavar="DIRECTORY_PREFIX",
53 | dest="save_prefix",
54 | )
55 |
56 | parser.add_argument(
57 | "-s",
58 | "--scan-from-prefix",
59 | action="store_true",
60 | help="Scan usernames (as directory name) from prefix directory.",
61 | dest="scan_prefix",
62 | )
63 |
64 | parser.add_argument(
65 | "-d",
66 | "--dump-json",
67 | action="store_true",
68 | help="Save metadata to a JSON file next to downloaded videos/pictures.",
69 | dest="dump_json",
70 | )
71 |
72 | parser.add_argument(
73 | "-l",
74 | "--limit-story",
75 | action="store",
76 | default=-1,
77 | help="Set maximum number of stories to download.",
78 | metavar="MAX_NUM_STORY",
79 | dest="limit_story",
80 | type=int,
81 | )
82 |
83 | parser.add_argument(
84 | "-j",
85 | "--max-concurrent-downloads",
86 | action="store",
87 | default=2,
88 | help="Set maximum number of parallel downloads.",
89 | metavar="MAX_WORKERS",
90 | dest="max_workers",
91 | type=int,
92 | )
93 |
94 | parser.add_argument(
95 | "-t",
96 | "--update-interval",
97 | action="store",
98 | default=60 * 10,
99 | help="Set the update interval for checking new story in seconds. (Default: 10m)",
100 | metavar="INTERVAL",
101 | dest="interval",
102 | type=int,
103 | )
104 |
105 | parser.add_argument(
106 | "--sleep-interval",
107 | action="store",
108 | default=1,
109 | help="Sleep between downloads in seconds. (Default: 1s)",
110 | metavar="INTERVAL",
111 | dest="sleep_interval",
112 | type=int,
113 | )
114 |
115 | parser.add_argument(
116 | "-q",
117 | "--quiet",
118 | action="store_true",
119 | help="Do not print anything except errors to the console.",
120 | )
121 |
122 | if len(sys.argv) == 1:
123 | parser.print_help()
124 | sys.exit(1)
125 |
126 | return parser.parse_args()
127 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Contributions are welcome, and they are greatly appreciated! Every little bit
4 | helps, and credit will always be given.
5 |
6 | You can contribute in many ways:
7 |
8 | ## Types of Contributions
9 |
10 | ### Report Bugs
11 |
12 | Report bugs [here](https://github.com/skyme5/snapchat-dl/issues).
13 |
14 | If you are reporting a bug, please include:
15 |
16 | * Your operating system name and version.
17 | * Any details about your local setup that might be helpful in troubleshooting.
18 | * Detailed steps to reproduce the bug.
19 |
20 | ### Fix Bugs
21 |
22 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help
23 | wanted" is open to whoever wants to implement it.
24 |
25 | ### Implement Features
26 |
27 | Look through the GitHub issues for features. Anything tagged with "enhancement"
28 | and "help wanted" is open to whoever wants to implement it.
29 |
30 | ### Write Documentation
31 |
32 | Snapchat Downloader could always use more documentation, whether as part of the
33 | official Snapchat Downloader docs, in docstrings, or even on the web in blog posts,
34 | articles, and such.
35 |
36 | ### Submit Feedback
37 |
38 | The best way to send feedback is to file an issue [here](https://github.com/skyme5/snapchat-dl/issues).
39 |
40 | If you are proposing a feature:
41 |
42 | * Explain in detail how it would work.
43 | * Keep the scope as narrow as possible, to make it easier to implement.
44 | * Remember that this is a volunteer-driven project, and that contributions
45 | are welcome :)
46 |
47 | ### Get Started!
48 |
49 | Ready to contribute? Here's how to set up `snapchat-dl` for local development.
50 |
51 | 1. Fork the `snapchat-dl` repo on GitHub.
52 | 2. Clone your fork locally::
53 |
54 | ```bash
55 | $ git clone git@github.com:/snapchat-dl.git
56 | ```
57 |
58 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development::
59 |
60 | ```bash
61 | $ mkvirtualenv snapchat-dl
62 | $ cd snapchat-dl/
63 | $ python setup.py develop
64 | ```
65 |
66 | 4. Create a branch for local development::
67 |
68 | ```bash
69 | $ git checkout -b name-of-your-bugfix-or-feature
70 | ```
71 |
72 | Now you can make your changes locally.
73 |
74 | 5. When you're done making changes, check that your changes pass flake8 and the
75 | tests, including testing other Python versions with tox::
76 |
77 | ```bash
78 | $ flake8 snapchat_dl tests
79 | $ python setup.py test or pytest
80 | ```
81 |
82 | To get flake8 and tox, just pip install them into your virtualenv.
83 |
84 | 6. Commit your changes and push your branch to GitHub::
85 |
86 | ```bash
87 | $ git add .
88 | $ git commit -m "Your detailed description of your changes."
89 | $ git push origin name-of-your-bugfix-or-feature
90 | ```
91 |
92 | 7. Submit a pull request through the GitHub website.
93 |
94 | ### Pull Request Guidelines
95 |
96 | Before you submit a pull request, check that it meets these guidelines:
97 |
98 | 1. The pull request should include tests.
99 | 2. If the pull request adds functionality, the docs should be updated. Put
100 | your new functionality into a function with a docstring, and add the
101 | feature to the list in README.rst.
102 | 3. The pull request should work for Python 3.5, 3.6, 3.7 and 3.8, and for PyPy. Check
103 | https://travis-ci.com/skyme5/snapchat-dl/pull_requests
104 | and make sure that the tests pass for all supported Python versions.
105 |
106 | ### Tips
107 |
108 | To run a subset of tests::
109 |
110 | ```bash
111 | $ python -m unittest tests.test_snapchat_dl
112 | ```
113 |
114 | ### Deploying
115 |
116 | A reminder for the maintainers on how to deploy.
117 | Make sure all your changes are committed (including an entry in HISTORY.rst).
118 | Then run::
119 |
120 | ```bash
121 | $ bump2version patch # possible: major / minor / patch
122 | $ git push
123 | $ git push --tags
124 | ```
125 |
126 | Travis will then deploy to PyPI if tests pass.
127 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
54 |

55 |
56 |
57 |
58 | ### Installation
59 |
60 | Install using pip,
61 |
62 | ```bash
63 | pip install snapchat-dl
64 | ```
65 |
66 | Install from GitHub,
67 |
68 | ```bash
69 | pip install git+git://github.com/skyme5/snapchat-dl
70 | ```
71 |
72 | Unix users might want to add `--user` flag to install without requiring `sudo`.
73 |
74 | ### Usage
75 |
76 | ```text
77 |
78 | usage: snapchat-dl [-h] [-c | -u] [-i BATCH_FILENAME] [-P DIRECTORY_PREFIX]
79 | [-s] [-d] [-l MAX_NUM_STORY] [-j MAX_WORKERS] [-t INTERVAL]
80 | [--sleep-interval INTERVAL] [-q]
81 | [username [username ...]]
82 |
83 | positional arguments:
84 | username At least one or more usernames to download stories
85 | for.
86 |
87 | optional arguments:
88 | -h, --help show this help message and exit
89 | -c, --scan-clipboard Scan clipboard for story links
90 | ('https://story.snapchat.com//').
91 | -u, --check-for-update
92 | Periodically check for new stories.
93 | -i BATCH_FILENAME, --batch-file BATCH_FILENAME
94 | Read usernames from batch file (one username per
95 | line).
96 | -P DIRECTORY_PREFIX, --directory-prefix DIRECTORY_PREFIX
97 | Location to store downloaded media.
98 | -s, --scan-from-prefix
99 | Scan usernames (as directory name) from prefix
100 | directory.
101 | -d, --dump-json Save metadata to a JSON file next to downloaded
102 | videos/pictures.
103 | -l MAX_NUM_STORY, --limit-story MAX_NUM_STORY
104 | Set maximum number of stories to download.
105 | -j MAX_WORKERS, --max-concurrent-downloads MAX_WORKERS
106 | Set maximum number of parallel downloads.
107 | -t INTERVAL, --update-interval INTERVAL
108 | Set the update interval for checking new story in
109 | seconds. (Default: 10m)
110 | --sleep-interval INTERVAL
111 | Sleep between downloads in seconds. (Default: 1s)
112 | -q, --quiet Do not print anything except errors to the console.
113 |
114 | ```
115 |
--------------------------------------------------------------------------------
/snapchat_dl/utils.py:
--------------------------------------------------------------------------------
1 | """Utility functions for snapchat_dl."""
2 | import json
3 | import os
4 | import re
5 | from argparse import Namespace
6 | from datetime import datetime
7 |
8 | from loguru import logger
9 |
10 |
11 | MEDIA_TYPE = ["jpg", "mp4"]
12 |
13 |
14 | class NoStoriesFound(Exception):
15 | """No stories found."""
16 |
17 | pass
18 |
19 |
20 | class APIResponseError(Exception):
21 | """Invalid API Response"""
22 |
23 | pass
24 |
25 |
26 | class UserNotFoundError(Exception):
27 | """User not found"""
28 |
29 | pass
30 |
31 |
32 | def strf_time(timestamp, format_str):
33 | """Format unix timestamp to custom format.
34 |
35 | Args:
36 | timestamp (int): unix timestamp
37 | format_str (str): valid python date time format
38 |
39 | Returns:
40 | str: timestamp formatted to custom format.
41 | """
42 | return datetime.utcfromtimestamp(timestamp).strftime(format_str)
43 |
44 |
45 | def valid_username(username):
46 | """Validate Username.
47 |
48 | Args:
49 | username (str): Snapchat Username
50 |
51 | Returns:
52 | bool: True if username is valid.
53 | """
54 | match = re.match(r"(?P^[\-\w\.\_]{3,15}$)", username)
55 | if match is None:
56 | return False
57 |
58 | return match and match.groupdict()["username"] == username
59 |
60 |
61 | def search_usernames(string: str) -> list:
62 | """Return list of usernames found in a string.
63 |
64 | Args:
65 | string (str): string to search for usernames
66 |
67 | Returns:
68 | list: usernames found in string
69 | """
70 | return list(
71 | sorted(
72 | set(
73 | [
74 | username
75 | for username in re.findall(
76 | r"https?://(?:story|www).snapchat.com/(?:[suad]+/|@)([\-\w\.\_]{3,15})",
77 | string,
78 | )
79 | if valid_username(username)
80 | ]
81 | )
82 | )
83 | )
84 |
85 |
86 | def use_batch_file(args: Namespace) -> list:
87 | """Return list of usernames from file args.batch_file.
88 |
89 | Args:
90 | args (Namespace): argparse Namespace
91 |
92 | Raises:
93 | os.error: raises if batch_file not found
94 |
95 | Returns:
96 | list: usernames read from batch_file
97 | """
98 | usernames = list()
99 | if args.batch_file is not None:
100 | if os.path.isfile(args.batch_file) is False:
101 | raise Exception(
102 | logger.error("Invalid Batch File at {}".format(args.batch_file))
103 | )
104 |
105 | with open(args.batch_file, "r") as f:
106 | for u in f.read().split("\n"):
107 | username = u.strip()
108 | if valid_username(username) and username not in usernames:
109 | usernames.append(username)
110 |
111 | return usernames
112 |
113 |
114 | def use_prefix_dir(args: Namespace):
115 | """Return dirnames as username from file args.scan_prefix.
116 |
117 | Args:
118 | args (Namespace): argparse Namespace
119 |
120 | Returns:
121 | list: usernames read from scan_prefix
122 | """
123 | usernames = list()
124 | if args.scan_prefix:
125 | for username in [
126 | o
127 | for o in os.listdir(args.save_prefix)
128 | if os.path.isdir(os.path.join(args.save_prefix, o))
129 | ]:
130 | if username not in usernames and valid_username(username):
131 | usernames.append(username)
132 |
133 | if args.quiet is False:
134 | logger.info(
135 | "Added {} usernames from {}".format(len(usernames), args.save_prefix)
136 | )
137 |
138 | return list(sorted(set(usernames)))
139 |
140 |
141 | def dump_text_file(content: str, filepath: str):
142 | """Write content to filepath using `tx` mode.
143 |
144 | Args:
145 | content (str): File content to write.
146 | filepath (str): Filepath.
147 |
148 | This will overwrite the file.
149 | """
150 | dirpath = os.path.dirname(filepath)
151 |
152 | os.makedirs(dirpath, exist_ok=True)
153 |
154 | if not os.path.isfile(filepath):
155 | with open(filepath, "w+") as f:
156 | f.write(content)
157 |
158 |
159 | def dump_response(content: dict, path: str):
160 | """Save JSON file
161 |
162 | Args:
163 | content: JSON data
164 | path: Path to save json
165 |
166 | Returns:
167 | None
168 | """
169 | dump_text_file(json.dumps(content), path)
170 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # snapchat_dl documentation build configuration file, created by
4 | # sphinx-quickstart on Fri Jun 9 13:47:02 2017.
5 | #
6 | # This file is execfile()d with the current directory set to its
7 | # containing dir.
8 | #
9 | # Note that not all possible configuration values are present in this
10 | # autogenerated file.
11 | #
12 | # All configuration values have a default; values that are commented out
13 | # serve to show the default.
14 | # If extensions (or modules to document with autodoc) are in another
15 | # directory, add these directories to sys.path here. If the directory is
16 | # relative to the documentation root, use os.path.abspath to make it
17 | # absolute, like shown here.
18 | #
19 | import os
20 | import sys
21 |
22 | sys.path.insert(0, os.path.abspath(".."))
23 |
24 | import snapchat_dl
25 |
26 | # -- General configuration ---------------------------------------------
27 |
28 | # If your documentation needs a minimal Sphinx version, state it here.
29 | #
30 | # needs_sphinx = '1.0'
31 |
32 | # Add any Sphinx extension module names here, as strings. They can be
33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
34 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"]
35 |
36 | # Add any paths that contain templates here, relative to this directory.
37 | templates_path = ["_templates"]
38 |
39 | # The suffix(es) of source filenames.
40 | # You can specify multiple suffix as a list of string:
41 | #
42 | # source_suffix = ['.rst', '.md']
43 | source_suffix = ".rst"
44 |
45 | # The master toctree document.
46 | master_doc = "index"
47 |
48 | # General information about the project.
49 | project = "Snapchat Downloader"
50 | copyright = "2020, Aakash Gajjar"
51 | author = "Aakash Gajjar"
52 |
53 | # The version info for the project you're documenting, acts as replacement
54 | # for |version| and |release|, also used in various other places throughout
55 | # the built documents.
56 | #
57 | # The short X.Y version.
58 | version = snapchat_dl.__version__
59 | # The full version, including alpha/beta/rc tags.
60 | release = snapchat_dl.__version__
61 |
62 | # The language for content autogenerated by Sphinx. Refer to documentation
63 | # for a list of supported languages.
64 | #
65 | # This is also used if you do content translation via gettext catalogs.
66 | # Usually you set "language" from the command line for these cases.
67 | language = None
68 |
69 | # List of patterns, relative to source directory, that match files and
70 | # directories to ignore when looking for source files.
71 | # This patterns also effect to html_static_path and html_extra_path
72 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
73 |
74 | # The name of the Pygments (syntax highlighting) style to use.
75 | pygments_style = "sphinx"
76 |
77 | # If true, `todo` and `todoList` produce output, else they produce nothing.
78 | todo_include_todos = False
79 |
80 |
81 | # -- Options for HTML output -------------------------------------------
82 |
83 | # The theme to use for HTML and HTML Help pages. See the documentation for
84 | # a list of builtin themes.
85 | #
86 | html_theme = "alabaster"
87 |
88 | # Theme options are theme-specific and customize the look and feel of a
89 | # theme further. For a list of options available for each theme, see the
90 | # documentation.
91 | #
92 | # html_theme_options = {}
93 |
94 | # Add any paths that contain custom static files (such as style sheets) here,
95 | # relative to this directory. They are copied after the builtin static files,
96 | # so a file named "default.css" will overwrite the builtin "default.css".
97 | html_static_path = ["_static"]
98 |
99 |
100 | # -- Options for HTMLHelp output ---------------------------------------
101 |
102 | # Output file base name for HTML help builder.
103 | htmlhelp_basename = "snapchat_dldoc"
104 |
105 |
106 | # -- Options for LaTeX output ------------------------------------------
107 |
108 | latex_elements = {
109 | # The paper size ('letterpaper' or 'a4paper').
110 | #
111 | # 'papersize': 'letterpaper',
112 | # The font size ('10pt', '11pt' or '12pt').
113 | #
114 | # 'pointsize': '10pt',
115 | # Additional stuff for the LaTeX preamble.
116 | #
117 | # 'preamble': '',
118 | # Latex figure (float) alignment
119 | #
120 | # 'figure_align': 'htbp',
121 | }
122 |
123 | # Grouping the document tree into LaTeX files. List of tuples
124 | # (source start file, target name, title, author, documentclass
125 | # [howto, manual, or own class]).
126 | latex_documents = [
127 | (
128 | master_doc,
129 | "snapchat_dl.tex",
130 | "Snapchat Downloader Documentation",
131 | "Aakash Gajjar",
132 | "manual",
133 | ),
134 | ]
135 |
136 |
137 | # -- Options for manual page output ------------------------------------
138 |
139 | # One entry per manual page. List of tuples
140 | # (source start file, name, description, authors, manual section).
141 | man_pages = [
142 | (master_doc, "snapchat_dl", "Snapchat Downloader Documentation", [author], 1)
143 | ]
144 |
145 |
146 | # -- Options for Texinfo output ----------------------------------------
147 |
148 | # Grouping the document tree into Texinfo files. List of tuples
149 | # (source start file, target name, title, author,
150 | # dir menu entry, description, category)
151 | texinfo_documents = [
152 | (
153 | master_doc,
154 | "snapchat_dl",
155 | "Snapchat Downloader Documentation",
156 | author,
157 | "snapchat_dl",
158 | "One line description of project.",
159 | "Miscellaneous",
160 | ),
161 | ]
162 |
--------------------------------------------------------------------------------
/snapchat_dl/snapchat_dl.py:
--------------------------------------------------------------------------------
1 | """The Main Snapchat Downloader Class."""
2 |
3 | import concurrent.futures
4 | import json
5 | import os
6 | import re
7 |
8 | import requests
9 | from loguru import logger
10 |
11 | from snapchat_dl.downloader import download_url
12 | from snapchat_dl.utils import APIResponseError
13 | from snapchat_dl.utils import dump_response
14 | from snapchat_dl.utils import MEDIA_TYPE
15 | from snapchat_dl.utils import NoStoriesFound
16 | from snapchat_dl.utils import strf_time
17 | from snapchat_dl.utils import UserNotFoundError
18 |
19 |
20 | class SnapchatDL:
21 | """Interact with Snapchat API to download story."""
22 |
23 | def __init__(
24 | self,
25 | directory_prefix=".",
26 | max_workers=2,
27 | limit_story=-1,
28 | sleep_interval=1,
29 | quiet=False,
30 | dump_json=False,
31 | ):
32 | self.directory_prefix = os.path.abspath(os.path.normpath(directory_prefix))
33 | self.max_workers = max_workers
34 | self.limit_story = limit_story
35 | self.sleep_interval = sleep_interval
36 | self.quiet = quiet
37 | self.dump_json = dump_json
38 | self.endpoint_web = "https://www.snapchat.com/add/{}/"
39 | self.regexp_web_json = (
40 | r'Mrunu (@mrunu11) on Snapchat