├── tests
├── __init__.py
└── test_main.py
├── readme-assets
└── icons
│ ├── name.png
│ ├── TStickers.xcf
│ └── proj-icon.png
├── documentation
├── tutorials
│ ├── assets
│ │ ├── step1.png
│ │ ├── step3_0.png
│ │ └── step3_1.png
│ ├── README.md
│ ├── backends.md
│ └── getting-started.md
└── reference
│ ├── tstickers
│ ├── module.md
│ ├── cli.md
│ ├── index.md
│ ├── convert_pyrlottie.md
│ ├── convert_rlottie_python.md
│ ├── caching.md
│ ├── convert.md
│ └── manager.md
│ └── README.md
├── tstickers
├── __main__.py
├── __init__.py
├── convert_pyrlottie.py
├── caching.py
├── convert_rlottie_python.py
├── cli.py
├── convert.py
└── manager.py
├── move_webp_stickers.py
├── .github
└── workflows
│ ├── mirror-to-codeberg.yaml
│ └── test-lint.yaml
├── requirements.txt
├── LICENSE.md
├── .pre-commit-config.yaml
├── pyproject.toml
├── .gitignore
├── CHANGELOG.md
└── README.md
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/readme-assets/icons/name.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FHPythonUtils/TStickers/HEAD/readme-assets/icons/name.png
--------------------------------------------------------------------------------
/readme-assets/icons/TStickers.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FHPythonUtils/TStickers/HEAD/readme-assets/icons/TStickers.xcf
--------------------------------------------------------------------------------
/readme-assets/icons/proj-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FHPythonUtils/TStickers/HEAD/readme-assets/icons/proj-icon.png
--------------------------------------------------------------------------------
/documentation/tutorials/assets/step1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FHPythonUtils/TStickers/HEAD/documentation/tutorials/assets/step1.png
--------------------------------------------------------------------------------
/documentation/tutorials/assets/step3_0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FHPythonUtils/TStickers/HEAD/documentation/tutorials/assets/step3_0.png
--------------------------------------------------------------------------------
/documentation/tutorials/assets/step3_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FHPythonUtils/TStickers/HEAD/documentation/tutorials/assets/step3_1.png
--------------------------------------------------------------------------------
/tstickers/__main__.py:
--------------------------------------------------------------------------------
1 | """entry point for python -m tstickers."""
2 |
3 | from __future__ import annotations
4 |
5 | from tstickers import cli
6 |
7 | cli()
8 |
--------------------------------------------------------------------------------
/documentation/reference/tstickers/module.md:
--------------------------------------------------------------------------------
1 | # Module
2 |
3 | [Tstickers Index](../README.md#tstickers-index) / [Tstickers](./index.md#tstickers) / Module
4 |
5 | > Auto-generated documentation for [tstickers.__main__](../../../tstickers/__main__.py) module.
6 | - [Module](#module)
7 |
--------------------------------------------------------------------------------
/tstickers/__init__.py:
--------------------------------------------------------------------------------
1 | """Entry point for python -m sigstickers."""
2 |
3 | import sys
4 |
5 | from loguru import logger
6 |
7 | from tstickers.cli import cli
8 |
9 | _ = cli
10 |
11 |
12 | logger.remove(0)
13 | logger.add(
14 | sys.stderr,
15 | format="{level: <8} | {message}",
16 | )
17 |
--------------------------------------------------------------------------------
/documentation/reference/tstickers/cli.md:
--------------------------------------------------------------------------------
1 | # Cli
2 |
3 | [Tstickers Index](../README.md#tstickers-index) / [Tstickers](./index.md#tstickers) / Cli
4 |
5 | > Auto-generated documentation for [tstickers.cli](../../../tstickers/cli.py) module.
6 |
7 | - [Cli](#cli)
8 | - [cli](#cli)
9 |
10 | ## cli
11 |
12 | [Show source in cli.py:20](../../../tstickers/cli.py#L20)
13 |
14 | Cli entry point.
15 |
16 | #### Signature
17 |
18 | ```python
19 | def cli() -> None: ...
20 | ```
--------------------------------------------------------------------------------
/move_webp_stickers.py:
--------------------------------------------------------------------------------
1 | import shutil
2 | from pathlib import Path
3 |
4 | source_dir = Path("./downloads")
5 |
6 | packnames = [name.name for name in source_dir.iterdir() if name.is_dir()]
7 |
8 | for packname in packnames:
9 | source_path = source_dir / packname / "webp"
10 | dest_path = Path("./sorted") / packname
11 |
12 | print(packname)
13 |
14 | if not dest_path.exists():
15 | dest_path.mkdir(parents=True)
16 |
17 | for file_path in source_path.iterdir():
18 | shutil.copy(file_path, dest_path)
19 |
--------------------------------------------------------------------------------
/documentation/reference/tstickers/index.md:
--------------------------------------------------------------------------------
1 | # Tstickers
2 |
3 | [Tstickers Index](../README.md#tstickers-index) / Tstickers
4 |
5 | > Auto-generated documentation for [tstickers](../../../tstickers/__init__.py) module.
6 |
7 | - [Tstickers](#tstickers)
8 | - [Modules](#modules)
9 |
10 | ## Modules
11 |
12 | - [Module](./module.md)
13 | - [Caching](./caching.md)
14 | - [Cli](./cli.md)
15 | - [Convert](./convert.md)
16 | - [Convert Pyrlottie](./convert_pyrlottie.md)
17 | - [Convert Rlottie Python](./convert_rlottie_python.md)
18 | - [Manager](./manager.md)
--------------------------------------------------------------------------------
/.github/workflows/mirror-to-codeberg.yaml:
--------------------------------------------------------------------------------
1 |
2 | # Sync repo to the Codeberg mirror
3 | name: Repo sync GitHub -> Codeberg
4 | on:
5 | push:
6 | branches:
7 | - '**'
8 |
9 | jobs:
10 | codeberg:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | with:
15 | fetch-depth: 0
16 | - uses: spyoungtech/mirror-action@v0.7.0
17 | with:
18 | REMOTE: "https://codeberg.org/FredHappyface/TStickers.git"
19 | GIT_USERNAME: FredHappyface
20 | GIT_PASSWORD: ${{ secrets.CODEBERG_PASSWORD }}
21 |
--------------------------------------------------------------------------------
/documentation/reference/README.md:
--------------------------------------------------------------------------------
1 | # Tstickers Index
2 |
3 | > Auto-generated documentation index.
4 |
5 | A full list of `Tstickers` project modules.
6 |
7 | - [Tstickers](tstickers/index.md#tstickers)
8 | - [Module](tstickers/module.md#module)
9 | - [Caching](tstickers/caching.md#caching)
10 | - [Cli](tstickers/cli.md#cli)
11 | - [Convert](tstickers/convert.md#convert)
12 | - [Convert Pyrlottie](tstickers/convert_pyrlottie.md#convert-pyrlottie)
13 | - [Convert Rlottie Python](tstickers/convert_rlottie_python.md#convert-rlottie-python)
14 | - [Manager](tstickers/manager.md#manager)
15 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # This file was autogenerated by uv via the following command:
2 | # uv export --no-hashes --no-dev -o requirements.txt
3 | -e .
4 | attrs==23.2.0
5 | cattrs==24.1.2
6 | certifi==2025.1.31
7 | charset-normalizer==3.4.1
8 | colorama==0.4.6 ; sys_platform == 'win32'
9 | emoji==2.14.1
10 | exceptiongroup==1.2.2 ; python_full_version < '3.11'
11 | idna==3.10
12 | loguru==0.7.3
13 | pillow==10.4.0
14 | platformdirs==4.3.6
15 | requests==2.32.3
16 | requests-cache==1.2.1
17 | rlottie-python==1.3.6
18 | six==1.17.0
19 | typing-extensions==4.12.2 ; python_full_version < '3.11'
20 | url-normalize==1.4.3
21 | urllib3==2.3.0
22 | win32-setctime==1.2.0 ; sys_platform == 'win32'
23 |
--------------------------------------------------------------------------------
/documentation/tutorials/README.md:
--------------------------------------------------------------------------------
1 |
2 | # TStickers Tutorials and User Guides
3 |
4 | Welcome to the TStickers tutorials. This section provides a handful of tutorials
5 | to help you get started with TStickers and make the most of its features.
6 |
7 | ## Resource Table
8 |
9 | | Guide | Description | Link |
10 | |-----------------------|---------------------|---------------------------------------|
11 | | **getting-started.md** | This guide will walk you through the initial setup of TStickers, including installation and basic usage. | [View getting-started.md](./getting-started.md) |
12 | | **backends.md** | Learn about the different backend options available with TStickers. | [View backends.md](./backends.md) |
13 |
--------------------------------------------------------------------------------
/.github/workflows/test-lint.yaml:
--------------------------------------------------------------------------------
1 | name: Python Test and Lint
2 |
3 | on:
4 | push:
5 | branches:
6 | - '*'
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | test:
13 | name: Python Test and Lint
14 | runs-on: ubuntu-latest
15 |
16 | strategy:
17 | matrix:
18 | python-version:
19 | - '3.9'
20 | - '3.10'
21 | - '3.11'
22 | - '3.12'
23 | - '3.13'
24 |
25 | steps:
26 | - name: Checkout code
27 | uses: actions/checkout@v4
28 |
29 | - name: Set up Python ${{ matrix.python-version }}
30 | uses: actions/setup-python@v4
31 | with:
32 | python-version: ${{ matrix.python-version }}
33 |
34 | - name: Install UV
35 | run: |
36 | curl -LsSf https://astral.sh/uv/install.sh | sh
37 |
38 | - name: Install dependencies
39 | run: uv sync
40 |
41 | - name: Run pytest
42 | run: uv run pytest
43 |
44 | - name: Run ruff
45 | run: uv run ruff check --output-format=github
46 | continue-on-error: true
47 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | Copyright (c) 2020 Kieran W
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 |
--------------------------------------------------------------------------------
/documentation/reference/tstickers/convert_pyrlottie.md:
--------------------------------------------------------------------------------
1 | # Convert Pyrlottie
2 |
3 | [Tstickers Index](../README.md#tstickers-index) / [Tstickers](./index.md#tstickers) / Convert Pyrlottie
4 |
5 | > Auto-generated documentation for [tstickers.convert_pyrlottie](../../../tstickers/convert_pyrlottie.py) module.
6 |
7 | - [Convert Pyrlottie](#convert-pyrlottie)
8 | - [convertAnimated](#convertanimated)
9 |
10 | ## convertAnimated
11 |
12 | [Show source in convert_pyrlottie.py:17](../../../tstickers/convert_pyrlottie.py#L17)
13 |
14 | Convert animated stickers, over a number of threads, at a given framerate, scale and to a
15 | set of formats.
16 |
17 | #### Arguments
18 |
19 | - `swd` *Path* - The sticker working directory (e.g., downloads/packName).
20 | - `_threads` *int* - This is ignored for the pyrlottie backend
21 | - `fps` *int* - framerate of the converted sticker, affecting optimization and
22 | quality (default: 20)
23 | - `scale` *float* - Scale factor for up/downscaling images, affecting optimization and
24 | quality (default: 1).
25 | :param set[str] | None _formats: This is ignored for the pyrlottie backend
26 |
27 | #### Returns
28 |
29 | Type: *int*
30 | Number of stickers successfully converted.
31 |
32 | #### Signature
33 |
34 | ```python
35 | def convertAnimated(
36 | swd: Path,
37 | _threads: int = 4,
38 | fps: int = 20,
39 | scale: float = 1,
40 | _formats: set[str] | None = None,
41 | ) -> int: ...
42 | ```
--------------------------------------------------------------------------------
/tests/test_main.py:
--------------------------------------------------------------------------------
1 | """tests"""
2 |
3 | from __future__ import annotations
4 |
5 | import sys
6 | from pathlib import Path
7 |
8 | THISDIR = str(Path(__file__).resolve().parent)
9 | PROJECT_DIR = Path(THISDIR).parent
10 | sys.path.insert(0, str(PROJECT_DIR))
11 |
12 | from tstickers.convert import Backend
13 | from tstickers.manager import StickerManager
14 |
15 | token = ""
16 | for candidate in [PROJECT_DIR / "env.txt", PROJECT_DIR / "env"]:
17 | if candidate.exists():
18 | token = candidate.read_text(encoding="utf-8").strip()
19 | if not token:
20 | msg = (
21 | '!! Generate a bot token and paste in a file called "env". Send a '
22 | "message to @BotFather to get started"
23 | )
24 | raise RuntimeError(msg)
25 |
26 |
27 | stickerManager = StickerManager(token)
28 | stickerManager.cwd = Path(THISDIR) / "data"
29 |
30 | packs = [{"pack": "DonutTheDog", "len": 31}]
31 |
32 |
33 | def test_getPack() -> None:
34 | stickerPack = stickerManager.getPack(packs[0]["pack"])
35 | assert stickerPack is not None
36 | assert len(stickerPack["files"]) == packs[0]["len"]
37 |
38 |
39 | def test_downloadPack() -> None:
40 | stickerManager.downloadPack(packs[0]["pack"])
41 | assert len(list(Path(f"{stickerManager.cwd}/donutthedog/tgs").iterdir())) == packs[0]["len"]
42 |
43 |
44 | def test_convertPack() -> None:
45 | stickerManager.downloadPack(packs[0]["pack"])
46 | stickerManager.convertPack(
47 | packs[0]["pack"], scale=0.05, noCache=True, backend=Backend.RLOTTIE_PYTHON
48 | )
49 | assert len(list(Path(f"{stickerManager.cwd}/donutthedog/webp").iterdir())) == packs[0]["len"]
50 |
51 |
52 | # def test_convertPack_slow() -> None:
53 | # stickerManager.downloadPack(packs[0]["pack"])
54 | # stickerManager.convertPack(packs[0]["pack"], scale=1, noCache=True, backend=Backend.PYRLOTTIE)
55 | # assert len(list(Path(f"{stickerManager.cwd}/donutthedog/webp").iterdir())) == packs[0]["len"]
56 |
--------------------------------------------------------------------------------
/documentation/tutorials/backends.md:
--------------------------------------------------------------------------------
1 |
2 | # Using Backends in TStickers
3 |
4 | TStickers supports multiple backends for converting sticker formats. You can choose between
5 | `rlottie-python` and `pyrlottie` depending on your needs and system compatibility. Here’s a brief
6 | guide on how to use and select these backends.
7 |
8 | ## Available Backends
9 |
10 | - **pyrlottie**: in my testing this seems to be a little faster at converting telegram stickers
11 | - **rlottie-python**: this backend has broader system compatibility, though seems to be a little
12 | slower on my machine™
13 |
14 | ## How to Specify a Backend
15 |
16 | You can specify the backend you want to use by using the `-b` or `--backend` option in your
17 | TStickers command.
18 |
19 | ### Command Syntax
20 |
21 | ```bash
22 | tstickers -b BACKEND [other options]
23 | ```
24 |
25 | ### Examples
26 |
27 | 1. **Using `rlottie-python` Backend:**
28 |
29 | If you want to use the `rlottie-python` backend, run:
30 |
31 | ```bash
32 | tstickers -b rlottie-python -t YOUR_BOT_TOKEN -p https://t.me/addstickers/YourStickerPack
33 | ```
34 |
35 | This command uses the `rlottie-python` backend to process the sticker pack specified by the URL.
36 |
37 | 2. **Using `pyrlottie` Backend:**
38 |
39 | To use the `pyrlottie` backend, execute:
40 |
41 | ```bash
42 | tstickers -b pyrlottie -t YOUR_BOT_TOKEN -p https://t.me/addstickers/YourStickerPack
43 | ```
44 |
45 | This command processes the sticker pack using the `pyrlottie` backend.
46 |
47 | ## Choosing the Right Backend
48 |
49 | - **Performance:** Test both backends to see which one performs better for your specific needs.
50 | - **Compatibility:** If you are having trouble with the default then try `rlottie-python`.
51 |
52 | Note if performance is important then you may want to explore the other options `--frameskip` and
53 | `--scale`. These will change the quality of the output image though!
54 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/astral-sh/ruff-pre-commit
3 | rev: v0.9.6
4 | hooks:
5 | - id: ruff
6 | args: [ --fix ]
7 | - id: ruff-format
8 |
9 | - repo: https://github.com/RobertCraigie/pyright-python
10 | rev: v1.1.394
11 | hooks:
12 | - id: pyright
13 |
14 | - repo: local
15 | hooks:
16 | - id: generate requirements
17 | name: generate requirements
18 | entry: uv export --no-hashes --no-dev -o requirements.txt
19 | language: system
20 | pass_filenames: false
21 | - id: safety
22 | name: safety
23 | entry: uv run safety
24 | language: system
25 | pass_filenames: false
26 | - id: make docs
27 | name: make docs
28 | entry: uv run handsdown --cleanup -o documentation/reference
29 | language: system
30 | pass_filenames: false
31 | - id: build package
32 | name: build package
33 | entry: uv build
34 | language: system
35 | pass_filenames: false
36 | - id: pytest
37 | name: pytest
38 | entry: uv run pytest
39 | language: system
40 | pass_filenames: false
41 |
42 | - repo: https://github.com/pre-commit/pre-commit-hooks
43 | rev: v5.0.0
44 | hooks:
45 | - id: trailing-whitespace
46 | - id: end-of-file-fixer
47 | - id: check-case-conflict
48 | - id: check-executables-have-shebangs
49 | - id: check-json
50 | - id: check-merge-conflict
51 | - id: check-shebang-scripts-are-executable
52 | - id: check-symlinks
53 | - id: check-toml
54 | - id: check-vcs-permalinks
55 | - id: check-yaml
56 | - id: detect-private-key
57 | - id: mixed-line-ending
58 |
59 | - repo: https://github.com/boidolr/pre-commit-images
60 | rev: v1.8.4
61 | hooks:
62 | - id: optimize-jpg
63 | - id: optimize-png
64 | - id: optimize-svg
65 | - id: optimize-webp
66 |
67 | exclude: "tests/data|documentation/reference"
68 |
--------------------------------------------------------------------------------
/documentation/reference/tstickers/convert_rlottie_python.md:
--------------------------------------------------------------------------------
1 | # Convert Rlottie Python
2 |
3 | [Tstickers Index](../README.md#tstickers-index) / [Tstickers](./index.md#tstickers) / Convert Rlottie Python
4 |
5 | > Auto-generated documentation for [tstickers.convert_rlottie_python](../../../tstickers/convert_rlottie_python.py) module.
6 |
7 | - [Convert Rlottie Python](#convert-rlottie-python)
8 | - [convertAnimated](#convertanimated)
9 | - [convert_single_tgs](#convert_single_tgs)
10 |
11 | ## convertAnimated
12 |
13 | [Show source in convert_rlottie_python.py:59](../../../tstickers/convert_rlottie_python.py#L59)
14 |
15 | Convert animated stickers, over a number of threads, at a given framerate, scale and to a
16 | set of formats.
17 |
18 | #### Arguments
19 |
20 | - `swd` *Path* - The sticker working directory (e.g., downloads/packName).
21 | - `threads` *int* - Number of threads for ProcessPoolExecutor (default: number of
22 | logical processors).
23 | - `fps` *int* - framerate of the converted sticker, affecting optimization and
24 | quality (default: 20)
25 | - `scale` *float* - Scale factor for up/downscaling images, affecting optimization and
26 | quality (default: 1).
27 | :param set[str]|None formats: Set of formats to convert telegram tgs stickers to
28 | (default: {"gif", "webp", "apng"})
29 |
30 | #### Returns
31 |
32 | Type: *int*
33 | Number of stickers successfully converted.
34 |
35 | #### Signature
36 |
37 | ```python
38 | def convertAnimated(
39 | swd: Path,
40 | threads: int = multiprocessing.cpu_count(),
41 | fps: int = 20,
42 | scale: float = 1,
43 | formats: set[str] | None = None,
44 | ) -> int: ...
45 | ```
46 |
47 |
48 |
49 | ## convert_single_tgs
50 |
51 | [Show source in convert_rlottie_python.py:18](../../../tstickers/convert_rlottie_python.py#L18)
52 |
53 | Convert a single tgs file.
54 |
55 | #### Arguments
56 |
57 | - `stckr` *Path* - Path to the sticker
58 | - `fps` *int* - framerate of the converted sticker, affecting optimization and
59 | quality (default: 20)
60 | - `scale` *float* - Scale factor for up/downscaling images, affecting optimization and
61 | quality (default: 1).
62 | :param set[str]|None formats: Set of formats to convert telegram tgs stickers to
63 | (default: {"gif", "webp", "apng"})
64 |
65 | #### Returns
66 |
67 | Type: *int*
68 | 1 if success
69 |
70 | #### Signature
71 |
72 | ```python
73 | def convert_single_tgs(
74 | stckr: Path, fps: int, scale: float = 1.0, formats: set[str] | None = None
75 | ) -> int: ...
76 | ```
--------------------------------------------------------------------------------
/tstickers/convert_pyrlottie.py:
--------------------------------------------------------------------------------
1 | """
2 | Conversion functionality for animated stickers.
3 |
4 | implements the conversion functionality for the pyrlottie backend. exposing a
5 | public function called `convertAnimated`, which is used to perform the conversion.
6 |
7 | """
8 |
9 | from __future__ import annotations
10 |
11 | import asyncio
12 | from pathlib import Path
13 |
14 | import pyrlottie
15 |
16 |
17 | def convertAnimated(
18 | swd: Path,
19 | _threads: int = 4,
20 | fps: int = 20,
21 | scale: float = 1,
22 | _formats: set[str] | None = None,
23 | ) -> int:
24 | """Convert animated stickers, over a number of threads, at a given framerate, scale and to a
25 | set of formats.
26 |
27 | :param Path swd: The sticker working directory (e.g., downloads/packName).
28 | :param int _threads: This is ignored for the pyrlottie backend
29 | :param int fps: framerate of the converted sticker, affecting optimization and
30 | quality (default: 20)
31 | :param float scale: Scale factor for up/downscaling images, affecting optimization and
32 | quality (default: 1).
33 | :param set[str] | None _formats: This is ignored for the pyrlottie backend
34 | :return int: Number of stickers successfully converted.
35 |
36 | """
37 | converted = 0
38 |
39 | (swd / "webp").mkdir(parents=True, exist_ok=True)
40 | (swd / "gif").mkdir(parents=True, exist_ok=True)
41 |
42 | # here we are going to assume that the raw image is 60 fps as pyrlottie does not have a way
43 | # of setting the fps
44 | frameSkip = 0
45 | if fps < 45: # bisector of 30 and 60
46 | frameSkip = 1 # ~30fps
47 | if fps < 25: # bisector of 20 and 30
48 | frameSkip = 2 # ~20fps
49 | if fps < 18: # bisector of 15 and 20
50 | frameSkip = 3 # ~15fps
51 | if fps < 13: # 12fps and below
52 | frameSkip = 4 # ~12fps
53 |
54 | def doConvMultLottie(filemaps: list[pyrlottie.FileMap], frameSkip: int, scale: float) -> int:
55 | return (
56 | len(
57 | asyncio.get_event_loop().run_until_complete(
58 | pyrlottie.convMultLottie(filemaps, frameSkip=frameSkip, scale=scale)
59 | )
60 | )
61 | // 2
62 | )
63 |
64 | converted += doConvMultLottie(
65 | filemaps=[
66 | pyrlottie.FileMap(
67 | pyrlottie.LottieFile(stckr.absolute().as_posix()),
68 | {
69 | stckr.absolute().as_posix().replace("tgs", "gif"),
70 | stckr.absolute().as_posix().replace("tgs", "webp"),
71 | },
72 | )
73 | for stckr in swd.glob("**/*.tgs")
74 | ],
75 | frameSkip=frameSkip,
76 | scale=scale,
77 | )
78 | return converted
79 |
--------------------------------------------------------------------------------
/tstickers/caching.py:
--------------------------------------------------------------------------------
1 | """Sticker caching functionality used by the downloader."""
2 |
3 | from __future__ import annotations
4 |
5 | import json
6 | from pathlib import Path
7 | from typing import Any, Callable
8 |
9 | from loguru import logger
10 | from requests_cache.session import CachedSession
11 |
12 | # requests_cache
13 | cachedSession = CachedSession(
14 | ".cache/tstickers.requests",
15 | backend="filesystem",
16 | expire_after=60 * 60 * 12,
17 | allowable_codes=(200,),
18 | allowable_methods=("GET", "POST"),
19 | )
20 |
21 |
22 | CACHE_DIR = Path(".cache")
23 | if not CACHE_DIR.exists():
24 | CACHE_DIR.mkdir()
25 |
26 |
27 | def verify_converted(pack_name: str) -> bool:
28 | """Verify the cache for a packName eg. "DonutTheDog". Uses the cache "version"
29 | to call the verify function for that version.
30 |
31 | Args:
32 | ----
33 | pack_name (str): name of the sticker pack eg. "DonutTheDog"
34 |
35 | Returns:
36 | -------
37 | bool: if the converted cache has been verified
38 |
39 | """
40 | cache = CACHE_DIR / pack_name
41 | if cache.exists():
42 | data = json.loads(cache.read_text(encoding="utf-8"))
43 | verify_func = _get_verify_function(data.get("version", 1))
44 | if verify_func(data):
45 | logger.info(f"-> Cache hit for {pack_name}!")
46 | return True
47 | logger.info(f"-> Cache miss for {pack_name}!")
48 | return False
49 |
50 |
51 | def _verify_converted_v1(data: dict[str, Any]) -> bool:
52 | """Verify the cache for a packName using cache data.
53 |
54 | Args:
55 | ----
56 | data (dict[Path, Any]): packName cache data to verify
57 |
58 | Returns:
59 | -------
60 | bool: if the converted cache has been verified
61 |
62 | """
63 | return (
64 | len(list(Path(f"{data['info']['swd']}").glob("**/*"))) > 0
65 | and data["converted"]["static"] + data["converted"]["animated"]
66 | >= data["converted"]["total"]
67 | )
68 |
69 |
70 | def create_converted(pack_name: str, data: dict) -> None:
71 | """Write cache data to a file identified by packName.
72 |
73 | Args:
74 | ----
75 | pack_name (str): name of the sticker pack eg. "DonutTheDog"
76 | data (dict): packName cache data to write to cache
77 |
78 | """
79 | cache = CACHE_DIR / pack_name
80 | cache.write_text(json.dumps(data), encoding="utf-8")
81 |
82 |
83 | def _get_verify_function(version: int) -> Callable[[dict[str, Any]], bool]:
84 | """Get the appropriate cache verification function based on version.
85 |
86 | Args:
87 | ----
88 | version (int): Cache version
89 |
90 | Returns:
91 | -------
92 | Callable[[dict[str, Any]], bool]: Cache verification function
93 |
94 | """
95 | return {
96 | 1: _verify_converted_v1,
97 | }.get(version, _verify_converted_v1)
98 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "tstickers"
3 | version = "2025"
4 | description = "Download sticker packs from Telegram"
5 | authors = [{ name = "FredHappyface" }]
6 | requires-python = ">=3.9"
7 | readme = "README.md"
8 | license = "mit"
9 | classifiers = [
10 | "Development Status :: 5 - Production/Stable",
11 | "Intended Audience :: Developers",
12 | "Intended Audience :: Education",
13 | "Natural Language :: English",
14 | "Operating System :: OS Independent",
15 | "Programming Language :: Python :: Implementation :: CPython",
16 | "Topic :: Software Development :: Libraries :: Python Modules",
17 | "Topic :: Utilities",
18 | "Topic :: Multimedia :: Graphics",
19 | ]
20 | dependencies = [
21 | "emoji>=2.14.1",
22 | "loguru>=0.7.3",
23 | "pillow>=10.4.0",
24 | "requests>=2.32.3",
25 | "requests-cache>=1.2.1",
26 | "rlottie-python>=1.3.6",
27 | ]
28 |
29 | [project.optional-dependencies]
30 | pyrlottie = ["pyrlottie<2026,>=2024.0.1"]
31 |
32 | [project.urls]
33 | Homepage = "https://github.com/FHPythonUtils/TStickers"
34 | Repository = "https://github.com/FHPythonUtils/TStickers"
35 | Documentation = "https://github.com/FHPythonUtils/TStickers/blob/master/README.md"
36 |
37 | [project.scripts]
38 | tstickers = "tstickers:cli"
39 |
40 | [dependency-groups]
41 | dev = [
42 | "coverage>=7.6.12",
43 | "handsdown>=2.1.0",
44 | "pyright>=1.1.394",
45 | "pytest>=8.3.4",
46 | "ruff>=0.9.6",
47 | "safety>=3.3.0",
48 | ]
49 |
50 | [tool.ruff]
51 | line-length = 100
52 | indent-width = 4
53 | target-version = "py38"
54 |
55 | [tool.ruff.lint]
56 | select = ["ALL"]
57 | ignore = [
58 | "ANN101", # type annotation for self in method
59 | "COM812", # enforce trailing comma
60 | "D2", # pydocstyle formatting
61 | "ISC001",
62 | "N", # pep8 naming
63 | "PLR09", # pylint refactor too many
64 | "TCH", # type check blocks
65 | "W191", # ignore this to allow tabs
66 | ]
67 | fixable = ["ALL"]
68 |
69 | [tool.ruff.lint.per-file-ignores]
70 | "**/{tests,docs,tools}/*" = ["D", "S101", "E402"]
71 |
72 | [tool.ruff.lint.flake8-tidy-imports]
73 | ban-relative-imports = "all" # Disallow all relative imports.
74 |
75 | [tool.ruff.format]
76 | indent-style = "tab"
77 | docstring-code-format = true
78 | line-ending = "lf"
79 |
80 | [tool.pyright]
81 | venvPath = "."
82 | venv = ".venv"
83 |
84 | [tool.coverage.run]
85 | branch = true
86 |
87 | [tool.tox]
88 | legacy_tox_ini = """
89 | [tox]
90 | env_list =
91 | py313
92 | py312
93 | py311
94 | py310
95 | py39
96 |
97 | [testenv]
98 | deps =
99 | pytest
100 | commands = pytest tests
101 | """
102 |
103 | [build-system]
104 | requires = ["hatchling"]
105 | build-backend = "hatchling.build"
106 |
--------------------------------------------------------------------------------
/documentation/reference/tstickers/caching.md:
--------------------------------------------------------------------------------
1 | # Caching
2 |
3 | [Tstickers Index](../README.md#tstickers-index) / [Tstickers](./index.md#tstickers) / Caching
4 |
5 | > Auto-generated documentation for [tstickers.caching](../../../tstickers/caching.py) module.
6 |
7 | #### Attributes
8 |
9 | - `cachedSession` - requests_cache: CachedSession('.cache/tstickers.requests.sqlite', backend='sqlite', expire_after=60 * 60 * 12, allowable_codes=(200), allowable_methods=('GET', 'POST'))
10 |
11 |
12 | - [Caching](#caching)
13 | - [_get_verify_function](#_get_verify_function)
14 | - [_verify_converted_v1](#_verify_converted_v1)
15 | - [create_converted](#create_converted)
16 | - [verify_converted](#verify_converted)
17 |
18 | ## _get_verify_function
19 |
20 | [Show source in caching.py:83](../../../tstickers/caching.py#L83)
21 |
22 | Get the appropriate cache verification function based on version.
23 |
24 | #### Arguments
25 |
26 | ----
27 | - `version` *int* - Cache version
28 |
29 | #### Returns
30 |
31 | -------
32 | Callable[[dict[str, Any]], bool]: Cache verification function
33 |
34 | #### Signature
35 |
36 | ```python
37 | def _get_verify_function(version: int) -> Callable[[dict[str, Any]], bool]: ...
38 | ```
39 |
40 |
41 |
42 | ## _verify_converted_v1
43 |
44 | [Show source in caching.py:51](../../../tstickers/caching.py#L51)
45 |
46 | Verify the cache for a packName using cache data.
47 |
48 | #### Arguments
49 |
50 | ----
51 | data (dict[Path, Any]): packName cache data to verify
52 |
53 | #### Returns
54 |
55 | -------
56 | - `bool` - if the converted cache has been verified
57 |
58 | #### Signature
59 |
60 | ```python
61 | def _verify_converted_v1(data: dict[str, Any]) -> bool: ...
62 | ```
63 |
64 |
65 |
66 | ## create_converted
67 |
68 | [Show source in caching.py:70](../../../tstickers/caching.py#L70)
69 |
70 | Write cache data to a file identified by packName.
71 |
72 | #### Arguments
73 |
74 | ----
75 | - `pack_name` *str* - name of the sticker pack eg. "DonutTheDog"
76 | - `data` *dict* - packName cache data to write to cache
77 |
78 | #### Signature
79 |
80 | ```python
81 | def create_converted(pack_name: str, data: dict) -> None: ...
82 | ```
83 |
84 |
85 |
86 | ## verify_converted
87 |
88 | [Show source in caching.py:27](../../../tstickers/caching.py#L27)
89 |
90 | Verify the cache for a packName eg. "DonutTheDog". Uses the cache "version"
91 | to call the verify function for that version.
92 |
93 | #### Arguments
94 |
95 | ----
96 | - `pack_name` *str* - name of the sticker pack eg. "DonutTheDog"
97 |
98 | #### Returns
99 |
100 | -------
101 | - `bool` - if the converted cache has been verified
102 |
103 | #### Signature
104 |
105 | ```python
106 | def verify_converted(pack_name: str) -> bool: ...
107 | ```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | downloads/
2 | env*
3 | tests/data
4 | sticker*.txt
5 | sorted
6 | perf.py
7 | /*.tgs
8 |
9 | uv.lock
10 |
11 | # Byte-compiled / optimized / DLL files
12 | __pycache__/
13 | *.py[cod]
14 | *$py.class
15 |
16 | # C extensions
17 | *.so
18 |
19 | # Distribution / packaging
20 | .Python
21 | build/
22 | develop-eggs/
23 | dist/
24 | downloads/
25 | eggs/
26 | .eggs/
27 | lib/
28 | lib64/
29 | parts/
30 | sdist/
31 | var/
32 | wheels/
33 | pip-wheel-metadata/
34 | share/python-wheels/
35 | *.egg-info/
36 | .installed.cfg
37 | *.egg
38 | MANIFEST
39 |
40 | # PyInstaller
41 | # Usually these files are written by a python script from a template
42 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
43 | *.manifest
44 | *.spec
45 |
46 | # Installer logs
47 | pip-log.txt
48 | pip-delete-this-directory.txt
49 |
50 | # Unit test / coverage reports
51 | htmlcov/
52 | .tox/
53 | .nox/
54 | .coverage
55 | .coverage.*
56 | .cache
57 | nosetests.xml
58 | coverage.xml
59 | *.cover
60 | *.py,cover
61 | .hypothesis/
62 | .pytest_cache/
63 | cover/
64 |
65 | # Translations
66 | *.mo
67 | *.pot
68 |
69 | # Django stuff:
70 | *.log
71 | local_settings.py
72 | db.sqlite3
73 | db.sqlite3-journal
74 |
75 | # Flask stuff:
76 | instance/
77 | .webassets-cache
78 |
79 | # Scrapy stuff:
80 | .scrapy
81 |
82 | # Sphinx documentation
83 | docs/_build/
84 |
85 | # PyBuilder
86 | target/
87 |
88 | # Jupyter Notebook
89 | .ipynb_checkpoints
90 |
91 | # IPython
92 | profile_default/
93 | ipython_config.py
94 |
95 | # pyenv
96 | # For a library or package, you might want to ignore these files since the code is
97 | # intended to run in multiple environments; otherwise, check them in:
98 | # .python-version
99 |
100 | # pipenv
101 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
102 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
103 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
104 | # install all needed dependencies.
105 | #Pipfile.lock
106 |
107 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
108 | __pypackages__/
109 |
110 | # Celery stuff
111 | celerybeat-schedule
112 | celerybeat.pid
113 |
114 | # SageMath parsed files
115 | *.sage.py
116 |
117 | # Environments
118 | .env
119 | .venv
120 | env/
121 | venv/
122 | ENV/
123 | env.bak/
124 | venv.bak/
125 |
126 | # Spyder project settings
127 | .spyderproject
128 | .spyproject
129 |
130 | # Rope project settings
131 | .ropeproject
132 |
133 | # mkdocs documentation
134 | /site
135 |
136 | # mypy
137 | .mypy_cache/
138 | .dmypy.json
139 | dmypy.json
140 |
141 | # Pyre type checker
142 | .pyre/
143 |
144 | # pytype static type analyzer
145 | .pytype/
146 |
--------------------------------------------------------------------------------
/tstickers/convert_rlottie_python.py:
--------------------------------------------------------------------------------
1 | """
2 | Conversion functionality for animated stickers.
3 |
4 | implements the conversion functionality for the rlottie_python backend. exposing a
5 | public function called `convertAnimated`, which is used to perform the conversion.
6 | """
7 |
8 | from __future__ import annotations
9 |
10 | import concurrent.futures
11 | import multiprocessing
12 | from pathlib import Path
13 |
14 | from loguru import logger
15 | from rlottie_python import LottieAnimation
16 |
17 |
18 | def convert_single_tgs(
19 | stckr: Path,
20 | fps: int,
21 | scale: float = 1.0,
22 | formats: set[str] | None = None,
23 | ) -> int:
24 | """Convert a single tgs file.
25 |
26 | :param Path stckr: Path to the sticker
27 | :param int fps: framerate of the converted sticker, affecting optimization and
28 | quality (default: 20)
29 | :param float scale: Scale factor for up/downscaling images, affecting optimization and
30 | quality (default: 1).
31 | :param set[str]|None formats: Set of formats to convert telegram tgs stickers to
32 | (default: {"gif", "webp", "apng"})
33 | :return int: 1 if success
34 |
35 | """
36 | tgs_file = stckr.absolute().as_posix()
37 | if formats is None:
38 | formats = {"gif", "webp", "apng"}
39 |
40 | # Read the animation outside the context manager to avoid issues with pickling
41 | anim = LottieAnimation.from_tgs(path=tgs_file)
42 |
43 | try:
44 | width, height = anim.lottie_animation_get_size()
45 | fps_orig = anim.lottie_animation_get_framerate()
46 | fps = min(fps, fps_orig)
47 | width = int(width * scale)
48 | height = int(height * scale)
49 |
50 | for fmt in formats:
51 | anim.save_animation(tgs_file.replace("tgs", fmt), fps, width=width, height=height)
52 |
53 | finally:
54 | anim.lottie_animation_destroy()
55 |
56 | return 1
57 |
58 |
59 | def convertAnimated(
60 | swd: Path,
61 | threads: int = multiprocessing.cpu_count(),
62 | fps: int = 20,
63 | scale: float = 1,
64 | formats: set[str] | None = None,
65 | ) -> int:
66 | """Convert animated stickers, over a number of threads, at a given framerate, scale and to a
67 | set of formats.
68 |
69 | :param Path swd: The sticker working directory (e.g., downloads/packName).
70 | :param int threads: Number of threads for ProcessPoolExecutor (default: number of
71 | logical processors).
72 | :param int fps: framerate of the converted sticker, affecting optimization and
73 | quality (default: 20)
74 | :param float scale: Scale factor for up/downscaling images, affecting optimization and
75 | quality (default: 1).
76 | :param set[str]|None formats: Set of formats to convert telegram tgs stickers to
77 | (default: {"gif", "webp", "apng"})
78 | :return int: Number of stickers successfully converted.
79 |
80 | """
81 | if formats is None:
82 | formats = {"gif", "webp", "apng"}
83 | converted = 0
84 |
85 | with concurrent.futures.ProcessPoolExecutor(max_workers=threads) as executor:
86 | # Using list comprehension to submit tasks to the executor
87 | future_to_variable = {
88 | executor.submit(convert_single_tgs, stckr, fps, scale, formats): stckr
89 | for stckr in swd.glob("**/*.tgs")
90 | }
91 |
92 | # Wait for all tasks to complete and retrieve results
93 | for future in concurrent.futures.as_completed(future_to_variable):
94 | variable = future_to_variable[future]
95 | try:
96 | converted += future.result()
97 | except Exception as e:
98 | logger.error(f"Error processing {variable}: {e}")
99 |
100 | return converted
101 |
--------------------------------------------------------------------------------
/tstickers/cli.py:
--------------------------------------------------------------------------------
1 | """Download sticker packs from Telegram."""
2 |
3 | from __future__ import annotations
4 |
5 | import argparse
6 | import functools
7 | import importlib.util
8 | import operator
9 | from pathlib import Path
10 | from sys import exit as sysexit
11 |
12 | from loguru import logger
13 |
14 | from tstickers.convert import Backend
15 | from tstickers.manager import StickerManager
16 |
17 | allowed_formats = {"gif", "png", "webp", "apng"}
18 |
19 |
20 | def cli() -> None: # pragma: no cover
21 | """Cli entry point."""
22 | parser = argparse.ArgumentParser("Welcome to TStickers, providing all of your sticker needs")
23 | parser.add_argument(
24 | "-t",
25 | "--token",
26 | help="Pass in a bot token inline",
27 | )
28 | parser.add_argument(
29 | "-p",
30 | "--pack",
31 | action="append",
32 | nargs="+",
33 | help="Pass in a pack url, or pack name",
34 | )
35 | parser.add_argument(
36 | "--fmt",
37 | action="append",
38 | nargs="+",
39 | choices=allowed_formats,
40 | help=f"Formats to convert to {allowed_formats}",
41 | )
42 | parser.add_argument(
43 | "-f",
44 | "--file",
45 | help="Path to file containing pack urls",
46 | )
47 | parser.add_argument(
48 | "--fps",
49 | default=20,
50 | type=int,
51 | help="Set fps for converted animated stickers (this does not affect "
52 | "static images). default=20",
53 | )
54 | parser.add_argument(
55 | "--scale",
56 | default=1,
57 | type=float,
58 | help="Set scale for converted animated stickers (this does not affect "
59 | "static images). default=1.0",
60 | )
61 | parser.add_argument(
62 | "-b",
63 | "--backend",
64 | choices={"rlottie_python", "pyrlottie"},
65 | default="rlottie_python",
66 | help="Specify the convert backend",
67 | )
68 | args = parser.parse_args()
69 |
70 | # Get the token
71 | token = args.token
72 | if token is None:
73 | token = ""
74 | for candidate in [Path.cwd() / "env.txt", Path.cwd() / "env"]:
75 | if candidate.exists():
76 | token = candidate.read_text(encoding="utf-8").strip()
77 | if not token:
78 | logger.error(
79 | '!! Generate a bot token and paste in a file called "env". Send a '
80 | "message to @BotFather to get started"
81 | )
82 | sysexit(1)
83 | # Get the backend
84 | backend = args.backend
85 |
86 | if importlib.util.find_spec(backend) is None:
87 | logger.error(f'!! {backend} is not installed! Install with "pip install {backend}"')
88 | sysexit(2)
89 |
90 | # Get the packs
91 |
92 | packs = []
93 | if args.file:
94 | fp = Path(args.file)
95 | if fp.is_file():
96 | packs = fp.read_text("utf-8").strip().splitlines()
97 |
98 | packs.extend(functools.reduce(operator.iadd, args.pack or [[]], []))
99 | if len(packs) == 0:
100 | logger.info("No packs provided, entering interactive mode...")
101 | while True:
102 | name = input("Enter pack url, or name (hit enter to stop):>").strip()
103 | if name == "":
104 | break
105 | packs.append(name)
106 | packs = [name.split("/")[-1] for name in packs]
107 |
108 | formats = {fmt for sublist in (args.fmt or []) for fmt in sublist}
109 | if len(formats) == 0:
110 | formats = {"png", "webp"}
111 |
112 | downloader = StickerManager(token)
113 | for pack in packs:
114 | logger.info("-" * 60)
115 | _ = downloader.downloadPack(pack)
116 | logger.info("-" * 60)
117 |
118 | backend_map = {"rlottie_python": Backend.RLOTTIE_PYTHON, "pyrlottie": Backend.PYRLOTTIE}
119 |
120 | downloader.convertPack(
121 | pack,
122 | args.fps,
123 | args.scale,
124 | backend=backend_map.get(args.backend, Backend.PYRLOTTIE),
125 | formats=formats,
126 | )
127 |
--------------------------------------------------------------------------------
/documentation/reference/tstickers/convert.md:
--------------------------------------------------------------------------------
1 | # Convert
2 |
3 | [Tstickers Index](../README.md#tstickers-index) / [Tstickers](./index.md#tstickers) / Convert
4 |
5 | > Auto-generated documentation for [tstickers.convert](../../../tstickers/convert.py) module.
6 |
7 | - [Convert](#convert)
8 | - [Backend](#backend)
9 | - [convertAnimated](#convertanimated)
10 | - [convertAnimatedFunc](#convertanimatedfunc)
11 | - [convertStatic](#convertstatic)
12 | - [convertWithPIL](#convertwithpil)
13 |
14 | ## Backend
15 |
16 | [Show source in convert.py:16](../../../tstickers/convert.py#L16)
17 |
18 | Represents different conversion libraries such as pyrlottie, and rlottie-python.
19 |
20 | #### Signature
21 |
22 | ```python
23 | class Backend(IntEnum): ...
24 | ```
25 |
26 |
27 |
28 | ## convertAnimated
29 |
30 | [Show source in convert.py:93](../../../tstickers/convert.py#L93)
31 |
32 | Convert animated stickers, over a number of threads, at a given framerate, scale and to a
33 | set of formats.
34 |
35 | #### Arguments
36 |
37 | - `swd` *Path* - The sticker working directory (e.g., downloads/packName).
38 | - `threads` *int* - Number of threads for ProcessPoolExecutor (default: number of
39 | logical processors).
40 | - `fps` *int* - framerate of the converted sticker, affecting optimization and
41 | quality (default: 20)
42 | - `scale` *float* - Scale factor for up/downscaling images, affecting optimization and
43 | quality (default: 1).
44 | :param set[str]|None formats: Set of formats to convert telegram tgs stickers to
45 | (default: {"gif", "webp", "apng"})
46 |
47 | #### Returns
48 |
49 | Type: *int*
50 | Number of stickers successfully converted.
51 |
52 | #### Signature
53 |
54 | ```python
55 | def convertAnimated(
56 | swd: Path,
57 | threads: int = multiprocessing.cpu_count(),
58 | fps: int = 20,
59 | scale: float = 1,
60 | backend: Backend = Backend.UNDEFINED,
61 | formats: set[str] | None = None,
62 | ) -> int: ...
63 | ```
64 |
65 | #### See also
66 |
67 | - [Backend](#backend)
68 |
69 |
70 |
71 | ## convertAnimatedFunc
72 |
73 | [Show source in convert.py:24](../../../tstickers/convert.py#L24)
74 |
75 | Convert animated stickers with (Base/Backend.UNDEFINED).
76 |
77 | #### Signature
78 |
79 | ```python
80 | def convertAnimatedFunc(
81 | _swd: Path, _threads: int, _fps: int, _scale: float, _formats: set[str] | None
82 | ) -> int: ...
83 | ```
84 |
85 |
86 |
87 | ## convertStatic
88 |
89 | [Show source in convert.py:62](../../../tstickers/convert.py#L62)
90 |
91 | Convert static stickers to specified formats.
92 |
93 | #### Arguments
94 |
95 | - `swd` *Path* - The sticker working directory (e.g., downloads/packName).
96 | - `threads` *int* - Number of threads for ProcessPoolExecutor (default: number of
97 | logical processors).
98 | :param set[str]|None formats: Set of formats to convert telegram webp stickers to
99 | (default: {"gif", "png", "webp", "apng"})
100 |
101 | #### Returns
102 |
103 | Type: *int*
104 | Number of stickers successfully converted.
105 |
106 | #### Signature
107 |
108 | ```python
109 | def convertStatic(
110 | swd: Path, threads: int = 4, formats: set[str] | None = None
111 | ) -> int: ...
112 | ```
113 |
114 |
115 |
116 | ## convertWithPIL
117 |
118 | [Show source in convert.py:44](../../../tstickers/convert.py#L44)
119 |
120 | Convert a webp file to specified formats.
121 |
122 | #### Arguments
123 |
124 | - `input_file` *Path* - path to the input image/ sticker
125 | :param set[str] formats: set of formats
126 |
127 | #### Returns
128 |
129 | Type: *Path*
130 | path of the original image/sticker file
131 |
132 | #### Signature
133 |
134 | ```python
135 | def convertWithPIL(input_file: Path, formats: set[str]) -> Path: ...
136 | ```
--------------------------------------------------------------------------------
/tstickers/convert.py:
--------------------------------------------------------------------------------
1 | """Sticker convert functions used by the downloader."""
2 |
3 | from __future__ import annotations
4 |
5 | import contextlib
6 | import multiprocessing
7 | import time
8 | from concurrent.futures import ThreadPoolExecutor
9 | from enum import IntEnum, auto
10 | from pathlib import Path
11 |
12 | from loguru import logger
13 | from PIL import Image
14 |
15 |
16 | class Backend(IntEnum):
17 | """Represents different conversion libraries such as pyrlottie, and rlottie-python."""
18 |
19 | UNDEFINED = -1
20 | PYRLOTTIE = auto()
21 | RLOTTIE_PYTHON = auto()
22 |
23 |
24 | def convertAnimatedFunc(
25 | _swd: Path,
26 | _threads: int,
27 | _fps: int,
28 | _scale: float,
29 | _formats: set[str] | None,
30 | ) -> int:
31 | """Convert animated stickers with (Base/Backend.UNDEFINED)."""
32 | msg = "Backend could not be loaded"
33 | raise RuntimeError(msg)
34 |
35 |
36 | convertRlottiePython = convertPyRlottie = convertAnimatedFunc
37 |
38 | with contextlib.suppress(ModuleNotFoundError):
39 | from tstickers.convert_rlottie_python import convertAnimated as convertRlottiePython
40 | with contextlib.suppress(ModuleNotFoundError):
41 | from tstickers.convert_pyrlottie import convertAnimated as convertPyRlottie
42 |
43 |
44 | def convertWithPIL(input_file: Path, formats: set[str]) -> Path:
45 | """Convert a webp file to specified formats.
46 |
47 | :param Path input_file: path to the input image/ sticker
48 | :param set[str] formats: set of formats
49 | :return Path: path of the original image/sticker file
50 | """
51 |
52 | img = Image.open(input_file)
53 |
54 | for fmt in formats:
55 | output_file = Path(input_file.as_posix().replace("webp", fmt))
56 | if not output_file.exists():
57 | img.save(output_file)
58 |
59 | return input_file
60 |
61 |
62 | def convertStatic(
63 | swd: Path,
64 | threads: int = 4,
65 | formats: set[str] | None = None,
66 | ) -> int:
67 | """Convert static stickers to specified formats.
68 |
69 |
70 | :param Path swd: The sticker working directory (e.g., downloads/packName).
71 | :param int threads: Number of threads for ProcessPoolExecutor (default: number of
72 | logical processors).
73 | :param set[str]|None formats: Set of formats to convert telegram webp stickers to
74 | (default: {"gif", "png", "webp", "apng"})
75 | :return int: Number of stickers successfully converted.
76 |
77 | """
78 | if formats is None:
79 | formats = {"gif", "png", "webp", "apng"}
80 | webp_files = list((swd / "webp").glob("**/*.webp"))
81 | converted = 0
82 | start = time.time()
83 |
84 | with ThreadPoolExecutor(max_workers=threads) as executor:
85 | results = executor.map(lambda f: convertWithPIL(f, formats), webp_files)
86 | converted = sum(1 for _ in results)
87 |
88 | end = time.time()
89 | logger.info(f"Converted {converted} stickers (static) in {end - start:.3f}s\n")
90 | return converted
91 |
92 |
93 | def convertAnimated(
94 | swd: Path,
95 | threads: int = multiprocessing.cpu_count(),
96 | fps: int = 20,
97 | scale: float = 1,
98 | backend: Backend = Backend.UNDEFINED,
99 | formats: set[str] | None = None,
100 | ) -> int:
101 | """Convert animated stickers, over a number of threads, at a given framerate, scale and to a
102 | set of formats.
103 |
104 | :param Path swd: The sticker working directory (e.g., downloads/packName).
105 | :param int threads: Number of threads for ProcessPoolExecutor (default: number of
106 | logical processors).
107 | :param int fps: framerate of the converted sticker, affecting optimization and
108 | quality (default: 20)
109 | :param float scale: Scale factor for up/downscaling images, affecting optimization and
110 | quality (default: 1).
111 | :param set[str]|None formats: Set of formats to convert telegram tgs stickers to
112 | (default: {"gif", "webp", "apng"})
113 | :return int: Number of stickers successfully converted.
114 |
115 | """
116 | if formats is None:
117 | formats = {"gif", "webp", "apng"}
118 | if backend == Backend.UNDEFINED:
119 | msg = "You must specify a conversion backend"
120 | raise RuntimeError(msg)
121 | start = time.time()
122 |
123 | convertMap = {
124 | Backend.UNDEFINED: convertAnimatedFunc,
125 | Backend.PYRLOTTIE: convertPyRlottie,
126 | Backend.RLOTTIE_PYTHON: convertRlottiePython,
127 | }
128 |
129 | converted = convertMap[backend](swd, threads, fps, scale, formats)
130 |
131 | end = time.time()
132 | logger.info(f"Time taken to convert {converted} stickers (tgs) - {end - start:.3f}s")
133 | logger.info("")
134 | return converted
135 |
--------------------------------------------------------------------------------
/documentation/tutorials/getting-started.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Getting Started with TStickers
4 |
5 | Welcome to the TStickers tutorial! Follow these steps to get up and running with TStickers. This
6 | guide will walk you through setting up a Telegram bot, retrieving sticker pack URLs, and using
7 | TStickers to download and convert stickers.
8 |
9 |
10 | ## Table of Contents
11 |
12 | - [Step 1 - Send a message to @BotFather](#step-1---send-a-message-to-botfather)
13 | - [Step 2 - Create a File Called 'env'](#step-2---create-a-file-called-env)
14 | - [Step 3 - Get the URL of the Telegram Sticker Pack(s)](#step-3---get-the-url-of-the-telegram-sticker-packs)
15 | - [Option 1 - Use a Browser and Search for the Pack](#option-1---use-a-browser-and-search-for-the-pack)
16 | - [Option 2 - Use Telegram](#option-2---use-telegram)
17 | - [Step 4 - Use TStickers](#step-4---use-tstickers)
18 |
19 | ## Step 1 - Send a message to @BotFather
20 |
21 | To start using TStickers, you need to create a Telegram bot. Follow these steps to obtain your bot token:
22 |
23 | 1. **Create a Telegram Account:** If you don’t already have one, download the Telegram app and sign up.
24 |
25 | 2. **Contact @BotFather:** Open Telegram and search for the user `@BotFather`. This is the official bot for managing other bots on Telegram.
26 |
27 | 3. **Create a New Bot:**
28 | - Send the command `/newbot` to @BotFather.
29 | - Follow the prompts to provide a name for your bot (e.g., `TestBot`) and a username (e.g., `test_bot`).
30 |
31 | 4. **Receive Your Token:** @BotFather will reply with a message containing your bot’s API token. Keep this token safe, as you'll need it for the next steps.
32 |
33 |
34 |
35 | ## Step 2 - Create a File Called 'env'
36 |
37 | To store your bot token securely, you need to create a configuration file:
38 |
39 | 1. **Create the File:**
40 | - Create a new text file in the same directory where you'll run TStickers.
41 | - Name the file `env` or `env.txt`.
42 |
43 | 2. **Add Your Token:**
44 | - Open the file and paste your bot token into it.
45 |
46 | Example `env.txt`:
47 | ```txt
48 | 14************
49 | ```
50 |
51 | ## Step 3 - Get the URL of the Telegram Sticker Pack(s)
52 |
53 | To use TStickers, you'll need the URL of the sticker pack(s) you want to download. You can get this URL in two ways:
54 |
55 | ### Option 1 - Use a Browser and Search for the Pack
56 |
57 | 1. **Search for the Sticker Pack:**
58 | - Open your web browser and search for the sticker pack by name (e.g., `Telegram Donut The Dog`).
59 |
60 |
61 |
62 | 2. **Copy the URL:**
63 | - Find the sticker pack link and copy its URL. It should look something like `https://t.me/addstickers/DonutTheDog`.
64 |
65 | ### Option 2 - Use Telegram
66 |
67 | 1. **Find the Sticker Pack:**
68 | - Open the Telegram app, search for the sticker pack, and open it.
69 |
70 | 2. **Copy the Link:**
71 | - Tap on the sticker pack’s name or menu options and select "Share" or "Copy Link" (on mobile devices). The URL will be copied to your clipboard.
72 |
73 | Example URL: `https://t.me/addstickers/DonutTheDog`
74 |
75 |
76 |
77 | ## Step 4 - Use TStickers
78 |
79 | Now you’re ready to use TStickers to download and convert stickers from the URL you obtained:
80 |
81 | 1. **Install TStickers:**
82 | - Run the following command in your terminal:
83 | ```bash
84 | python3 -m pip install tstickers
85 | ```
86 |
87 | 2. **Run TStickers:**
88 | - Start the program by executing:
89 | ```bash
90 | python3 -m tstickers
91 | ```
92 |
93 | 3. **Enter the Sticker Pack URL:**
94 | - When prompted, paste the URL of the sticker pack and press Enter.
95 |
96 | 4. **Check the Output:**
97 | - TStickers will download and convert the stickers. The output will be saved in the `downloads` folder.
98 |
99 | Example output:
100 | ```bash
101 | $ tstickers
102 | Enter sticker_set url (leave blank to stop): https://t.me/addstickers/DonutTheDog
103 | Enter sticker_set url (leave blank to stop):
104 | INFO | ============================================================
105 | INFO | Starting to scrape "DonutTheDog" ..
106 | INFO | Time taken to scrape 31 stickers - 0.044s
107 | INFO |
108 | INFO | ------------------------------------------------------------
109 | INFO | Starting download of "donutthedog" into downloads\donutthedog
110 | INFO | Time taken to download 31 stickers - 0.157s
111 | INFO |
112 | INFO | ------------------------------------------------------------
113 | INFO | -> Cache miss for DonutTheDog!
114 | INFO | Converting stickers "DonutTheDog"...
115 | INFO | Time taken to convert 31 stickers (tgs) - 60.970s
116 | INFO |
117 | INFO | Time taken to convert 31 stickers (webp) - 0.447s
118 | INFO |
119 | INFO | Time taken to convert 62 stickers (total) - 61.434s
120 | INFO |
121 | ```
122 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All major and minor version changes will be documented in this file. Details of
4 | patch-level version changes can be found in [commit messages](../../commits/master).
5 |
6 | ## 2025 - 2025/02/16
7 |
8 | - be opinionated and install `rlottie-python` by default/ regardless
9 | - set default backend to `rlottie-python` in the cli
10 | - add `--fmt` to the cli, where the user can select a list of formats to convert to (default is png
11 | and webp formats)
12 | - replace `--frameskip` with `--fps` as this is more intuitive
13 |
14 | ## 2024.1.3 - 2024/08/26
15 |
16 | - clearly notify user if backend is not installed
17 |
18 | ## 2024.1.2 - 2024/03/25
19 |
20 | - revert 'fix'
21 |
22 | ## 2024.1.1 - 2024/03/24
23 |
24 | - fix `convert_rlottie_python.py`
25 |
26 | ## 2024.1 - 2024/03/22
27 |
28 | - Add '--file' arg for passing in a list of packs
29 | - Implement a custom `demojize` function similar to the `emoji.demojize` function.
30 | However, returns a string of unique keywords in alphabetical order seperated by "_"
31 |
32 | ## 2024 - 2024/03/17
33 |
34 | - add convert backends to give the user a choice of using their preferred tool
35 | - ruff
36 | - code quality improvements
37 |
38 | ## 2022.1.1 - 2022/06/25
39 |
40 | - Fix: add `parents=True` to `Path.mkdir()`
41 | - Update pre-commit
42 |
43 | ## 2022.1 - 2022/02/01
44 |
45 | - Refactor
46 | - Add support for webm stickers
47 |
48 | ## 2022 - 2022/01/24
49 |
50 | - Bump pillow version (CVE-2022-22815, CVE-2022-22816, CVE-2022-22817)
51 | - Update deps
52 | - Add formal tests
53 |
54 | ## 2021.4.4 - 2021/12/10
55 |
56 | - Fix https://github.com/FHPythonUtils/SigStickers/issues/1
57 | - More meaningful error messages
58 |
59 | ## 2021.4.2 - 2021/10/08
60 |
61 | - Implement action='extend' for pre 3.7 eg. `python3 -m tstickers -p pack1 pack2 -p pack3`
62 |
63 | ## 2021.4.1 - 2021/10/04
64 |
65 | - Update function names and docs
66 |
67 | ## 2021.4 - 2021/10/04
68 |
69 | - Added caching functionality using requests_cache and to the converter -
70 | output cache hit/miss to stdout for converter
71 |
72 | ## 2021.3.3 - 2021/10/03
73 |
74 | - Use `asyncio.get_event_loop().run_until_complete` in place of `asyncio.run` for compat
75 | with pyrlottie 2021.1
76 | - Marginal performance improvements with pyrlottie 2021.1 (~3% so may be a fluke?)
77 |
78 | ```txt
79 | Performance testing with https://t.me/addstickers/DonutTheDog on:
80 | OS: Windows 10 (2021/10/03)
81 | CPU: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
82 | RAM: 16gb
83 |
84 | using pyrlottie (lottie2gif.exe + gif2webp.exe)
85 | ~85s (frameskip=0, scale=1) (-5s)
86 | ~47s (frameskip=1, scale=1) (0s)
87 | ~33s (frameskip=2, scale=1) (-1s)
88 | ```
89 |
90 | ## 2021.3.2 - 2021/10/03
91 |
92 | - Produce pngs for animated stickers as in SigStickers
93 | - Tidy up
94 |
95 | ## 2021.3.1 - 2021/10/02
96 |
97 | - Bugfixes in dependency (pyrlottie) for linux/ wsl - so now runs
98 |
99 | ```txt
100 | Performance testing with https://t.me/addstickers/DonutTheDog on:
101 | OS: Windows 10 WSL Ubuntu (2021/10/02)
102 | CPU: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
103 | RAM: 16gb
104 |
105 | using pyrlottie (lottie2gif + gif2webp)
106 | ~61s (frameskip=0, scale=1)
107 | ~27s (frameskip=1, scale=1)
108 | ~18s (frameskip=2, scale=1)
109 |
110 | => Approximately a 3.5x speed improvement for like-to-like image quality
111 | => Approximately a 2.4x speed improvement for improved image quality
112 | ```
113 |
114 | ## 2021.3 - 2021/10/02
115 |
116 | - code quality improvements (eg readability)
117 | - significant performance improvements
118 |
119 | ```txt
120 | Performance testing with https://t.me/addstickers/DonutTheDog on:
121 | OS: Windows 10 (2021/10/02)
122 | CPU: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz
123 | RAM: 16gb
124 |
125 | using pylottie (pyppeteer backend)
126 | ~72s quality=1 (equal to frameskip=2)
127 |
128 | using pyrlottie (lottie2gif.exe + gif2webp.exe)
129 | ~90s (frameskip=0, scale=1)
130 | ~47s (frameskip=1, scale=1)
131 | ~34s (frameskip=2, scale=1)
132 | ~32s (frameskip=1, scale=0.5)
133 |
134 | => Approximately a 2x speed improvement for like-to-like image quality
135 | => Approximately a 1.4x speed improvement for improved image quality
136 | ```
137 |
138 | ## 2021.2.1 - 2021/02/21
139 |
140 | - Fix `ResourceWarning: unclosed ssl.SSLSocket`
141 |
142 | ## 2021.2 - 2021/01/19
143 |
144 | - File names are now the emoji as text followed by the emoji glyph e.g.
145 | "647+smiling_face_with_3_hearts+🥰" followed by the file extension (requires
146 | `emoji` for this)
147 | - If no animated stickers then puppeteer is not launched resulting in a small
148 | speed increase
149 | - Strings double-quoted
150 |
151 | ## 2021.1.5 - 2021/01/13
152 |
153 | - Update `pylottie` for significant speed improvements (animation renders take
154 | approx 2/3 as long)
155 | - Leverage the quality setting exposed by `pylottie` to further improve speed if
156 | desired (quality 0 is fastest, quality 3 is best quality)
157 |
158 | ## 2021.1.3 - 2021/01/13
159 |
160 | - Use `pylottie` to convert animated stickers increasing processing speed by about 10%
161 | - Can pass in packs with `-p` or `--pack`
162 | - Can also pass in the bot token with `-t` or `--token`
163 |
164 | ## 2021.1.2 - 2021/01/07
165 |
166 | - Static stickers are saved as gif in addition to png and webp
167 |
168 | ## 2021.1.1 - 2021/01/07
169 |
170 | - Save animated stickers as webp
171 | - Fixed animation times
172 |
173 | ## 2021.1 - 2021/01/06
174 |
175 | - Added animated sticker support
176 | - These are converted to gif
177 | - No transparency support at this time
178 |
179 | ## 2021.0.1 - 2021/01/04
180 |
181 | - Using pillow for conversions
182 |
183 | ## 2021 - 2021/01/04
184 |
185 | - First release
186 |
--------------------------------------------------------------------------------
/documentation/reference/tstickers/manager.md:
--------------------------------------------------------------------------------
1 | # Manager
2 |
3 | [Tstickers Index](../README.md#tstickers-index) / [Tstickers](./index.md#tstickers) / Manager
4 |
5 | > Auto-generated documentation for [tstickers.manager](../../../tstickers/manager.py) module.
6 |
7 | - [Manager](#manager)
8 | - [Sticker](#sticker)
9 | - [Sticker().__repr__](#sticker()__repr__)
10 | - [Sticker().emojiName](#sticker()emojiname)
11 | - [StickerManager](#stickermanager)
12 | - [StickerManager().convertPack](#stickermanager()convertpack)
13 | - [StickerManager().doAPIReq](#stickermanager()doapireq)
14 | - [StickerManager().downloadPack](#stickermanager()downloadpack)
15 | - [StickerManager().downloadSticker](#stickermanager()downloadsticker)
16 | - [StickerManager().getPack](#stickermanager()getpack)
17 | - [StickerManager().getSticker](#stickermanager()getsticker)
18 | - [demojize](#demojize)
19 |
20 | ## Sticker
21 |
22 | [Show source in manager.py:56](../../../tstickers/manager.py#L56)
23 |
24 | Sticker instance attributes.
25 |
26 | #### Signature
27 |
28 | ```python
29 | class Sticker: ...
30 | ```
31 |
32 | ### Sticker().__repr__
33 |
34 | [Show source in manager.py:64](../../../tstickers/manager.py#L64)
35 |
36 | Get Sticker representation in the form .
37 |
38 | #### Returns
39 |
40 | Type: *str*
41 | representation
42 |
43 | #### Signature
44 |
45 | ```python
46 | def __repr__(self) -> str: ...
47 | ```
48 |
49 | ### Sticker().emojiName
50 |
51 | [Show source in manager.py:71](../../../tstickers/manager.py#L71)
52 |
53 | Get the emoji as a string.
54 |
55 | #### Signature
56 |
57 | ```python
58 | def emojiName(self) -> str: ...
59 | ```
60 |
61 |
62 |
63 | ## StickerManager
64 |
65 | [Show source in manager.py:76](../../../tstickers/manager.py#L76)
66 |
67 | The StickerManager sets up the api and makes requests.
68 |
69 | #### Signature
70 |
71 | ```python
72 | class StickerManager:
73 | def __init__(
74 | self, token: str, session: caching.CachedSession | None = None, threads: int = 4
75 | ) -> None: ...
76 | ```
77 |
78 | ### StickerManager().convertPack
79 |
80 | [Show source in manager.py:248](../../../tstickers/manager.py#L248)
81 |
82 | Convert a downloaded sticker pack given by packName to other formats specified.
83 |
84 | #### Arguments
85 |
86 | - `packName` *str* - name of the pack to convert
87 | - `fps` *int* - framerate of animated stickers, affecting optimization and
88 | quality (default: 20)
89 | - `scale` *float* - Scale factor of animated stickers, for up/downscaling images,
90 | affecting optimization and quality (default: 1).
91 | - `noCache` *bool* - set to true to disable cache. Defaults to False.
92 | - `backend` *Backend* - select the backend to use to convert animated stickers
93 | :param set[str]|None formats: Set of formats to convert telegram tgs stickers to
94 | (default: {"gif", "webp", "apng"})
95 |
96 | #### Signature
97 |
98 | ```python
99 | def convertPack(
100 | self,
101 | packName: str,
102 | fps: int = 20,
103 | scale: float = 1,
104 | noCache: bool = False,
105 | backend: Backend = Backend.UNDEFINED,
106 | formats: set[str] | None = None,
107 | ) -> None: ...
108 | ```
109 |
110 | #### See also
111 |
112 | - [Backend](./convert.md#backend)
113 |
114 | ### StickerManager().doAPIReq
115 |
116 | [Show source in manager.py:107](../../../tstickers/manager.py#L107)
117 |
118 | Use the telegram api.
119 |
120 | #### Arguments
121 |
122 | ----
123 | - `function` *str* - function to execute
124 | params (dict[Any, Any]): function parameters
125 |
126 | #### Raises
127 |
128 | ------
129 | - `RuntimeError` - In the event of a failure
130 |
131 | #### Returns
132 |
133 | -------
134 | - `Optional[dict[Any,` *Any]]* - api response
135 |
136 | #### Signature
137 |
138 | ```python
139 | def doAPIReq(self, function: str, params: dict[Any, Any]) -> dict[Any, Any] | None: ...
140 | ```
141 |
142 | ### StickerManager().downloadPack
143 |
144 | [Show source in manager.py:207](../../../tstickers/manager.py#L207)
145 |
146 | Download a sticker pack.
147 |
148 | #### Arguments
149 |
150 | - `packName` *str* - name of the pack
151 |
152 | #### Returns
153 |
154 | Type: *bool*
155 | success
156 |
157 | #### Signature
158 |
159 | ```python
160 | def downloadPack(self, packName: str) -> bool: ...
161 | ```
162 |
163 | ### StickerManager().downloadSticker
164 |
165 | [Show source in manager.py:195](../../../tstickers/manager.py#L195)
166 |
167 | Download a sticker from the server.
168 |
169 | #### Arguments
170 |
171 | - `path` *Path* - the path to write to
172 | - `link` *str* - the url to the file on the server
173 |
174 | #### Returns
175 |
176 | Type: *int*
177 | path.write_bytes(res.content)
178 |
179 | #### Signature
180 |
181 | ```python
182 | def downloadSticker(self, path: Path, link: str) -> int: ...
183 | ```
184 |
185 | ### StickerManager().getPack
186 |
187 | [Show source in manager.py:161](../../../tstickers/manager.py#L161)
188 |
189 | Get a list of File objects.
190 |
191 | #### Arguments
192 |
193 | ----
194 | - `packName` *str* - name of the pack
195 |
196 | #### Returns
197 |
198 | -------
199 | - `dict[str,` *Any]* - dictionary containing sticker data
200 |
201 | #### Signature
202 |
203 | ```python
204 | def getPack(self, packName: str) -> dict[str, Any] | None: ...
205 | ```
206 |
207 | ### StickerManager().getSticker
208 |
209 | [Show source in manager.py:138](../../../tstickers/manager.py#L138)
210 |
211 | Get sticker info from the server.
212 |
213 | #### Arguments
214 |
215 | ----
216 | fileData (dict[str, Any]): sticker id
217 |
218 | #### Returns
219 |
220 | -------
221 | - [Sticker](#sticker) - Sticker instance
222 |
223 | #### Signature
224 |
225 | ```python
226 | def getSticker(self, fileData: dict[str, Any]) -> Sticker: ...
227 | ```
228 |
229 | #### See also
230 |
231 | - [Sticker](#sticker)
232 |
233 |
234 |
235 | ## demojize
236 |
237 | [Show source in manager.py:22](../../../tstickers/manager.py#L22)
238 |
239 | Similar to the emoji.demojize function.
240 |
241 | However, returns a string of unique keywords in alphabetical order seperated by "_"
242 |
243 | #### Arguments
244 |
245 | - `emoji` *str* - emoji unicode char
246 |
247 | #### Returns
248 |
249 | Type: *str*
250 | returns a string of unique keywords in alphabetical order seperated by "_"
251 |
252 | #### Signature
253 |
254 | ```python
255 | def demojize(emoji: str) -> str: ...
256 | ```
--------------------------------------------------------------------------------
/tstickers/manager.py:
--------------------------------------------------------------------------------
1 | """Sticker download functions used by the module entry point."""
2 |
3 | from __future__ import annotations
4 |
5 | import re
6 | import time
7 | import urllib.parse
8 | from concurrent.futures import ThreadPoolExecutor, as_completed
9 | from dataclasses import dataclass
10 | from json.decoder import JSONDecodeError
11 | from pathlib import Path
12 | from sys import exit as sysexit
13 | from typing import Any
14 |
15 | from emoji import EMOJI_DATA
16 | from loguru import logger
17 |
18 | from tstickers import caching
19 | from tstickers.convert import Backend, convertAnimated, convertStatic
20 |
21 |
22 | def demojize(emoji: str) -> str:
23 | """Similar to the emoji.demojize function.
24 |
25 | However, returns a string of unique keywords in alphabetical order seperated by "_"
26 |
27 | :param str emoji: emoji unicode char
28 | :return str: returns a string of unique keywords in alphabetical order seperated by "_"
29 | """
30 |
31 | def c14n_part(part: str) -> str:
32 | return re.sub(r"_!@#$%^&*'", "_", part).replace("-", "_").lower()
33 |
34 | def merge_parts(parts: set[str]) -> str:
35 | unique_set = set()
36 | for part in parts:
37 | unique_set.update(part.split("_"))
38 |
39 | unique_set.discard("")
40 | unique_set.discard("with")
41 |
42 | result_list = sorted(unique_set)
43 | return "_".join(result_list)
44 |
45 | emoji_data = EMOJI_DATA.get(emoji)
46 | if emoji_data is None:
47 | return "unknown"
48 |
49 | parts = {c14n_part(emoji_data.get("en", "").strip(":"))}
50 | parts.update(c14n_part(x.strip(":")) for x in emoji_data.get("alias", []))
51 |
52 | return merge_parts(parts)
53 |
54 |
55 | @dataclass
56 | class Sticker:
57 | """Sticker instance attributes."""
58 |
59 | name: str = "None"
60 | link: str = "None"
61 | emoji: str = "😀"
62 | fileType: str = "webp"
63 |
64 | def __repr__(self) -> str:
65 | """Get Sticker representation in the form .
66 |
67 | :return str: representation
68 | """
69 | return f""
70 |
71 | def emojiName(self) -> str:
72 | """Get the emoji as a string."""
73 | return demojize(self.emoji)
74 |
75 |
76 | class StickerManager:
77 | """The StickerManager sets up the api and makes requests."""
78 |
79 | def __init__(
80 | self,
81 | token: str,
82 | session: caching.CachedSession | None = None,
83 | threads: int = 4,
84 | ) -> None:
85 | """Telegram Sticker API and provides functions to simplify downloading
86 | new packs.
87 |
88 | :param str token: bot token obtained from @BotFather
89 | :param caching.CachedSession | None session: the requests session to use, defaults to None
90 | :param int threads: number of threads to download over, defaults to 4
91 | """
92 | self.threads = threads
93 | self.token = token
94 | self.cwd = Path("downloads")
95 | if session is None:
96 | self.session = caching.cachedSession
97 | else:
98 | self.session = session
99 | self.api = f"https://api.telegram.org/bot{self.token}/"
100 | verify = self.doAPIReq("getMe", {})
101 | if verify is not None and verify["ok"]:
102 | pass
103 | else:
104 | logger.info("Invalid token.")
105 | sysexit(1)
106 |
107 | def doAPIReq(self, function: str, params: dict[Any, Any]) -> dict[Any, Any] | None:
108 | """Use the telegram api.
109 |
110 | Args:
111 | ----
112 | function (str): function to execute
113 | params (dict[Any, Any]): function parameters
114 |
115 | Raises:
116 | ------
117 | RuntimeError: In the event of a failure
118 |
119 | Returns:
120 | -------
121 | Optional[dict[Any, Any]]: api response
122 |
123 | """
124 | urlParams = "?" + urllib.parse.urlencode(params)
125 | res = self.session.get(f"{self.api}{function}{urlParams}")
126 | try:
127 | res = res.json()
128 | except JSONDecodeError:
129 | res = {"ok": False, "raw": res}
130 | if res["ok"]:
131 | return res
132 |
133 | logger.info(
134 | f'API method {function} with params {params} failed. Error: "{res["description"]}"'
135 | )
136 | return None
137 |
138 | def getSticker(self, fileData: dict[str, Any]) -> Sticker:
139 | """Get sticker info from the server.
140 |
141 | Args:
142 | ----
143 | fileData (dict[str, Any]): sticker id
144 |
145 | Returns:
146 | -------
147 | Sticker: Sticker instance
148 |
149 | """
150 | info = self.doAPIReq("getFile", {"file_id": fileData["file_id"]})
151 | if info is not None:
152 | filePath = info["result"]["file_path"]
153 | return Sticker(
154 | name=filePath.split("/")[-1],
155 | link=f"https://api.telegram.org/file/bot{self.token}/{filePath}",
156 | emoji=fileData["emoji"],
157 | fileType=filePath.split(".")[-1],
158 | )
159 | return Sticker()
160 |
161 | def getPack(self, packName: str) -> dict[str, Any] | None:
162 | """Get a list of File objects.
163 |
164 | Args:
165 | ----
166 | packName (str): name of the pack
167 |
168 | Returns:
169 | -------
170 | dict[str, Any]: dictionary containing sticker data
171 |
172 | """
173 | params = {"name": packName}
174 | res = self.doAPIReq("getStickerSet", params)
175 | if res is None:
176 | return None
177 | stickers = res["result"]["stickers"]
178 | files = []
179 |
180 | logger.info(f'Starting to scrape "{packName}" ..')
181 | start = time.time()
182 | with ThreadPoolExecutor(max_workers=self.threads) as executor:
183 | futures = [executor.submit(self.getSticker, i) for i in stickers]
184 | files = [i.result() for i in as_completed(futures)]
185 | end = time.time()
186 | logger.info(f"Time taken to scrape {len(files)} stickers - {end - start:.3f}s")
187 | logger.info("")
188 |
189 | return {
190 | "name": res["result"]["name"].lower(),
191 | "title": res["result"]["title"],
192 | "files": files,
193 | }
194 |
195 | def downloadSticker(self, path: Path, link: str) -> int:
196 | """Download a sticker from the server.
197 |
198 | :param Path path: the path to write to
199 | :param str link: the url to the file on the server
200 |
201 | :return int: path.write_bytes(res.content)
202 |
203 | """
204 | path.parent.mkdir(parents=True, exist_ok=True)
205 | return path.write_bytes(self.session.get(link).content)
206 |
207 | def downloadPack(self, packName: str) -> bool:
208 | """Download a sticker pack.
209 |
210 | :param str packName: name of the pack
211 | :return bool: success
212 |
213 | """
214 |
215 | stickerPack = self.getPack(packName)
216 | if stickerPack is None:
217 | return False
218 |
219 | swd: Path = self.cwd / packName
220 | swd.mkdir(parents=True, exist_ok=True)
221 |
222 | downloads = 0
223 | logger.info(f'Starting download of "{packName}" into {swd}')
224 | start = time.time()
225 | with ThreadPoolExecutor(max_workers=self.threads) as executor:
226 | futures = [
227 | executor.submit(
228 | self.downloadSticker,
229 | swd
230 | / sticker.fileType
231 | / (
232 | f"{sticker.name.split('_')[-1].split('.')[0]}+{sticker.emojiName()}"
233 | f".{sticker.fileType}"
234 | ),
235 | link=sticker.link,
236 | )
237 | for sticker in stickerPack["files"]
238 | ]
239 | for i in as_completed(futures):
240 | downloads += 1 if i.result() > 0 else 0
241 | self.session.close()
242 |
243 | end = time.time()
244 | logger.info(f"Time taken to download {downloads} stickers - {end - start:.3f}s")
245 | logger.info("")
246 | return downloads == stickerPack["files"]
247 |
248 | def convertPack(
249 | self,
250 | packName: str,
251 | fps: int = 20,
252 | scale: float = 1,
253 | *,
254 | noCache: bool = False,
255 | backend: Backend = Backend.UNDEFINED,
256 | formats: set[str] | None = None,
257 | ) -> None:
258 | """Convert a downloaded sticker pack given by packName to other formats specified.
259 |
260 | :param str packName: name of the pack to convert
261 | :param int fps: framerate of animated stickers, affecting optimization and
262 | quality (default: 20)
263 | :param float scale: Scale factor of animated stickers, for up/downscaling images,
264 | affecting optimization and quality (default: 1).
265 | :param bool noCache: set to true to disable cache. Defaults to False.
266 | :param Backend backend: select the backend to use to convert animated stickers
267 | :param set[str]|None formats: Set of formats to convert telegram tgs stickers to
268 | (default: {"gif", "webp", "apng"})
269 |
270 | """
271 | if formats is None:
272 | formats = {"gif", "png", "webp", "apng"}
273 | if not noCache and caching.verify_converted(packName):
274 | return
275 | swd = self.cwd / packName
276 |
277 | start = time.time()
278 | total = len([x for x in swd.glob("**/*") if x.is_file()])
279 |
280 | logger.info(f'Converting stickers "{packName}"...')
281 |
282 | for fmt in formats:
283 | (swd / fmt).mkdir(parents=True, exist_ok=True)
284 |
285 | animatedFormats = formats.copy()
286 | animatedFormats.discard("png")
287 |
288 | converted = convertedTgs = convertAnimated(
289 | swd, self.threads, fps=fps, scale=scale, backend=backend, formats=animatedFormats
290 | )
291 |
292 | convertedWebp = convertStatic(swd, self.threads, formats=formats)
293 | converted += convertedWebp
294 |
295 | end = time.time()
296 | logger.info(f"Time taken to convert {converted} stickers (total) - {end - start:.3f}s")
297 | logger.info("")
298 |
299 | caching.create_converted(
300 | packName,
301 | data={
302 | "version": 2,
303 | "info": {
304 | "packName": packName,
305 | "fps": fps,
306 | "scale": scale,
307 | "swd": swd.as_posix(),
308 | },
309 | "converted": {
310 | "static": convertedWebp,
311 | "animated": convertedTgs,
312 | "total": total,
313 | },
314 | },
315 | )
316 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](../../)
2 | [](../../issues)
3 | [](/LICENSE.md)
4 | [](../../commits/master)
5 | [](../../commits/master)
6 | [](https://pypistats.org/packages/tstickers)
7 | [](https://pepy.tech/project/tstickers)
8 | [](https://pypi.org/project/tstickers)
9 |
10 |
11 | # TStickers - Telegram Sticker Downloader
12 |
13 |
14 |
15 | The `tstickers` package provides functionality for downloading and converting sticker packs from https://t.me/addstickers. Download stickers, and convert them to multiple formats, with caching the converted stickers for faster retrieval.
16 |
17 | - [Basic Use](#basic-use)
18 | - [Using](#using)
19 | - [Help](#help)
20 | - [Documentation](#documentation)
21 | - [Formats](#formats)
22 | - [Install With PIP](#install-with-pip)
23 | - [Language information](#language-information)
24 | - [Built for](#built-for)
25 | - [Install Python on Windows](#install-python-on-windows)
26 | - [Chocolatey](#chocolatey)
27 | - [Windows - Python.org](#windows---pythonorg)
28 | - [Install Python on Linux](#install-python-on-linux)
29 | - [Apt](#apt)
30 | - [Dnf](#dnf)
31 | - [Install Python on MacOS](#install-python-on-macos)
32 | - [Homebrew](#homebrew)
33 | - [MacOS - Python.org](#macos---pythonorg)
34 | - [How to run](#how-to-run)
35 | - [Windows](#windows)
36 | - [Linux/ MacOS](#linux-macos)
37 | - [Building](#building)
38 | - [Testing](#testing)
39 | - [Download Project](#download-project)
40 | - [Clone](#clone)
41 | - [Using The Command Line](#using-the-command-line)
42 | - [Using GitHub Desktop](#using-github-desktop)
43 | - [Download Zip File](#download-zip-file)
44 | - [Community Files](#community-files)
45 | - [Licence](#licence)
46 | - [Changelog](#changelog)
47 | - [Code of Conduct](#code-of-conduct)
48 | - [Contributing](#contributing)
49 | - [Security](#security)
50 | - [Support](#support)
51 | - [Rationale](#rationale)
52 |
53 | ## Basic Use
54 |
55 | https://t.me/addstickers/DonutTheDog
56 |
57 | - NOTE: You need a telegram bot token to make use of the script. Generate a bot
58 | token and paste in a file called 'env'. Send a message to @BotFather to get started.
59 | - Create a file called 'env' (or env.txt) and paste your token
60 | - Get the URL of the telegram sticker pack
61 | - Run the program `python -m tstickers`
62 | - Enter the URL of the sticker pack
63 | - Get the output in the `downloads` folder.
64 |
65 | More info at [Tutorials](/documentation/tutorials)
66 |
67 | ## Using
68 |
69 | 1. Get the URL of the Signal sticker pack. In the form https://t.me/addstickers
70 |
71 | 2. Pass in multiple packs from the commandline with `-p/--pack`
72 |
73 | ```bash
74 | $ tstickers --pack https://t.me/addstickers/DonutTheDog
75 | INFO | ============================================================
76 | INFO | Starting to scrape "DonutTheDog" ..
77 | INFO | Time taken to scrape 31 stickers - 0.044s
78 | INFO |
79 | INFO | ------------------------------------------------------------
80 | INFO | Starting download of "donutthedog" into downloads\donutthedog
81 | INFO | Time taken to download 31 stickers - 0.157s
82 | INFO |
83 | INFO | ------------------------------------------------------------
84 | INFO | -> Cache miss for DonutTheDog!
85 | INFO | Converting stickers "DonutTheDog"...
86 | INFO | Time taken to convert 31 stickers (tgs) - 60.970s
87 | INFO |
88 | INFO | Time taken to convert 31 stickers (webp) - 0.447s
89 | INFO |
90 | INFO | Time taken to convert 62 stickers (total) - 61.434s
91 | INFO |
92 |
93 | ```
94 |
95 | 3. OR. Enter the URL of the sticker pack when prompted
96 |
97 | ```bash
98 | $ python -m tstickers
99 | Enter sticker_set URL (leave blank to stop): https://t.me/addstickers/DonutTheDog
100 | Enter sticker_set URL (leave blank to stop):
101 | INFO | ============================================================
102 | INFO | Starting to scrape "DonutTheDog" ..
103 | INFO | Time taken to scrape 31 stickers - 0.044s
104 | INFO |
105 | INFO | ------------------------------------------------------------
106 | INFO | Starting download of "donutthedog" into downloads\donutthedog
107 | INFO | Time taken to download 31 stickers - 0.157s
108 | INFO |
109 | INFO | ------------------------------------------------------------
110 | ...
111 | ```
112 |
113 | 4. Get the output in the `downloads` folder.
114 |
115 | ```powershell
116 | $ ls .\downloads\donutthedog\
117 |
118 | Mode LastWriteTime Length Name
119 | ---- ------------- ------ ----
120 | d----- 17/03/2024 17꞉00 apng
121 | d----- 17/03/2024 17꞉01 gif
122 | d----- 17/03/2024 17꞉06 png
123 | d----- 17/03/2024 17꞉00 tgs
124 | d----- 17/03/2024 17꞉02 webp
125 | ```
126 |
127 | ## Help
128 |
129 | ```bash
130 | $ python -m tstickers --help
131 | usage: Welcome to TStickers, providing all of your sticker needs [-h] [-t TOKEN] [-p PACK [PACK ...]]
132 | [--frameskip FRAMESKIP] [--scale SCALE]
133 | [-b {rlottie-python,pyrlottie}]
134 |
135 | options:
136 | -h, --help show this help message and exit
137 | -t TOKEN, --token TOKEN
138 | Pass in a bot token inline
139 | -p PACK [PACK ...], --pack PACK [PACK ...]
140 | Pass in a pack url inline
141 | --frameskip FRAMESKIP
142 | Set frameskip. default=1
143 | --scale SCALE Set scale. default=1.0
144 | -b {rlottie-python,pyrlottie}, --backend {rlottie-python,pyrlottie}
145 | Specify the convert backend
146 | ```
147 |
148 | ## Documentation
149 |
150 | A high-level overview of how the documentation is organized organized will help you know
151 | where to look for certain things:
152 |
153 | - [Tutorials](/documentation/tutorials) take you by the hand through a series of steps to get
154 | started using the software. Start here if you’re new.
155 | - The [Technical Reference](/documentation/reference) documents APIs and other aspects of the
156 | machinery. This documentation describes how to use the classes and functions at a lower level
157 | and assume that you have a good high-level understanding of the software.
158 |
162 |
163 | ## Formats
164 |
165 | | Format | Static | Animated | Animated (webm) |
166 | | ------ | ------ | -------- | --------------- |
167 | | .gif | ✔ | ✔ | ❌ |
168 | | .png | ✔ | ✔+ | ❌ |
169 | | .tgs | ❌ | ✔ | ❌ |
170 | | .webp | ✔ | ✔ | ❌ |
171 | | .webm | ❌ | ❌ | ✔ |
172 |
173 | ```txt
174 | + First frame of animated image only
175 | ```
176 |
177 | Note that static images can fail to save as .gif occasionally in testing
178 |
179 | ## Install With PIP
180 |
181 | ```python
182 | pip install tstickers
183 | ```
184 |
185 | Head to https://pypi.org/project/tstickers/ for more info
186 |
187 | ## Language information
188 |
189 | ### Built for
190 |
191 | This program has been written for Python versions 3.8 - 3.11 and has been tested with both 3.8 and
192 | 3.11
193 |
194 | ## Install Python on Windows
195 |
196 | ### Chocolatey
197 |
198 | ```powershell
199 | choco install python
200 | ```
201 |
202 | ### Windows - Python.org
203 |
204 | To install Python, go to https://www.python.org/downloads/windows/ and download the latest
205 | version.
206 |
207 | ## Install Python on Linux
208 |
209 | ### Apt
210 |
211 | ```bash
212 | sudo apt install python3.x
213 | ```
214 |
215 | ### Dnf
216 |
217 | ```bash
218 | sudo dnf install python3.x
219 | ```
220 |
221 | ## Install Python on MacOS
222 |
223 | ### Homebrew
224 |
225 | ```bash
226 | brew install python@3.x
227 | ```
228 |
229 | ### MacOS - Python.org
230 |
231 | To install Python, go to https://www.python.org/downloads/macos/ and download the latest
232 | version.
233 |
234 | ## How to run
235 |
236 | ### Windows
237 |
238 | - Module
239 | `py -3.x -m [module]` or `[module]` (if module installs a script)
240 |
241 | - File
242 | `py -3.x [file]` or `./[file]`
243 |
244 | ### Linux/ MacOS
245 |
246 | - Module
247 | `python3.x -m [module]` or `[module]` (if module installs a script)
248 |
249 | - File
250 | `python3.x [file]` or `./[file]`
251 |
252 | ## Building
253 |
254 | This project uses https://github.com/FHPythonUtils/FHMake to automate most of the building. This
255 | command generates the documentation, updates the requirements.txt and builds the library artefacts
256 |
257 | Note the functionality provided by fhmake can be approximated by the following
258 |
259 | ```sh
260 | handsdown --cleanup -o documentation/reference
261 | poetry export -f requirements.txt --output requirements.txt
262 | poetry export -f requirements.txt --with dev --output requirements_optional.txt
263 | poetry build
264 | ```
265 |
266 | `fhmake audit` can be run to perform additional checks
267 |
268 | ## Testing
269 |
270 | For testing with the version of python used by poetry use
271 |
272 | ```sh
273 | poetry run pytest
274 | ```
275 |
276 | Alternatively use `tox` to run tests over python 3.8 - 3.11
277 |
278 | ```sh
279 | tox
280 | ```
281 |
282 | ## Download Project
283 |
284 | ### Clone
285 |
286 | #### Using The Command Line
287 |
288 | 1. Press the Clone or download button in the top right
289 | 2. Copy the URL (link)
290 | 3. Open the command line and change directory to where you wish to
291 | clone to
292 | 4. Type 'git clone' followed by URL in step 2
293 |
294 | ```bash
295 | git clone https://github.com/FHPythonUtils/TStickers
296 | ```
297 |
298 | More information can be found at
299 | https://help.github.com/en/articles/cloning-a-repository
300 |
301 | #### Using GitHub Desktop
302 |
303 | 1. Press the Clone or download button in the top right
304 | 2. Click open in desktop
305 | 3. Choose the path for where you want and click Clone
306 |
307 | More information can be found at
308 | https://help.github.com/en/desktop/contributing-to-projects/cloning-a-repository-from-github-to-github-desktop
309 |
310 | ### Download Zip File
311 |
312 | 1. Download this GitHub repository
313 | 2. Extract the zip archive
314 | 3. Copy/ move to the desired location
315 |
316 | ## Community Files
317 |
318 | ### Licence
319 |
320 | MIT License
321 | Copyright (c) FredHappyface
322 | (See the [LICENSE](/LICENSE.md) for more information.)
323 |
324 | ### Changelog
325 |
326 | See the [Changelog](/CHANGELOG.md) for more information.
327 |
328 | ### Code of Conduct
329 |
330 | Online communities include people from many backgrounds. The *Project*
331 | contributors are committed to providing a friendly, safe and welcoming
332 | environment for all. Please see the
333 | [Code of Conduct](https://github.com/FHPythonUtils/.github/blob/master/CODE_OF_CONDUCT.md)
334 | for more information.
335 |
336 | ### Contributing
337 |
338 | Contributions are welcome, please see the
339 | [Contributing Guidelines](https://github.com/FHPythonUtils/.github/blob/master/CONTRIBUTING.md)
340 | for more information.
341 |
342 | ### Security
343 |
344 | Thank you for improving the security of the project, please see the
345 | [Security Policy](https://github.com/FHPythonUtils/.github/blob/master/SECURITY.md)
346 | for more information.
347 |
348 | ### Support
349 |
350 | Thank you for using this project, I hope it is of use to you. Please be aware that
351 | those involved with the project often do so for fun along with other commitments
352 | (such as work, family, etc). Please see the
353 | [Support Policy](https://github.com/FHPythonUtils/.github/blob/master/SUPPORT.md)
354 | for more information.
355 |
356 | ### Rationale
357 |
358 | The rationale acts as a guide to various processes regarding projects such as
359 | the versioning scheme and the programming styles used. Please see the
360 | [Rationale](https://github.com/FHPythonUtils/.github/blob/master/RATIONALE.md)
361 | for more information.
362 |
--------------------------------------------------------------------------------