diff --git a/data/locale/ar.json b/data/locale/ar.json
index 915abed..a69e626 100644
--- a/data/locale/ar.json
+++ b/data/locale/ar.json
@@ -9,7 +9,7 @@
"lang": "تم تعيين اللغة إلى العربية🇸🇦",
"start": "لقد قمت بتشغيل No Watermark TikTok🤖\n\nيدعم هذا البوت تنزيل:\n📹الفيديو، 🖼الصور و 🔈الصوت\nمن TikTok وInstagram بدون علامة مائية\n\nيمكنك أيضًا الاشتراك في قناتنا للحصول على آخر الأخبار حول حالة البوت والتحديثات والإعلانات!\n@ttgrab\n\nأرسل رابط الفيديو للبدء",
"maintenance": "⚠️ صيانة البوت ⚠️\n\nالبوت حاليًا تحت الصيانة وسيعود قريبًا.\nشكرًا لصبرك!\n\nتابع @ttgrab للحصول على التحديثات.",
- "get_sound": "تحميل الصوت",
+ "get_sound": " \uD83C\uDFB5 تحميل الصوت",
"bot_tag": "No Watermark TikTok",
"result": "المصدر\n\n{0}",
"result_song": "غلاف الأغنية\n\n{0}",
diff --git a/data/locale/en.json b/data/locale/en.json
index 3fddb73..037f4bf 100644
--- a/data/locale/en.json
+++ b/data/locale/en.json
@@ -9,7 +9,7 @@
"lang": "The language is set to English🇺🇸",
"start": "You have launched No Watermark TikTok🤖\n\nThis bot supports download of:\n📹Video, 🖼Images and 🔈Audio\nfrom TikTok and Instagram without watermark\n\nYou can also subscribe to our channel to get the latest news about bot status, updates and news!\n@ttgrab\n\nSend video link to get started",
"maintenance": "⚠️ Bot Maintenance ⚠️\n\nThe bot is currently undergoing maintenance and will be back soon.\nThank you for your patience!\n\nFollow @ttgrab for updates.",
- "get_sound": "Get Sound",
+ "get_sound": "\uD83C\uDFB5 Get Sound",
"bot_tag": "No Watermark TikTok",
"result": "Source\n\n{0}",
"result_song": "Song cover\n\n{0}",
diff --git a/data/locale/hi.json b/data/locale/hi.json
index 3a866db..329e992 100644
--- a/data/locale/hi.json
+++ b/data/locale/hi.json
@@ -9,7 +9,7 @@
"lang": "भाषा हिन्दी🇮🇳 पर सेट है",
"start": "आपने No Watermark TikTok🤖 शुरू किया है\n\nयह बॉट TikTok और Instagram से बिना वॉटरमार्क:\n📹वीडियो, 🖼छवियाँ और 🔈ऑडियो\nडाउनलोड करने का समर्थन करता है\n\nआप बॉट की स्थिति, अपडेट और खबरों के लिए हमारे चैनल को भी सब्सक्राइब कर सकते हैं!\n@ttgrab\n\nशुरू करने के लिए वीडियो लिंक भेजें",
"maintenance": "⚠️ बॉट मेंटेनेंस ⚠️\n\nबॉट अभी मेंटेनेंस में है और जल्द ही वापस आएगा।\nआपके धैर्य के लिए धन्यवाद!\n\nअपडेट के लिए @ttgrab फॉलो करें।",
- "get_sound": "साउंड प्राप्त करें",
+ "get_sound": "\uD83C\uDFB5 साउंड प्राप्त करें",
"bot_tag": "No Watermark TikTok",
"result": "स्रोत\n\n{0}",
"result_song": "गाने का कवर\n\n{0}",
diff --git a/data/locale/id.json b/data/locale/id.json
index 0c99269..e1fe685 100644
--- a/data/locale/id.json
+++ b/data/locale/id.json
@@ -9,7 +9,7 @@
"lang": "Bahasa disetel ke Bahasa Indonesia🇮🇩",
"start": "Kamu telah menjalankan No Watermark TikTok🤖\n\nBot ini mendukung unduhan:\n📹Video, 🖼Gambar, dan 🔈Audio\ndari TikTok dan Instagram tanpa watermark\n\nKamu juga bisa berlangganan channel kami untuk mendapatkan kabar terbaru tentang status bot, pembaruan, dan berita!\n@ttgrab\n\nKirim tautan video untuk memulai",
"maintenance": "⚠️ Pemeliharaan Bot ⚠️\n\nBot sedang dalam pemeliharaan dan akan segera kembali.\nTerima kasih atas kesabaranmu!\n\nIkuti @ttgrab untuk pembaruan.",
- "get_sound": "Unduh Audio",
+ "get_sound": "\uD83C\uDFB5 Unduh Audio",
"bot_tag": "No Watermark TikTok",
"result": "Sumber\n\n{0}",
"result_song": "Sampul lagu\n\n{0}",
diff --git a/data/locale/ru.json b/data/locale/ru.json
index c64473d..9e18c1a 100644
--- a/data/locale/ru.json
+++ b/data/locale/ru.json
@@ -9,7 +9,7 @@
"lang": "Установлен язык Русский🇷🇺",
"start": "Вы запустили No Watermark TikTok🤖\n\nЭтот бот поддерживает загрузку:\n📹Видео, 🖼Изображений и 🔈Аудио\nс TikTok и Instagram без водяного знака\n\nВы также можете подписаться на наш канал, чтобы получать последние новости о статусе бота, обновлениях и новостях!\n@ttgrab\n\nОтправьте ссылку на видео, чтобы начать",
"maintenance": "⚠️ Техническое обслуживание бота ⚠️\n\nВ настоящее время бот находится на техническом обслуживании и скоро вернется.\nСпасибо за ваше терпение!\n\nСледите за обновлениями в @ttgrab.",
- "get_sound": "Скачать звук",
+ "get_sound": "\uD83C\uDFB5 Скачать звук",
"bot_tag": "No Watermark TikTok",
"result": "Оригинал\n\n{0}",
"result_song": "Обложка\n\n{0}",
diff --git a/data/locale/so.json b/data/locale/so.json
index c9af713..9882f82 100644
--- a/data/locale/so.json
+++ b/data/locale/so.json
@@ -9,7 +9,7 @@
"lang": "Luqadda waxaa loo dejiyey Soomaali🇸🇴",
"start": "Waxaad bilowday No Watermark TikTok🤖\n\nBot-kan wuxuu taageeraa soo dejinta:\n📹Fiidiyow, 🖼Sawirro iyo 🔈Cod\nTikTok iyo Instagram aan watermark lahayn\n\nWaxaad sidoo kale raaci kartaa kanaalkeenna si aad u hesho wararkii ugu dambeeyay ee xaaladda bot-ka, cusboonaysiinta iyo wararka!\n@ttgrab\n\nDir link-ga fiidiyowga si aad u bilowdo",
"maintenance": "⚠️ Dayactirka Bot-ka ⚠️\n\nBot-ku hadda wuxuu ku jiraa dayactir wuxuuna soo laaban doonaa dhawaan.\nWaad ku mahadsan tahay dulqaadkaaga!\n\nRaac @ttgrab si aad u hesho warar cusub.",
- "get_sound": "Hel Cod",
+ "get_sound": "\uD83C\uDFB5 Hel Cod",
"bot_tag": "No Watermark TikTok",
"result": "Isha\n\n{0}",
"result_song": "Daboolka heesta\n\n{0}",
diff --git a/data/locale/uk.json b/data/locale/uk.json
index cceff59..fff5b78 100644
--- a/data/locale/uk.json
+++ b/data/locale/uk.json
@@ -9,7 +9,7 @@
"lang": "Встановлено мову Українська🇺🇦",
"start": "Ви запустили No Watermark TikTok🤖\n\nЦей бот підтримує завантаження:\n📹Відео, 🖼Зображень та 🔈Аудіо\nз TikTok та Instagram без водяного знака\n\nВи також можете підписатися на наш канал, щоб отримувати останні новини про статус бота, оновлення та новини!\n@ttgrab\n\nНадішліть посилання на відео, щоб почати",
"maintenance": "⚠️ Технічне обслуговування бота ⚠️\n\nНаразі бот перебуває на технічному обслуговуванні і скоро повернеться.\nДякуємо за ваше терпіння!\n\nСлідкуйте за оновленнями в @ttgrab.",
- "get_sound": "Завантажити звук",
+ "get_sound": "\uD83C\uDFB5 Завантажити звук",
"bot_tag": "No Watermark TikTok",
"result": "Оригінал\n\n{0}",
"result_song": "Обкладинка\n\n{0}",
diff --git a/data/locale/vi.json b/data/locale/vi.json
index 7325922..1b11113 100644
--- a/data/locale/vi.json
+++ b/data/locale/vi.json
@@ -9,7 +9,7 @@
"lang": "Ngôn ngữ hiện tại: Tiếng Việt🇻🇳",
"start": "Bạn đã khởi chạy No Watermark TikTok🤖\n\nBot này hỗ trợ tải:\n📹Video, 🖼Hình ảnh và 🔈Âm thanh\ntừ TikTok và Instagram không có watermark\n\nBạn cũng có thể theo dõi kênh của chúng tôi để nhận tin mới nhất về trạng thái bot, cập nhật và thông báo!\n@ttgrab\n\nGửi liên kết video để bắt đầu",
"maintenance": "⚠️ Bảo trì bot ⚠️\n\nBot hiện đang được bảo trì và sẽ sớm quay lại.\nCảm ơn bạn đã kiên nhẫn!\n\nTheo dõi @ttgrab để cập nhật.",
- "get_sound": "Tải âm thanh",
+ "get_sound": "\uD83C\uDFB5 Tải âm thanh",
"bot_tag": "No Watermark TikTok",
"result": "Nguồn\n\n{0}",
"result_song": "Bìa bài hát\n\n{0}",
diff --git a/handlers/get_inline.py b/handlers/get_inline.py
index 46b62ca..7846864 100644
--- a/handlers/get_inline.py
+++ b/handlers/get_inline.py
@@ -24,7 +24,7 @@
from media_types import send_video_result, get_error_message
from media_types.image_processing import ensure_native_format
from media_types.storage import upload_photo_to_storage
-from media_types.ui import result_caption
+from media_types.ui import result_caption, stats_keyboard
from handlers.inline_slideshow import register_slideshow
inline_router = Router(name=__name__)
@@ -175,12 +175,13 @@ async def _handle_tiktok_inline(
message_id, image_urls, image_data, lang, video_link,
user_id, username, full_name,
client=api, video_info=video_info,
+ likes=video_info.likes, views=video_info.views,
)
else:
file_id = await upload_photo_to_storage(
image_data, video_link, user_id, username, full_name
)
- keyboard = None
+ keyboard = stats_keyboard(video_info.likes, video_info.views)
if not file_id:
raise ValueError(
"Failed to upload photo to storage. "
diff --git a/handlers/get_music.py b/handlers/get_music.py
index c7be366..54a416a 100644
--- a/handlers/get_music.py
+++ b/handlers/get_music.py
@@ -10,6 +10,7 @@
from tiktok_api import TikTokClient, TikTokError, ProxyManager
from misc.utils import lang_func, error_catch
from media_types import send_music_result, music_button, get_error_message
+from media_types.ui import STATS_CALLBACK_PREFIX
music_router = Router(name=__name__)
@@ -17,6 +18,11 @@
RETRY_EMOJIS = ["👀", "🤔", "🙏"]
+@music_router.callback_query(F.data == STATS_CALLBACK_PREFIX)
+async def handle_stats_noop(callback: CallbackQuery):
+ await callback.answer()
+
+
@music_router.callback_query(F.data.startswith("id"))
async def send_tiktok_sound(callback_query: CallbackQuery):
# Vars
diff --git a/handlers/inline_slideshow.py b/handlers/inline_slideshow.py
index 750a520..eef017e 100644
--- a/handlers/inline_slideshow.py
+++ b/handlers/inline_slideshow.py
@@ -22,7 +22,7 @@
_build_storage_caption,
upload_photo_to_storage,
)
-from media_types.ui import result_caption
+from media_types.ui import result_caption, stats_row
from misc.utils import lang_func
from tiktok_api import TikTokClient, ProxyManager
from tiktok_api.models import VideoInfo
@@ -44,6 +44,8 @@ class SlideshowSession:
user_id: int
username: str | None
full_name: str | None
+ likes: int | None = None
+ views: int | None = None
_cleanup_task: asyncio.Task | None = field(default=None, repr=False)
_loading_indices: set[int] = field(default_factory=set, repr=False)
@@ -52,18 +54,28 @@ class SlideshowSession:
_refreshing_sessions: set[str] = set() # inline_message_ids currently refreshing
-def _build_keyboard(index: int, total: int) -> InlineKeyboardMarkup:
- buttons: list[InlineKeyboardButton] = []
+def _build_keyboard(
+ index: int,
+ total: int,
+ likes: int | None = None,
+ views: int | None = None,
+) -> InlineKeyboardMarkup:
+ rows: list[list[InlineKeyboardButton]] = []
+ sr = stats_row(likes, views)
+ if sr:
+ rows.append(sr)
+ nav: list[InlineKeyboardButton] = []
if index > 0:
- buttons.append(InlineKeyboardButton(text="◀️", callback_data="slide:prev"))
- buttons.append(
+ nav.append(InlineKeyboardButton(text="◀️", callback_data="slide:prev"))
+ nav.append(
InlineKeyboardButton(
text=f"📸 {index + 1}/{total}", callback_data="slide:noop"
)
)
if index < total - 1:
- buttons.append(InlineKeyboardButton(text="▶️", callback_data="slide:next"))
- return InlineKeyboardMarkup(inline_keyboard=[buttons])
+ nav.append(InlineKeyboardButton(text="▶️", callback_data="slide:next"))
+ rows.append(nav)
+ return InlineKeyboardMarkup(inline_keyboard=rows)
def _compress_url(source_link: str) -> str:
@@ -88,24 +100,31 @@ def _expand_url(compressed: str) -> str:
def _build_expired_keyboard(
- index: int, total: int, source_link: str
+ index: int,
+ total: int,
+ source_link: str,
+ likes: int | None = None,
+ views: int | None = None,
) -> InlineKeyboardMarkup:
"""Build a keyboard with counter + refresh button for expired sessions."""
compressed = _compress_url(source_link)
- return InlineKeyboardMarkup(
- inline_keyboard=[
- [
- InlineKeyboardButton(
- text=f"📸 {index + 1}/{total}",
- callback_data="slide:noop",
- ),
- InlineKeyboardButton(
- text="🔄",
- callback_data=f"sr:{index}:{compressed}",
- ),
- ]
+ rows: list[list[InlineKeyboardButton]] = []
+ sr = stats_row(likes, views)
+ if sr:
+ rows.append(sr)
+ rows.append(
+ [
+ InlineKeyboardButton(
+ text=f"📸 {index + 1}/{total}",
+ callback_data="slide:noop",
+ ),
+ InlineKeyboardButton(
+ text="🔄",
+ callback_data=f"sr:{index}:{compressed}",
+ ),
]
)
+ return InlineKeyboardMarkup(inline_keyboard=rows)
async def _expire_session(inline_message_id: str) -> None:
@@ -119,6 +138,8 @@ async def _expire_session(inline_message_id: str) -> None:
session.current_index,
len(session.image_urls),
session.source_link,
+ session.likes,
+ session.views,
)
await bot.edit_message_reply_markup(
inline_message_id=inline_message_id, reply_markup=keyboard
@@ -244,6 +265,8 @@ async def register_slideshow(
full_name: str | None,
client: TikTokClient | None = None,
video_info: VideoInfo | None = None,
+ likes: int | None = None,
+ views: int | None = None,
) -> tuple[str, InlineKeyboardMarkup]:
"""Download all images, upload as galleries, create session, return (first_file_id, keyboard)."""
file_ids = await _download_and_upload_images(
@@ -268,10 +291,12 @@ async def register_slideshow(
user_id=user_id,
username=username,
full_name=full_name,
+ likes=likes,
+ views=views,
)
_slideshow_sessions[inline_message_id] = session
_reset_ttl(inline_message_id, session)
- return first_file_id, _build_keyboard(0, len(image_urls))
+ return first_file_id, _build_keyboard(0, len(image_urls), likes, views)
def cleanup_all_slideshows() -> None:
@@ -342,7 +367,7 @@ async def handle_slideshow_callback(callback: CallbackQuery) -> None:
session.current_index = new_index
caption = result_caption(session.lang, session.source_link)
media = InputMediaPhoto(media=file_id, caption=caption)
- keyboard = _build_keyboard(new_index, total)
+ keyboard = _build_keyboard(new_index, total, session.likes, session.views)
await bot.edit_message_media(
inline_message_id=inline_message_id,
@@ -438,6 +463,13 @@ async def handle_slideshow_refresh(callback: CallbackQuery) -> None:
await callback.answer("Failed to upload images.", show_alert=True)
return
+ # Extract stats from refreshed TikTok data if available
+ refresh_likes = None
+ refresh_views = None
+ if tiktok_video_info:
+ refresh_likes = tiktok_video_info.likes
+ refresh_views = tiktok_video_info.views
+
# Create new session
session = SlideshowSession(
image_urls=image_urls,
@@ -448,13 +480,15 @@ async def handle_slideshow_refresh(callback: CallbackQuery) -> None:
user_id=user_id,
username=username,
full_name=full_name,
+ likes=refresh_likes,
+ views=refresh_views,
)
_slideshow_sessions[inline_message_id] = session
# Edit message with refreshed image + nav keyboard
caption = result_caption(lang, source_link)
media = InputMediaPhoto(media=file_id, caption=caption)
- keyboard = _build_keyboard(index, total)
+ keyboard = _build_keyboard(index, total, refresh_likes, refresh_views)
await bot.edit_message_media(
inline_message_id=inline_message_id,
diff --git a/media_types/send_images.py b/media_types/send_images.py
index 1133279..8ba4808 100644
--- a/media_types/send_images.py
+++ b/media_types/send_images.py
@@ -204,7 +204,7 @@ async def process_and_send_images():
if final and len(final) > 0:
await final[0].reply(
result_caption(lang, video_info.link, bool(image_limit)),
- reply_markup=music_button(video_id, lang),
+ reply_markup=music_button(video_id, lang, video_info.likes, video_info.views),
disable_web_page_preview=True,
)
diff --git a/media_types/send_video.py b/media_types/send_video.py
index 3e79bbc..8473232 100644
--- a/media_types/send_video.py
+++ b/media_types/send_video.py
@@ -5,7 +5,7 @@
from .http_session import download_thumbnail
from .storage import upload_video_to_storage
-from .ui import music_button, result_caption
+from .ui import music_button, result_caption, stats_keyboard
async def send_video_result(
@@ -55,7 +55,11 @@ async def send_video_result(
duration=video_duration,
supports_streaming=True,
)
- await bot.edit_message_media(inline_message_id=targed_id, media=video_media)
+ await bot.edit_message_media(
+ inline_message_id=targed_id,
+ media=video_media,
+ reply_markup=stats_keyboard(video_info.likes, video_info.views),
+ )
return
if isinstance(video_data, bytes):
@@ -68,7 +72,7 @@ async def send_video_result(
chat_id=targed_id,
document=video_file,
caption=result_caption(lang, video_info.link),
- reply_markup=music_button(video_id, lang),
+ reply_markup=music_button(video_id, lang, video_info.likes, video_info.views),
reply_to_message_id=reply_to_message_id,
disable_content_type_detection=True,
)
@@ -86,6 +90,6 @@ async def send_video_result(
duration=video_duration,
thumbnail=thumbnail,
supports_streaming=True,
- reply_markup=music_button(video_id, lang),
+ reply_markup=music_button(video_id, lang, video_info.likes, video_info.views),
reply_to_message_id=reply_to_message_id,
)
diff --git a/media_types/ui.py b/media_types/ui.py
index 3551702..de43d0a 100644
--- a/media_types/ui.py
+++ b/media_types/ui.py
@@ -1,12 +1,60 @@
-from aiogram.types import InlineKeyboardMarkup
+from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from data.config import locale
+STATS_CALLBACK_PREFIX = "stats_noop"
-def music_button(video_id: int, lang: str) -> InlineKeyboardMarkup:
+
+def format_stat(value: int) -> str:
+ if value >= 999_950:
+ formatted = f"{value / 1_000_000:.1f}".rstrip("0").rstrip(".")
+ return f"{formatted}M"
+ if value >= 1_000:
+ formatted = f"{value / 1_000:.1f}".rstrip("0").rstrip(".")
+ return f"{formatted}K"
+ return str(value)
+
+
+def stats_row(
+ likes: int | None = None, views: int | None = None
+) -> list[InlineKeyboardButton]:
+ buttons: list[InlineKeyboardButton] = []
+ if likes is not None:
+ buttons.append(
+ InlineKeyboardButton(
+ text=f"❤️ {format_stat(likes)}", callback_data=STATS_CALLBACK_PREFIX
+ )
+ )
+ if views is not None:
+ buttons.append(
+ InlineKeyboardButton(
+ text=f"👁 {format_stat(views)}", callback_data=STATS_CALLBACK_PREFIX
+ )
+ )
+ return buttons
+
+
+def stats_keyboard(
+ likes: int | None = None, views: int | None = None
+) -> InlineKeyboardMarkup | None:
+ row = stats_row(likes, views)
+ return InlineKeyboardMarkup(inline_keyboard=[row]) if row else None
+
+
+def music_button(
+ video_id: int,
+ lang: str,
+ likes: int | None = None,
+ views: int | None = None,
+) -> InlineKeyboardMarkup:
keyb = InlineKeyboardBuilder()
+ row = stats_row(likes, views)
+ for btn in row:
+ keyb.add(btn)
keyb.button(text=locale[lang]["get_sound"], callback_data=f"id/{video_id}")
+ if row:
+ keyb.adjust(len(row), 1)
return keyb.as_markup()
diff --git a/tiktok_api/client.py b/tiktok_api/client.py
index 8d292d3..7085b85 100644
--- a/tiktok_api/client.py
+++ b/tiktok_api/client.py
@@ -1521,6 +1521,10 @@ async def video(self, video_link: str) -> VideoInfo:
if image_urls:
author = video_data.get("author", {}).get("uniqueId", "")
+ stats = video_data.get("stats", {})
+ likes = stats.get("diggCount")
+ views = stats.get("playCount")
+
# Transfer context ownership to VideoInfo
# Part 3 (image download) happens later via download_slideshow_images()
context_transferred = True
@@ -1534,6 +1538,8 @@ async def video(self, video_link: str) -> VideoInfo:
duration=None,
link=video_link,
url=None,
+ likes=likes,
+ views=views,
_download_context=download_context,
_proxy_session=proxy_session, # For Part 3 retry
)
@@ -1586,6 +1592,10 @@ async def video(self, video_link: str) -> VideoInfo:
height = video_info_data.get("height")
cover = video_info_data.get("cover") or video_info_data.get("originCover")
+ stats = video_data.get("stats", {})
+ likes = stats.get("diggCount")
+ views = stats.get("playCount")
+
return VideoInfo(
type="video",
data=video_bytes,
@@ -1596,6 +1606,8 @@ async def video(self, video_link: str) -> VideoInfo:
duration=duration,
link=video_link,
url=video_url,
+ likes=likes,
+ views=views,
)
except TikTokError:
diff --git a/tiktok_api/models.py b/tiktok_api/models.py
index 6d2b155..9aa73ef 100644
--- a/tiktok_api/models.py
+++ b/tiktok_api/models.py
@@ -37,6 +37,8 @@ class VideoInfo:
duration: Optional[int]
link: str
url: Optional[str] = None # Only present for videos
+ likes: Optional[int] = None
+ views: Optional[int] = None
# Download context for slideshows (set by TikTokClient).
# Contains yt-dlp YoutubeDL instance and TikTok extractor with cookies/auth