fix: критические баги, FurTok картинки, image proxy, gitignore

- Удалён дубликат handle_uwu_cmd в uwu.py
- Исправлен UnboundLocalError (delta/next_dt) в main.py
- FurTok: поддержка картинок при кастомном запросе
- Добавлен image proxy /api/proxy-image для e621
- CSS: абсолютное позиционирование медиа в furtok-card
- .gitignore: docker-compose.override.yaml
This commit is contained in:
Danil 2026-04-14 22:49:49 +03:00
parent dd36d8f1c3
commit 3b29648a8c
5 changed files with 54 additions and 6 deletions

1
.gitignore vendored
View file

@ -22,3 +22,4 @@ db/
AI/models/ AI/models/
main_legacy.py main_legacy.py
docker-compose.override.yaml

View file

@ -95,10 +95,15 @@ async def fetch_uwu_post(tags: str) -> dict:
} }
async def fetch_furtok_feed(safe: bool = True, page: int = 1, extra_tags: str = "") -> list[dict]: async def fetch_furtok_feed(safe: bool = True, page: int = 1, extra_tags: str = "") -> list[dict]:
is_custom = bool(extra_tags and extra_tags.strip())
rating = "rating:safe" if safe else "-rating:safe" rating = "rating:safe" if safe else "-rating:safe"
if is_custom:
# Кастомный запрос: без принудительного animated, поддержка картинок
tags = f"{extra_tags.strip()} order:random score:>200"
else:
# Стандартная лента: только видео
tags = f"{rating} animated order:random score:>200" tags = f"{rating} animated order:random score:>200"
if extra_tags and extra_tags.strip():
tags = f"{extra_tags.strip()} animated order:random score:>200"
url = "https://e621.net/posts.json" url = "https://e621.net/posts.json"
params = { params = {
@ -122,18 +127,24 @@ async def fetch_furtok_feed(safe: bool = True, page: int = 1, extra_tags: str =
raise Exception("Не удалось загрузить ленту :(") raise Exception("Не удалось загрузить ленту :(")
data = await resp.json() data = await resp.json()
VIDEO_EXTS = ("gif", "webm", "mp4")
IMAGE_EXTS = ("png", "jpg", "jpeg", "webp")
allowed_exts = VIDEO_EXTS + IMAGE_EXTS if is_custom else VIDEO_EXTS
posts = data.get("posts", []) posts = data.get("posts", [])
feed = [] feed = []
for post in posts: for post in posts:
file_url = post.get("file", {}).get("url") file_url = post.get("file", {}).get("url")
ext = post.get("file", {}).get("ext", "") ext = post.get("file", {}).get("ext", "")
if not file_url or ext not in ("gif", "webm", "mp4"): if not file_url or ext not in allowed_exts:
continue continue
sample_url = post.get("sample", {}).get("url") or file_url sample_url = post.get("sample", {}).get("url") or file_url
media_type = "image" if ext in IMAGE_EXTS else "video"
feed.append({ feed.append({
"url": file_url, "url": file_url,
"sample": sample_url, "sample": sample_url,
"ext": ext, "ext": ext,
"type": media_type,
"score": post.get("score", {}).get("total", 0), "score": post.get("score", {}).get("total", 0),
"fav_count": post.get("fav_count", 0), "fav_count": post.get("fav_count", 0),
}) })

View file

@ -8,12 +8,16 @@ REST API для Telegram Mini App. Использует те же SQLite баз
import asyncio import asyncio
import logging import logging
import os import os
import hashlib
from urllib.parse import quote
import sys import sys
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import Annotated from typing import Annotated
from fastapi import FastAPI, Depends, Header, HTTPException import aiohttp
from fastapi import FastAPI, Depends, Header, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import Response
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel from pydantic import BaseModel
@ -401,6 +405,34 @@ async def get_furtok_feed_api(
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/proxy-image")
async def proxy_image(url: str):
"""Проксирует картинку с e621 чтобы обойти hotlink protection."""
allowed = ("static1.e621.net", "static2.e621.net", "static1.e926.net")
from urllib.parse import urlparse
parsed = urlparse(url)
if parsed.hostname not in allowed:
raise HTTPException(status_code=403, detail="Forbidden host")
headers = {
"User-Agent": f"FenyaBot/1.0 (by {config.E621_LOGIN or 'unknown'} on e621)",
}
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=15)) as resp:
if resp.status != 200:
raise HTTPException(status_code=resp.status, detail="Upstream error")
content_type = resp.content_type or "image/png"
body = await resp.read()
return Response(
content=body,
media_type=content_type,
headers={"Cache-Control": "public, max-age=86400"},
)
except aiohttp.ClientError:
raise HTTPException(status_code=502, detail="Failed to fetch image")
# --- ИИ Чат --- # --- ИИ Чат ---
@app.post("/api/talk") @app.post("/api/talk")

View file

@ -913,9 +913,13 @@ body::before {
.furtok-card video, .furtok-card video,
.furtok-card img { .furtok-card img {
position: absolute;
top: 0;
left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
display: block;
} }
.furtok-overlay { .furtok-overlay {

View file

@ -743,7 +743,7 @@ async function loadFurtokPage(feedEl) {
card.className = 'furtok-card'; card.className = 'furtok-card';
let mediaHtml = ''; let mediaHtml = '';
if (post.ext === 'gif') { if (post.type === 'image' || post.ext === 'gif') {
mediaHtml = `<img src="${post.url}" loading="lazy">`; mediaHtml = `<img src="${post.url}" loading="lazy">`;
} else { } else {
mediaHtml = `<video src="${post.url}" loop playsinline preload="metadata" muted></video>`; mediaHtml = `<video src="${post.url}" loop playsinline preload="metadata" muted></video>`;