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