snapshot current state before gitea sync
This commit is contained in:
10
StiggerDLBot/.claude/settings.local.json
Normal file
10
StiggerDLBot/.claude/settings.local.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(/Users/felixfoertsch/Developer/TelegramStickersDownloaderHTTP/.venv/bin/pip list:*)",
|
||||
"Bash(.venv/bin/pip install:*)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(.venv/bin/python:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
10
StiggerDLBot/.gitignore
vendored
Normal file
10
StiggerDLBot/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
.env
|
||||
.venv/
|
||||
cache/
|
||||
downloads/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.DS_Store
|
||||
.idea/
|
||||
.cache/
|
||||
.vscode/
|
||||
0
StiggerDLBot/app/__init__.py
Normal file
0
StiggerDLBot/app/__init__.py
Normal file
14
StiggerDLBot/app/config.py
Normal file
14
StiggerDLBot/app/config.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
bot_token: str
|
||||
downloads_dir: Path = Path("downloads")
|
||||
|
||||
|
||||
settings = Settings()
|
||||
20
StiggerDLBot/app/main.py
Normal file
20
StiggerDLBot/app/main.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.routers import stickersets
|
||||
|
||||
app = FastAPI(title="Telegram Stickers API", root_path="/StiggerDLBot")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["GET"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(stickersets.router)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "ok"}
|
||||
17
StiggerDLBot/app/models.py
Normal file
17
StiggerDLBot/app/models.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class StickerResponse(BaseModel):
|
||||
id: str
|
||||
emoji: str
|
||||
emoji_name: str
|
||||
is_animated: bool
|
||||
png_url: str
|
||||
gif_url: str | None = None
|
||||
|
||||
|
||||
class StickerSetResponse(BaseModel):
|
||||
name: str
|
||||
title: str
|
||||
sticker_count: int
|
||||
stickers: list[StickerResponse]
|
||||
0
StiggerDLBot/app/routers/__init__.py
Normal file
0
StiggerDLBot/app/routers/__init__.py
Normal file
31
StiggerDLBot/app/routers/stickersets.py
Normal file
31
StiggerDLBot/app/routers/stickersets.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from app.models import StickerSetResponse
|
||||
from app.services import sticker_service
|
||||
|
||||
router = APIRouter(prefix="/api/stickersets")
|
||||
|
||||
|
||||
@router.get("/{pack_name}", response_model=StickerSetResponse)
|
||||
def get_sticker_set(pack_name: str):
|
||||
result = sticker_service.get_sticker_set(pack_name)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="Sticker pack not found")
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/{pack_name}/stickers/{sticker_id}.png")
|
||||
def get_sticker_png(pack_name: str, sticker_id: str):
|
||||
path = sticker_service.get_sticker_file(pack_name, sticker_id, "png")
|
||||
if path is None:
|
||||
raise HTTPException(status_code=404, detail="Sticker not found")
|
||||
return FileResponse(path, media_type="image/png")
|
||||
|
||||
|
||||
@router.get("/{pack_name}/stickers/{sticker_id}.gif")
|
||||
def get_sticker_gif(pack_name: str, sticker_id: str):
|
||||
path = sticker_service.get_sticker_file(pack_name, sticker_id, "gif")
|
||||
if path is None:
|
||||
raise HTTPException(status_code=404, detail="Sticker not found")
|
||||
return FileResponse(path, media_type="image/gif")
|
||||
0
StiggerDLBot/app/services/__init__.py
Normal file
0
StiggerDLBot/app/services/__init__.py
Normal file
125
StiggerDLBot/app/services/sticker_service.py
Normal file
125
StiggerDLBot/app/services/sticker_service.py
Normal file
@@ -0,0 +1,125 @@
|
||||
from pathlib import Path
|
||||
|
||||
from tstickers.convert import Backend
|
||||
from tstickers.manager import StickerManager
|
||||
|
||||
from app.config import settings
|
||||
from app.models import StickerResponse, StickerSetResponse
|
||||
|
||||
_manager: StickerManager | None = None
|
||||
|
||||
|
||||
def _get_manager() -> StickerManager:
|
||||
global _manager
|
||||
if _manager is None:
|
||||
_manager = StickerManager(settings.bot_token)
|
||||
return _manager
|
||||
|
||||
|
||||
def _pack_is_cached(pack_name: str) -> bool:
|
||||
png_dir = settings.downloads_dir / pack_name / "png"
|
||||
gif_dir = settings.downloads_dir / pack_name / "gif"
|
||||
has_png = png_dir.exists() and any(png_dir.iterdir())
|
||||
has_gif = gif_dir.exists() and any(gif_dir.iterdir())
|
||||
return has_png or has_gif
|
||||
|
||||
|
||||
def get_sticker_set(pack_name: str) -> StickerSetResponse | None:
|
||||
"""Fetch a sticker pack, download and convert if not cached."""
|
||||
manager = _get_manager()
|
||||
|
||||
# Get pack metadata (uses requests-cache internally)
|
||||
pack = manager.getPack(pack_name)
|
||||
if pack is None:
|
||||
return None
|
||||
|
||||
actual_name = pack["name"]
|
||||
|
||||
if not _pack_is_cached(actual_name):
|
||||
# downloadPack calls getPack internally again, but it's cached
|
||||
manager.downloadPack(actual_name)
|
||||
manager.convertPack(actual_name, formats={"gif", "png"}, backend=Backend.RLOTTIE_PYTHON)
|
||||
|
||||
# Build response from files on disk
|
||||
png_dir = settings.downloads_dir / actual_name / "png"
|
||||
gif_dir = settings.downloads_dir / actual_name / "gif"
|
||||
|
||||
# Build a lookup of sticker metadata from the pack files
|
||||
sticker_lookup: dict[str, dict] = {}
|
||||
for sticker in pack["files"]:
|
||||
sticker_id = sticker.name.split("_")[-1].split(".")[0]
|
||||
sticker_lookup[sticker_id] = {
|
||||
"emoji": sticker.emoji,
|
||||
"emoji_name": sticker.emojiName(),
|
||||
"is_animated": sticker.fileType == "tgs",
|
||||
}
|
||||
|
||||
stickers: list[StickerResponse] = []
|
||||
if png_dir.exists():
|
||||
for png_file in sorted(png_dir.iterdir()):
|
||||
sticker_id = png_file.stem.split("+")[0]
|
||||
emoji_name = png_file.stem.split("+")[1] if "+" in png_file.stem else ""
|
||||
|
||||
meta = sticker_lookup.get(sticker_id, {})
|
||||
emoji = meta.get("emoji", "")
|
||||
is_animated = meta.get("is_animated", False)
|
||||
if not emoji_name:
|
||||
emoji_name = meta.get("emoji_name", "")
|
||||
|
||||
gif_url = None
|
||||
gif_file = gif_dir / f"{png_file.stem}.gif"
|
||||
if gif_file.exists():
|
||||
gif_url = f"/api/stickersets/{actual_name}/stickers/{sticker_id}.gif"
|
||||
|
||||
stickers.append(
|
||||
StickerResponse(
|
||||
id=sticker_id,
|
||||
emoji=emoji,
|
||||
emoji_name=emoji_name,
|
||||
is_animated=is_animated,
|
||||
png_url=f"/api/stickersets/{actual_name}/stickers/{sticker_id}.png",
|
||||
gif_url=gif_url,
|
||||
)
|
||||
)
|
||||
|
||||
# Also check for GIF-only stickers (animated TGS that don't produce PNG)
|
||||
if gif_dir.exists():
|
||||
existing_ids = {s.id for s in stickers}
|
||||
for gif_file in sorted(gif_dir.iterdir()):
|
||||
sticker_id = gif_file.stem.split("+")[0]
|
||||
if sticker_id in existing_ids:
|
||||
continue
|
||||
emoji_name = gif_file.stem.split("+")[1] if "+" in gif_file.stem else ""
|
||||
meta = sticker_lookup.get(sticker_id, {})
|
||||
|
||||
stickers.append(
|
||||
StickerResponse(
|
||||
id=sticker_id,
|
||||
emoji=meta.get("emoji", ""),
|
||||
emoji_name=emoji_name or meta.get("emoji_name", ""),
|
||||
is_animated=True,
|
||||
png_url=f"/api/stickersets/{actual_name}/stickers/{sticker_id}.png",
|
||||
gif_url=f"/api/stickersets/{actual_name}/stickers/{sticker_id}.gif",
|
||||
)
|
||||
)
|
||||
|
||||
return StickerSetResponse(
|
||||
name=actual_name,
|
||||
title=pack["title"],
|
||||
sticker_count=len(stickers),
|
||||
stickers=stickers,
|
||||
)
|
||||
|
||||
|
||||
def get_sticker_file(pack_name: str, sticker_id: str, fmt: str) -> Path | None:
|
||||
"""Return the path to a converted sticker file, or None if not found."""
|
||||
fmt_dir = settings.downloads_dir / pack_name / fmt
|
||||
if not fmt_dir.exists():
|
||||
return None
|
||||
|
||||
# Files are named like: {id}+{emoji_name}.{ext}
|
||||
for f in fmt_dir.iterdir():
|
||||
if f.stem.split("+")[0] == sticker_id:
|
||||
return f
|
||||
|
||||
return None
|
||||
10
StiggerDLBot/botfather.md
Normal file
10
StiggerDLBot/botfather.md
Normal file
@@ -0,0 +1,10 @@
|
||||
Name: StiggerDLBot
|
||||
Username: StiggerDLBot
|
||||
|
||||
Done! Congratulations on your new bot. You will find it at t.me/StiggerDLBot. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this.
|
||||
|
||||
Use this token to access the HTTP API:
|
||||
8247171504:AAE5o32Zv1B6N5VXh5S9m6H97LTB6c7Vu1s
|
||||
Keep your token secure and store it safely, it can be used by anyone to control your bot.
|
||||
|
||||
For a description of the Bot API, see this page: https://core.telegram.org/bots/api
|
||||
6
StiggerDLBot/requirements.txt
Normal file
6
StiggerDLBot/requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
pydantic-settings
|
||||
python-dotenv
|
||||
tstickers
|
||||
Pillow
|
||||
Reference in New Issue
Block a user