From d913bdcea8470b9b2fc07cd77754baff29d36119 Mon Sep 17 00:00:00 2001 From: 11linear11 <11linearlin11@gmail.com> Date: Tue, 14 Oct 2025 15:13:19 -0400 Subject: [PATCH 1/3] add multichanell update --- DeezerApiCall.py | 20 --- PLAYLIST_CHANNEL_MAPPING.md | 330 ++++++++++++++++++++++++++++++++++++ README.md | 64 ++++++- bot.py | 254 ++++++++++++++++++++++++++- 4 files changed, 633 insertions(+), 35 deletions(-) create mode 100644 PLAYLIST_CHANNEL_MAPPING.md diff --git a/DeezerApiCall.py b/DeezerApiCall.py index 5b26a49..c2f1ad4 100644 --- a/DeezerApiCall.py +++ b/DeezerApiCall.py @@ -343,23 +343,3 @@ def setup_arl(self): return False -# if __name__ == '__main__': -# downloader = DeemixDownloader() - -# if not downloader.is_configured(): -# print("⚠️ First time setup required!") -# if not downloader.setup_arl(): -# exit(1) -# else: -# print("✅ ARL token loaded from config\n") - -# try: -# # دانلود با کیفیت 128kbps (مناسب برای حساب رایگان) -# file_path = downloader.download_track( -# 'Vaghti Az Hame Khastam', -# 'Maslak', -# bitrate='128' # کیفیت پایین‌تر -# ) - -# except Exception as e: -# print(f"\n❌ Error: {e}") \ No newline at end of file diff --git a/PLAYLIST_CHANNEL_MAPPING.md b/PLAYLIST_CHANNEL_MAPPING.md new file mode 100644 index 0000000..826a83e --- /dev/null +++ b/PLAYLIST_CHANNEL_MAPPING.md @@ -0,0 +1,330 @@ +# 🔗 راهنمای ارتباط پلی‌لیست به چنل + +این قابلیت به شما اجازه می‌دهد که هر پلی‌لیست اسپاتیفای را به یک چنل تلگرام مخصوص متصل کنید. + +## 🎯 کاربردها + +- **چند چنل موضوعی**: هر چنل برای یک ژانر خاص + - چنل راک → پلی‌لیست راک + - چنل پاپ → پلی‌لیست پاپ + - چنل کلاسیک → پلی‌لیست کلاسیک + +- **چنل‌های مختلف برای مخاطبان مختلف**: + - چنل عمومی → پلی‌لیست‌های پرطرفدار + - چنل VIP → پلی‌لیست‌های اختصاصی + +- **سازماندهی بهتر**: هر پلی‌لیست به چنل مربوط به خودش می‌رود + +## 📋 دستورات جدید + +### 1. `/linkplaylist` - ارتباط پلی‌لیست به چنل + +این دستور برای تغییر یا تنظیم چنل یک پلی‌لیست موجود استفاده می‌شود. + +**مراحل استفاده:** + +1. دستور `/linkplaylist` را ارسال کنید +2. از لیست دکمه‌های نمایش داده شده، پلی‌لیست مورد نظر را انتخاب کنید +3. ID یا username چنل جدید را ارسال کنید + +**مثال:** + +``` +شما: /linkplaylist + +ربات: +🔗 ارتباط پلی‌لیست به چنل + +لطفا پلی‌لیستی که می‌خواهید به چنل جدید مرتبط کنید را انتخاب کنید: +[دکمه] 🎵 پلی‌لیست راک (چنل: @rockchannel) +[دکمه] 🎵 پلی‌لیست پاپ (چنل: @popchannel) +[دکمه] ❌ انصراف + +[شما یکی از دکمه‌ها را میزنید] + +ربات: +🎵 پلی‌لیست انتخاب شده: پلی‌لیست راک +📺 چنل فعلی: @rockchannel + +لطفا ID چنل جدید را ارسال کنید: +مثال: @channelname یا -1001234567890 + +شما: @newrockchannel + +ربات: +✅ چنل برای پلی‌لیست 'پلی‌لیست راک' به @newrockchannel تغییر یافت! + +حالا آهنگ‌های جدید این پلی‌لیست به این چنل ارسال می‌شوند. +``` + +### 2. `/showlinks` - نمایش ارتباطات + +این دستور لیست تمام پلی‌لیست‌ها و چنل‌های مرتبط با آن‌ها را نمایش می‌دهد. + +**خروجی نمونه:** + +``` +🔗 ارتباط پلی‌لیست‌ها با چنل‌ها: + +1. 🎵 پلی‌لیست راک + 📺 چنل: @rockchannel + 📊 تعداد آهنگ: 250 + 🕐 آخرین چک: 2025-10-14 15:30 + 🔗 [لینک پلی‌لیست](https://open.spotify.com/playlist/...) + +2. 🎵 پلی‌لیست پاپ + 📺 چنل: @popchannel + 📊 تعداد آهنگ: 180 + 🕐 آخرین چک: 2025-10-14 15:32 + 🔗 [لینک پلی‌لیست](https://open.spotify.com/playlist/...) + +3. 🎵 پلی‌لیست کلاسیک + 📺 چنل: ❌ تنظیم نشده + 📊 تعداد آهنگ: 95 + 🕐 آخرین چک: هرگز + 🔗 [لینک پلی‌لیست](https://open.spotify.com/playlist/...) + +[دکمه] 🔗 تغییر ارتباط +[دکمه] 🔙 بازگشت +``` + +### 3. `/addplaylist` - افزودن با تنظیم چنل + +هنگام افزودن پلی‌لیست جدید، حالا می‌توانید مستقیماً چنل را تنظیم کنید. + +**مراحل:** + +1. `/addplaylist` را ارسال کنید +2. لینک پلی‌لیست را ارسال کنید +3. نام دلخواه برای پلی‌لیست را وارد کنید +4. **ID چنل مورد نظر را ارسال کنید** (قابلیت جدید!) + +**مثال:** + +``` +شما: /addplaylist + +ربات: لطفا لینک پلی‌لیست اسپاتیفای را ارسال کنید + +شما: https://open.spotify.com/playlist/37i9dQZF1DXcBWIGoYBM5M + +ربات: ✅ لینک دریافت شد. +حالا یک نام برای این پلی‌لیست وارد کنید: + +شما: پلی‌لیست الکترونیک + +ربات: ✅ لینک و نام دریافت شد. +حالا ID چنل مورد نظر را ارسال کنید: +مثال: @channelname یا -1001234567890 + +شما: @electronicchannel + +ربات: ✅ پلی‌لیست 'پلی‌لیست الکترونیک' با موفقیت اضافه شد! +چنل تنظیم شده: @electronicchannel + +می‌خواهید الان آهنگ‌های آن را چک و ارسال کنید؟ +[دکمه] 📤 ارسال فوری آهنگ‌های این پلی‌لیست +``` + +## 🎮 نکات مهم + +### فرمت ID چنل + +دو فرمت قابل قبول هستند: + +1. **Username چنل** (اگر چنل عمومی است): + ``` + @mymusicchannel + ``` + یا فقط: + ``` + mymusicchannel + ``` + +2. **ID عددی چنل** (برای چنل‌های خصوصی): + ``` + -1001234567890 + ``` + +### چگونه ID چنل را پیدا کنیم؟ + +#### روش 1: استفاده از ربات‌های شناسایی ID + +1. ربات [@userinfobot](https://t.me/userinfobot) را به چنل خود اضافه کنید +2. یک پیام در چنل forward کنید به این ربات +3. ربات ID چنل را نمایش می‌دهد + +#### روش 2: استفاده از Web Telegram + +1. به https://web.telegram.org بروید +2. چنل خود را باز کنید +3. از URL چنل، عدد بعد از `s` را بردارید و `-100` را به ابتدای آن اضافه کنید + - مثال: اگر URL این است: `https://web.telegram.org/#/im?p=s1234567890` + - ID شما می‌شود: `-1001234567890` + +### دسترسی‌های لازم + +⚠️ **مهم**: ربات باید در چنل مورد نظر **Admin** باشد و دسترسی‌های زیر را داشته باشد: +- Post Messages +- Edit Messages +- Delete Messages + +## 🔄 تغییر چنل یک پلی‌لیست + +می‌توانید هر زمان که بخواهید چنل یک پلی‌لیست را تغییر دهید: + +1. `/linkplaylist` را ارسال کنید +2. پلی‌لیست مورد نظر را انتخاب کنید +3. چنل جدید را وارد کنید +4. تمام آهنگ‌های **جدید** بعد از این به چنل جدید می‌روند + +⚠️ **توجه**: آهنگ‌هایی که قبلاً ارسال شده‌اند، در چنل قدیم باقی می‌مانند. + +## 💡 سناریوهای کاربردی + +### سناریو 1: چند چنل موضوعی + +``` +پلی‌لیست‌ها: +- Rock Hits 2025 → @rockhits2025 +- Pop Charts → @popcharts2025 +- Classical Music → @classicalbeats +- Jazz Collection → @jazzlovers +``` + +### سناریو 2: چنل‌های زبان‌های مختلف + +``` +پلی‌لیست‌ها: +- English Songs → @englishmusic +- Persian Songs → @persianmusic +- Turkish Songs → @turkishmusic +``` + +### سناریو 3: سطح‌بندی کاربران + +``` +پلی‌لیست‌ها: +- Free Playlist → @freemusicchannel (عمومی) +- Premium Playlist → @premiummusicchannel (خصوصی) +- VIP Exclusive → @vipmusicchannel (خصوصی) +``` + +## 📊 مثال عملی کامل + +فرض کنید می‌خواهید 3 پلی‌لیست را به 3 چنل مختلف متصل کنید: + +```bash +# مرحله 1: افزودن پلی‌لیست اول +/addplaylist +# لینک: https://open.spotify.com/playlist/rock123 +# نام: Rock Legends +# چنل: @rocklegends + +# مرحله 2: افزودن پلی‌لیست دوم +/addplaylist +# لینک: https://open.spotify.com/playlist/pop456 +# نام: Pop Hits +# چنل: @pophits + +# مرحله 3: افزودن پلی‌لیست سوم +/addplaylist +# لینک: https://open.spotify.com/playlist/jazz789 +# نام: Jazz Classics +# چنل: @jazzclassics + +# بررسی ارتباطات +/showlinks + +# خروجی: +# 1. 🎵 Rock Legends → 📺 @rocklegends +# 2. 🎵 Pop Hits → 📺 @pophits +# 3. 🎵 Jazz Classics → 📺 @jazzclassics + +# اگر بخواهید چنل Rock Legends را عوض کنید: +/linkplaylist +# انتخاب: Rock Legends +# چنل جدید: @newrocklegends + +# تایید: +# ✅ چنل برای پلی‌لیست 'Rock Legends' به @newrocklegends تغییر یافت! +``` + +## 🐛 عیب‌یابی + +### مشکل: "ربات نمی‌تواند به چنل ارسال کند" + +**راه حل:** +1. مطمئن شوید ربات در چنل Admin است +2. دسترسی "Post Messages" را بررسی کنید +3. اگر چنل خصوصی است، از ID عددی استفاده کنید نه username + +### مشکل: "ID چنل نامعتبر است" + +**راه حل:** +1. مطمئن شوید فرمت صحیح است (@channel یا -100...) +2. اگر از username استفاده می‌کنید، چنل باید عمومی باشد +3. ID عددی را دوباره از ربات شناسایی ID بگیرید + +### مشکل: "آهنگ‌ها به چنل اشتباه می‌رود" + +**راه حل:** +1. `/showlinks` را اجرا کنید و ارتباطات را بررسی کنید +2. از `/linkplaylist` برای تصحیح چنل استفاده کنید +3. ربات را restart کنید: `docker-compose restart` + +## 🎯 نکات پیشرفته + +### استفاده از config.json + +تمام تنظیمات در فایل `config.json` ذخیره می‌شود: + +```json +{ + "playlists": [ + { + "url": "https://open.spotify.com/playlist/...", + "name": "Rock Legends", + "channel_id": "@rocklegends", + "added_by": 123456789, + "added_at": "2025-10-14T15:30:00", + "last_check": "2025-10-14T16:00:00", + "track_count": 250 + } + ] +} +``` + +می‌توانید این فایل را دستی ویرایش کنید (اگر لازم باشد). + +### Backup کردن تنظیمات + +```bash +# Backup کردن config +cp config.json config.json.backup + +# Restore کردن +cp config.json.backup config.json +docker-compose restart +``` + +## 📞 پشتیبانی + +اگر مشکلی داشتید: + +1. لاگ‌های ربات را بررسی کنید: + ```bash + docker-compose logs -f + ``` + +2. مطمئن شوید ربات در تمام چنل‌ها Admin است + +3. config.json را بررسی کنید + +4. Issue در GitHub ایجاد کنید با جزئیات مشکل + +--- + +**تاریخ ایجاد**: اکتبر 2025 +**نسخه**: 2.0 +**قابلیت جدید**: Multi-Channel Playlist Mapping diff --git a/README.md b/README.md index 8639f05..8890a68 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,11 @@ A powerful Telegram bot that monitors Spotify playlists and automatically downlo ## ✨ Features - 🎼 **Multiple Playlist Support**: Monitor multiple Spotify playlists simultaneously +- 📺 **Channel Mapping**: Link each playlist to a specific Telegram channel +- 🔗 **Flexible Channel Management**: Easily change which channel receives tracks from which playlist - 🤖 **Bot Management**: Add/remove playlists directly through Telegram commands - ⏰ **Automatic Monitoring**: Checks for new tracks every 6 hours -- 📤 **Auto-Upload**: Automatically downloads and sends new tracks to your channel +- 📤 **Auto-Upload**: Automatically downloads and sends new tracks to designated channels - 🔐 **Admin Control**: Secure admin-only access to bot commands - 🐳 **Fully Dockerized**: Easy deployment with Docker Compose - 📊 **Statistics**: Track playlist stats and bot performance @@ -16,17 +18,20 @@ A powerful Telegram bot that monitors Spotify playlists and automatically downlo ## 🏗️ Architecture ``` -┌─────────────┐ ┌──────────────┐ ┌─────────────┐ -│ Spotify │─────▶│ Docker Bot │─────▶│ Telegram │ -│ Playlists │ │ + Deemix │ │ Channel │ -└─────────────┘ └──────────────┘ └─────────────┘ +┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ +│ Spotify │─────▶│ Docker Bot │─────▶│ Telegram │ +│ Playlists │ │ + Deemix │ │ Channels │ +│ Playlist A │ │ │ │ → Channel 1 │ +│ Playlist B │ │ │ │ → Channel 2 │ +│ Playlist C │ │ │ │ → Channel 3 │ +└─────────────┘ └──────────────┘ └─────────────────┘ ``` ## 📋 Prerequisites - Docker and Docker Compose installed - Telegram Bot Token (from [@BotFather](https://t.me/BotFather)) -- Telegram Channel (make bot admin) +- Telegram Channel(s) (make bot admin in each) - Spotify API credentials (from [Spotify Developer Dashboard](https://developer.spotify.com/dashboard)) - Deezer account (free account works, but limited to 128kbps) @@ -108,13 +113,18 @@ If you didn't add ARL to .env: - `/start` - Welcome message and bot info - `/help` - Display help and usage instructions - `/listplaylists` - Show all monitored playlists +- `/showlinks` - Display playlist-to-channel mappings - `/stats` - View bot statistics ### Admin Commands - `/addplaylist` - Add a new Spotify playlist to monitor +- `/linkplaylist` - Link a playlist to a specific channel +- `/setchannel` - Set channel for a playlist (legacy method) - `/removeplaylist` - Remove a playlist from monitoring - `/checkplaylists` - Manually trigger playlist check (bypass 6-hour timer) +- `/setuparl` - Show instructions for getting Deezer ARL +- `/setarl` - Set Deezer ARL token ## 📖 Usage Guide @@ -129,7 +139,47 @@ If you didn't add ARL to .env: ``` My Favorite Mix ``` -4. Bot will confirm and start monitoring! +4. Send the channel ID where tracks should be posted: + ``` + @mymusicchannel + ``` + or + ``` + -1001234567890 + ``` +5. Bot will confirm and start monitoring! + +### Linking Playlist to Channel + +You can link different playlists to different channels: + +1. Send `/linkplaylist` to the bot +2. Select the playlist from the interactive menu +3. Send the new channel ID: + ``` + @mynewchannel + ``` +4. Bot will update the mapping and confirm! + +### Viewing Playlist-Channel Mappings + +Send `/showlinks` to see which playlist sends to which channel: + +``` +🔗 ارتباط پلی‌لیست‌ها با چنل‌ها: + +1. 🎵 My Favorite Mix + 📺 چنل: @mymusicchannel + 📊 تعداد آهنگ: 50 + 🕐 آخرین چک: 2025-01-15 14:30 + 🔗 [لینک پلی‌لیست](https://open.spotify.com/playlist/...) + +2. 🎵 Chill Vibes + 📺 چنل: @chillchannel + 📊 تعداد آهنگ: 32 + 🕐 آخرین چک: 2025-01-15 14:32 + 🔗 [لینک پلی‌لیست](https://open.spotify.com/playlist/...) +``` ### Viewing Playlists diff --git a/bot.py b/bot.py index 2e72b7b..1f2d7bb 100644 --- a/bot.py +++ b/bot.py @@ -218,6 +218,8 @@ async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE 📋 دستورات موجود: /addplaylist - افزودن پلی‌لیست جدید +/linkplaylist - ارتباط پلی‌لیست به چنل خاص +/showlinks - نمایش ارتباط پلی‌لیست‌ها با چنل‌ها /setchannel - تنظیم چنل برای پلی‌لیست /listplaylists - نمایش پلی‌لیست‌ها /removeplaylist - حذف پلی‌لیست @@ -236,36 +238,58 @@ async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE) 1️⃣ افزودن پلی‌لیست: /addplaylist -سپس لینک پلی‌لیست و نام دلخواه را ارسال کنید. +سپس لینک پلی‌لیست، نام و ID چنل را ارسال کنید. -2️⃣ تنظیم چنل برای پلی‌لیست: +2️⃣ ارتباط پلی‌لیست به چنل: +/linkplaylist +برای تغییر چنل یک پلی‌لیست موجود + +3️⃣ نمایش ارتباط پلی‌لیست‌ها با چنل‌ها: +/showlinks +برای مشاهده همه پلی‌لیست‌ها و چنل‌های مرتبط + +4️⃣ تنظیم چنل برای پلی‌لیست: /setchannel سپس شماره پلی‌لیست و ID چنل را وارد کنید. -3️⃣ مشاهده پلی‌لیست‌ها: +5️⃣ مشاهده پلی‌لیست‌ها: /listplaylists -4️⃣ حذف پلی‌لیست: +6️⃣ حذف پلی‌لیست: /removeplaylist سپس شماره پلی‌لیست را وارد کنید. -5️⃣ چک دستی: +7️⃣ چک دستی: /checkplaylists برای چک فوری تمام پلی‌لیست‌ها -6️⃣ آمار: +8️⃣ آمار: /stats برای مشاهده آمار ربات -7️⃣ تنظیم Deezer ARL: +9️⃣ تنظیم Deezer ARL: /setuparl - راهنمای دریافت ARL /setarl TOKEN - تنظیم ARL token ⏰ ربات هر 6 ساعت به صورت خودکار پلی‌لیست‌ها را چک می‌کند. 🎵 می‌توانید با دکمه‌های inline هر پلی‌لیست را جداگانه ارسال کنید. +📺 هر پلی‌لیست می‌تواند به چنل مخصوص خود متصل باشد. """ await update.message.reply_text(help_text) + async def add_playlist_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """Handle /addplaylist command""" + if not self.is_admin(update.effective_user.id): + await update.message.reply_text("⛔️ فقط ادمین‌ها می‌توانند پلی‌لیست اضافه کنند.") + return + + await update.message.reply_text( + "لطفا لینک پلی‌لیست اسپاتیفای را ارسال کنید:\n" + "مثال: https://open.spotify.com/playlist/...\n\n" + "یا /cancel برای لغو" + ) + context.user_data['awaiting_playlist_url'] = True + async def set_channel_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """Handle /setchannel command""" if not self.is_admin(update.effective_user.id): @@ -285,6 +309,75 @@ async def set_channel_command(self, update: Update, context: ContextTypes.DEFAUL await update.message.reply_text(message) context.user_data['awaiting_channel_playlist'] = True + async def link_playlist_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """Handle /linkplaylist command - ارتباط دادن پلی‌لیست به چنل""" + if not self.is_admin(update.effective_user.id): + await update.message.reply_text("⛔️ فقط ادمین‌ها می‌توانند پلی‌لیست را به چنل مرتبط کنند.") + return + + playlists = self.config_manager.get_playlists() + if not playlists: + await update.message.reply_text("📭 هیچ پلی‌لیستی برای ارتباط با چنل وجود ندارد.") + return + + # ایجاد دکمه‌های inline برای انتخاب پلی‌لیست + keyboard = [] + for i, playlist in enumerate(playlists): + current_channel = playlist.get('channel_id', 'تنظیم نشده') + keyboard.append([ + InlineKeyboardButton( + f"🎵 {playlist['name'][:30]} (چنل: {current_channel})", + callback_data=f"link_playlist_{i}" + ) + ]) + + keyboard.append([InlineKeyboardButton("❌ انصراف", callback_data="cancel_action")]) + reply_markup = InlineKeyboardMarkup(keyboard) + + await update.message.reply_text( + "🔗 *ارتباط پلی‌لیست به چنل*\n\n" + "لطفا پلی‌لیستی که می‌خواهید به چنل جدید مرتبط کنید را انتخاب کنید:", + reply_markup=reply_markup, + parse_mode='Markdown' + ) + + async def show_links_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """Handle /showlinks command - نمایش ارتباط پلی‌لیست‌ها با چنل‌ها""" + playlists = self.config_manager.get_playlists() + + if not playlists: + await update.message.reply_text("📭 هیچ پلی‌لیستی ثبت نشده است.") + return + + message = "🔗 *ارتباط پلی‌لیست‌ها با چنل‌ها:*\n\n" + + for i, playlist in enumerate(playlists, 1): + channel_id = playlist.get('channel_id', '❌ تنظیم نشده') + track_count = playlist.get('track_count', 0) + last_check = playlist.get('last_check', 'هرگز') + + if last_check != 'هرگز': + try: + last_check = datetime.fromisoformat(last_check).strftime('%Y-%m-%d %H:%M') + except: + pass + + message += f"{i}. 🎵 *{playlist['name']}*\n" + message += f" 📺 چنل: `{channel_id}`\n" + message += f" 📊 تعداد آهنگ: {track_count}\n" + message += f" 🕐 آخرین چک: {last_check}\n" + message += f" 🔗 [لینک پلی‌لیست]({playlist['url']})\n\n" + + # ایجاد دکمه برای ویرایش سریع + if self.is_admin(update.effective_user.id): + keyboard = [[ + InlineKeyboardButton("🔗 ارتباط پلی‌لیست به چنل", callback_data="show_link_menu") + ]] + reply_markup = InlineKeyboardMarkup(keyboard) + await update.message.reply_text(message, reply_markup=reply_markup, parse_mode='Markdown') + else: + await update.message.reply_text(message, parse_mode='Markdown') + async def list_playlists_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """Handle /listplaylists command""" playlists = self.config_manager.get_playlists() @@ -611,6 +704,46 @@ async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYP user_data.clear() return + + # Handle new channel ID for link playlist command + if user_data.get('awaiting_new_channel_id'): + channel_id = text.strip() + + # Handle @username format + if channel_id.startswith('@'): + channel_id = channel_id[1:] # Remove @ symbol + + # Basic validation + if not channel_id or len(channel_id) < 3: + await update.message.reply_text("❌ ID چنل نامعتبر است.") + return + + playlist_url = user_data.get('selected_playlist_for_link') + + if self.config_manager.set_playlist_channel(playlist_url, channel_id): + playlist_name = None + for playlist in self.config_manager.get_playlists(): + if playlist['url'] == playlist_url: + playlist_name = playlist['name'] + break + + keyboard = [[ + InlineKeyboardButton("🔗 مشاهده تمام ارتباطات", callback_data="show_all_links"), + InlineKeyboardButton("📤 ارسال فوری", callback_data=f"send_playlist_{self.config_manager.get_playlists().index([p for p in self.config_manager.get_playlists() if p['url'] == playlist_url][0])}") + ]] + reply_markup = InlineKeyboardMarkup(keyboard) + + await update.message.reply_text( + f"✅ چنل برای پلی‌لیست '{playlist_name}' به `{channel_id}` تغییر یافت!\n\n" + f"حالا آهنگ‌های جدید این پلی‌لیست به این چنل ارسال می‌شوند.", + reply_markup=reply_markup, + parse_mode='Markdown' + ) + else: + await update.message.reply_text("❌ خطا در تنظیم چنل.") + + user_data.clear() + return async def button_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """Handle button callbacks""" @@ -621,7 +754,8 @@ async def button_callback(self, update: Update, context: ContextTypes.DEFAULT_TY data = query.data # Check admin for restricted actions - if data in ['add_playlist', 'remove_playlist', 'check_now'] or data.startswith('send_playlist_'): + if data in ['add_playlist', 'remove_playlist', 'check_now', 'show_link_menu'] or \ + data.startswith('send_playlist_') or data.startswith('link_playlist_'): if not self.is_admin(user_id): await query.edit_message_text("⛔️ فقط ادمین‌ها می‌توانند این عملیات را انجام دهند.") return @@ -652,6 +786,23 @@ async def button_callback(self, update: Update, context: ContextTypes.DEFAULT_TY elif data == "show_help": await self.show_help_callback(query) + elif data == "show_link_menu": + # نمایش منوی ارتباط پلی‌لیست به چنل + await self.show_link_menu_callback(query, context) + + elif data.startswith("link_playlist_"): + # کاربر یک پلی‌لیست را برای ارتباط انتخاب کرد + playlist_index = int(data.split("_")[-1]) + await self.handle_link_playlist_selection(query, context, playlist_index) + + elif data == "cancel_action": + await query.edit_message_text("❌ عملیات لغو شد.") + context.user_data.clear() + + elif data == "show_all_links": + # نمایش تمام ارتباطات پلی‌لیست‌ها با چنل‌ها + await self.show_all_links_callback(query) + elif data.startswith("send_playlist_"): # Send specific playlist immediately playlist_index = int(data.split("_")[-1]) @@ -771,6 +922,91 @@ async def show_help_callback(self, query): await query.edit_message_text(help_text, reply_markup=reply_markup) + async def show_link_menu_callback(self, query, context): + """نمایش منوی ارتباط پلی‌لیست به چنل""" + playlists = self.config_manager.get_playlists() + + if not playlists: + await query.edit_message_text("📭 هیچ پلی‌لیستی برای ارتباط با چنل وجود ندارد.") + return + + # ایجاد دکمه‌های inline برای انتخاب پلی‌لیست + keyboard = [] + for i, playlist in enumerate(playlists): + current_channel = playlist.get('channel_id', 'تنظیم نشده') + keyboard.append([ + InlineKeyboardButton( + f"🎵 {playlist['name'][:30]} (چنل: {current_channel})", + callback_data=f"link_playlist_{i}" + ) + ]) + + keyboard.append([InlineKeyboardButton("❌ انصراف", callback_data="cancel_action")]) + reply_markup = InlineKeyboardMarkup(keyboard) + + await query.edit_message_text( + "🔗 *ارتباط پلی‌لیست به چنل*\n\n" + "لطفا پلی‌لیستی که می‌خواهید به چنل جدید مرتبط کنید را انتخاب کنید:", + reply_markup=reply_markup, + parse_mode='Markdown' + ) + + async def handle_link_playlist_selection(self, query, context, playlist_index: int): + """مدیریت انتخاب پلی‌لیست برای ارتباط به چنل""" + playlists = self.config_manager.get_playlists() + + if 0 <= playlist_index < len(playlists): + playlist = playlists[playlist_index] + current_channel = playlist.get('channel_id', 'تنظیم نشده') + + context.user_data['selected_playlist_for_link'] = playlist['url'] + context.user_data['awaiting_new_channel_id'] = True + + await query.edit_message_text( + f"🎵 پلی‌لیست انتخاب شده: *{playlist['name']}*\n" + f"📺 چنل فعلی: `{current_channel}`\n\n" + f"لطفا ID چنل جدید را ارسال کنید:\n" + f"مثال: @channelname یا -1001234567890\n\n" + f"یا /cancel برای لغو", + parse_mode='Markdown' + ) + else: + await query.edit_message_text("❌ پلی‌لیست یافت نشد.") + + async def show_all_links_callback(self, query): + """نمایش تمام ارتباطات پلی‌لیست‌ها با چنل‌ها""" + playlists = self.config_manager.get_playlists() + + if not playlists: + await query.edit_message_text("📭 هیچ پلی‌لیستی ثبت نشده است.") + return + + message = "🔗 *ارتباط پلی‌لیست‌ها با چنل‌ها:*\n\n" + + for i, playlist in enumerate(playlists, 1): + channel_id = playlist.get('channel_id', '❌ تنظیم نشده') + track_count = playlist.get('track_count', 0) + last_check = playlist.get('last_check', 'هرگز') + + if last_check != 'هرگز': + try: + last_check = datetime.fromisoformat(last_check).strftime('%Y-%m-%d %H:%M') + except: + pass + + message += f"{i}. 🎵 *{playlist['name']}*\n" + message += f" 📺 چنل: `{channel_id}`\n" + message += f" 📊 تعداد آهنگ: {track_count}\n" + message += f" 🕐 آخرین چک: {last_check}\n\n" + + keyboard = [[ + InlineKeyboardButton("🔗 تغییر ارتباط", callback_data="show_link_menu"), + InlineKeyboardButton("🔙 بازگشت", callback_data="list_playlists") + ]] + reply_markup = InlineKeyboardMarkup(keyboard) + + await query.edit_message_text(message, reply_markup=reply_markup, parse_mode='Markdown') + async def send_specific_playlist(self, query, playlist_index: int): """Send specific playlist immediately""" playlists = self.config_manager.get_playlists() @@ -995,6 +1231,8 @@ def run(self): app.add_handler(CommandHandler("start", self.start_command)) app.add_handler(CommandHandler("help", self.help_command)) app.add_handler(CommandHandler("addplaylist", self.add_playlist_command)) + app.add_handler(CommandHandler("linkplaylist", self.link_playlist_command)) + app.add_handler(CommandHandler("showlinks", self.show_links_command)) app.add_handler(CommandHandler("setchannel", self.set_channel_command)) app.add_handler(CommandHandler("listplaylists", self.list_playlists_command)) app.add_handler(CommandHandler("removeplaylist", self.remove_playlist_command)) From 1f5517e4504701d18f809bcca46d665bebfce492 Mon Sep 17 00:00:00 2001 From: 11linear11 <11linearlin11@gmail.com> Date: Wed, 15 Oct 2025 14:59:48 -0400 Subject: [PATCH 2/3] add multichanell update seems work --- ERROR_HANDLING.md | 300 ++++++++++++++++++++++++++++++++++++++++++++++ bot.py | 287 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 543 insertions(+), 44 deletions(-) create mode 100644 ERROR_HANDLING.md diff --git a/ERROR_HANDLING.md b/ERROR_HANDLING.md new file mode 100644 index 0000000..1660cef --- /dev/null +++ b/ERROR_HANDLING.md @@ -0,0 +1,300 @@ +# 🛡️ راهنمای مدیریت خطاها و Timeout + +این سند توضیح می‌دهد که ربات چگونه خطاها و timeout‌ها را مدیریت می‌کند. + +## 🎯 انواع خطاها + +### 1. Timeout Errors ⏱️ + +**علت:** +- فایل بزرگ +- اتصال اینترنت ضعیف +- سرور Telegram پاسخ نمی‌دهد + +**راه حل ربات:** +- ✅ 3 بار تلاش مجدد +- ✅ Exponential backoff (5s, 10s, 15s) +- ✅ Timeout قابل تنظیم (پیش‌فرض: 120 ثانیه) +- ✅ گزارش به ادمین در صورت شکست نهایی + +**لاگ نمونه:** +``` +⏱️ Timeout on attempt 1/3 for Coffee +⏳ Waiting 5s before retry... +📤 Attempt 2/3: Sending Coffee +✅ Successfully sent: Coffee +``` + +### 2. Rate Limiting (Flood Control) 🚫 + +**علت:** +- ارسال خیلی سریع به Telegram +- محدودیت تعداد پیام در واحد زمان + +**راه حل ربات:** +- ✅ تشخیص خطای "flood" یا "too many requests" +- ✅ صبر 60 ثانیه برای اولین بار +- ✅ صبر 120 ثانیه برای بار دوم +- ✅ صبر 180 ثانیه برای بار سوم +- ✅ فاصله 3 ثانیه بین ارسال موفق +- ✅ فاصله 5 ثانیه بعد از شکست + +**لاگ نمونه:** +``` +🚫 Rate limited. Waiting 60s... +📤 Retry after cooldown... +``` + +### 3. Channel Errors 📺 + +**علت:** +- ربات در چنل Admin نیست +- چنل پیدا نشد +- ID چنل اشتباه است + +**راه حل ربات:** +- ✅ تشخیص خطای "chat not found" +- ✅ عدم تلاش مجدد (خطای دائمی) +- ✅ اطلاع فوری به ادمین +- ✅ توقف ارسال برای جلوگیری از هدر رفت منابع + +**لاگ نمونه:** +``` +❌ Channel not found or bot not admin: @wrongchannel +❌ خطا: چنلی برای این پلی‌لیست تنظیم نشده است! +``` + +### 4. File Size Errors 📦 + +**علت:** +- فایل بیش از 50 مگابایت +- محدودیت Telegram Bot API + +**راه حل ربات:** +- ✅ نمایش سایز فایل قبل از ارسال +- ✅ تشخیص خطای "file is too big" +- ✅ عدم تلاش مجدد +- ✅ گزارش فایل‌های بزرگ به ادمین + +**لاگ نمونه:** +``` +📦 File size: 52.3 MB +❌ File too large: Long Song (52.3 MB) +``` + +### 5. Network Errors 🌐 + +**علت:** +- قطعی اینترنت +- مشکل DNS +- Firewall + +**راه حل ربات:** +- ✅ 3 بار تلاش مجدد +- ✅ صبر بین تلاش‌ها +- ✅ ادامه با آهنگ بعدی در صورت شکست +- ✅ اطلاع به ادمین برای خطاهای حیاتی + +## 🔧 تنظیمات + +### تغییر Timeout + +در `bot.py` خط مربوط به `send_audio_to_channel`: + +```python +async def send_audio_to_channel( + self, file_path: str, track_name: str, + artist_name: str, channel_id: str, + max_retries: int = 3, # تعداد تلاش + timeout: int = 120 # ثانیه (تغییر دهید) +) -> bool: +``` + +**مقادیر پیشنهادی:** +- اتصال سریع: `timeout = 60` +- اتصال معمولی: `timeout = 120` (پیش‌فرض) +- اتصال کند: `timeout = 180` +- فایل‌های بزرگ: `timeout = 300` + +### تغییر تعداد Retry + +```python +max_retries: int = 3 # تغییر به 5 برای ثبات بیشتر +``` + +### تغییر Rate Limiting + +در بخش `process_new_tracks`: + +```python +# بعد از ارسال موفق +await asyncio.sleep(3) # تغییر به 5 برای امنیت بیشتر + +# بعد از شکست +await asyncio.sleep(5) # تغییر به 10 برای cooldown بیشتر +``` + +## 📊 گزارش‌ها + +### گزارش موفقیت ✅ + +``` +📊 Summary: 8 succeeded, 2 failed out of 10 tracks +✅ Successfully sent 8 tracks to @channel +``` + +### گزارش شکست به ادمین ❌ + +``` +⚠️ گزارش ارسال آهنگ‌ها: + +✅ موفق: 8 +❌ ناموفق: 2 + +آهنگ‌های ناموفق: +• Coffee - beabadoobee +• Long Track Name - Artist +``` + +### لاگ دقیق 🔍 + +``` +📤 Attempt 1/3: Sending Coffee +📦 File size: 3.45 MB +📤 Sending: Coffee - beabadoobee +📁 File path: /app/downloads/Coffee.mp3 +✅ Successfully sent: Coffee - beabadoobee to @channel +✅ Marked as sent: Coffee +🗑️ Cleaned up: /app/downloads/Coffee.mp3 +``` + +## 🚨 خطاهای حیاتی + +ربات برای خطاهای زیر فوراً به ادمین اطلاع می‌دهد: + +1. **NetworkError**: مشکل شبکه +2. **TimedOut**: تمام تلاش‌ها timeout شدند +3. **RetryAfter**: Telegram درخواست کاهش سرعت داده +4. **Conflict**: bot instance دیگری در حال اجرا است +5. **Unauthorized**: توکن نامعتبر یا منقضی شده + +**پیام نمونه:** +``` +🚨 خطای حیاتی: + +نوع: NetworkError +پیام: httpx.RemoteProtocolError: Server disconnected +``` + +## 🔍 عیب‌یابی + +### مشکل: همه آهنگ‌ها timeout می‌شوند + +**راه حل‌ها:** +1. Timeout را افزایش دهید (180 یا 300) +2. اتصال اینترنت را بررسی کنید +3. از VPN استفاده کنید اگر Telegram فیلتر است +4. فایل‌های بزرگ را کوچک‌تر کنید (تنظیم bitrate) + +### مشکل: Rate limiting مداوم + +**راه حل‌ها:** +1. فاصله بین ارسال‌ها را افزایش دهید (5-10 ثانیه) +2. تعداد پلی‌لیست‌ها را کاهش دهید +3. بازه چک را افزایش دهید (12 ساعت به جای 6) + +### مشکل: Channel not found + +**راه حل‌ها:** +1. مطمئن شوید ربات در چنل Admin است +2. ID چنل را دوباره بررسی کنید +3. از `/linkplaylist` استفاده کنید +4. دسترسی‌های ربات را بررسی کنید + +### مشکل: فایل‌ها خیلی بزرگ هستند + +**راه حل‌ها:** +1. در `config.json` bitrate را کاهش دهید: +```json +"settings": { + "bitrate": "96" // به جای 128 +} +``` + +2. یا در کد تغییر دهید: +```python +bitrate=self.config_manager.config['settings']['bitrate'] +``` + +## 📈 بهترین شیوه‌ها + +### 1. تنظیمات متعادل ⚖️ + +```python +max_retries = 3 # کافی برای اکثر موارد +timeout = 120 # 2 دقیقه +sleep_after_success = 3 # جلوگیری از rate limit +sleep_after_fail = 5 # cooldown بیشتر +``` + +### 2. مانیتورینگ 📊 + +لاگ‌ها را بررسی کنید: +```bash +docker-compose logs -f | grep -E "❌|⚠️|🚨" +``` + +فقط خطاها: +```bash +docker-compose logs | grep ERROR +``` + +آمار موفقیت: +```bash +docker-compose logs | grep "📊 Summary" +``` + +### 3. Backup و Recovery 💾 + +در صورت خطاهای مکرر: + +```bash +# Backup database +cp tracks_database.json tracks_database.json.backup + +# Restart +docker-compose restart + +# اگر نیاز به rebuild است +docker-compose down +docker-compose up -d --build +``` + +### 4. Notification Settings 🔔 + +تنظیم اطلاع‌رسانی‌ها: + +**خطاهای مهم:** +- ✅ Channel errors +- ✅ ARL problems +- ✅ Network failures + +**خطاهای عادی:** +- ❌ Single timeout (retry می‌شود) +- ❌ Rate limiting (خودکار مدیریت می‌شود) + +## 🎯 خلاصه + +| خطا | Retry | Backoff | اطلاع به ادمین | +|-----|-------|---------|----------------| +| Timeout | ✅ 3x | 5s, 10s, 15s | در صورت شکست نهایی | +| Rate Limit | ✅ 3x | 60s, 120s, 180s | خیر | +| Channel Error | ❌ | - | ✅ فوری | +| File Too Big | ❌ | - | ✅ فوری | +| Network Error | ✅ 3x | 5s, 10s, 15s | برای خطاهای حیاتی | + +--- + +**آخرین بروزرسانی**: اکتبر 2025 +**نسخه**: 2.1 +**قابلیت**: Advanced Error Handling & Timeout Management diff --git a/bot.py b/bot.py index 1f2d7bb..4ac4658 100644 --- a/bot.py +++ b/bot.py @@ -117,6 +117,16 @@ def set_playlist_channel(self, url: str, channel_id: str) -> bool: self.save_config() return True return False + + def update_playlist_check(self, url: str, track_count: int) -> bool: + """Update last check time and track count for a playlist""" + for playlist in self.config['playlists']: + if playlist['url'] == url: + playlist['last_check'] = datetime.now().isoformat() + playlist['track_count'] = track_count + self.save_config() + return True + return False class SpotifyTelegramBot: @@ -1024,36 +1034,100 @@ async def send_specific_playlist(self, query, playlist_index: int): await query.edit_message_text("❌ پلی‌لیست یافت نشد.") async def send_audio_to_channel(self, file_path: str, track_name: str, - artist_name: str, channel_id: str) -> bool: - """Send audio file to specified channel""" - try: - if not os.path.exists(file_path): - logger.error(f"File not found: {file_path}") - return False - - caption = ( - f"🎵 {track_name}\n" - f"🎤 {artist_name}\n\n" - f"#موسیقی #دانلود" - ) - - bot = Bot(token=self.telegram_token) - with open(file_path, 'rb') as audio: - await bot.send_audio( - chat_id=channel_id, - audio=audio, - caption=caption, - parse_mode='HTML', - title=track_name, - performer=artist_name - ) - - logger.info(f"✅ Sent: {track_name} - {artist_name} to {channel_id}") - return True - - except Exception as e: - logger.error(f"Error sending file: {e}") + artist_name: str, channel_id: str, + max_retries: int = 3, timeout: int = 120) -> bool: + """Send audio file to specified channel with retry mechanism""" + + if not os.path.exists(file_path): + logger.error(f"❌ File not found: {file_path}") return False + + # Get file size for logging + file_size = os.path.getsize(file_path) / (1024 * 1024) # MB + logger.info(f"📦 File size: {file_size:.2f} MB") + + caption = ( + f"🎵 {track_name}\n" + f"🎤 {artist_name}\n\n" + f"#موسیقی #دانلود" + ) + + bot = Bot(token=self.telegram_token) + + for attempt in range(1, max_retries + 1): + try: + logger.info(f"📤 Attempt {attempt}/{max_retries}: Sending {track_name}") + + with open(file_path, 'rb') as audio: + # Set timeout for send operation + await asyncio.wait_for( + bot.send_audio( + chat_id=channel_id, + audio=audio, + caption=caption, + parse_mode='HTML', + title=track_name, + performer=artist_name, + read_timeout=timeout, + write_timeout=timeout, + connect_timeout=30, + pool_timeout=30 + ), + timeout=timeout + 30 # Total timeout + ) + + logger.info(f"✅ Successfully sent: {track_name} - {artist_name} to {channel_id}") + return True + + except asyncio.TimeoutError: + logger.warning(f"⏱️ Timeout on attempt {attempt}/{max_retries} for {track_name}") + if attempt < max_retries: + wait_time = attempt * 5 # Exponential backoff + logger.info(f"⏳ Waiting {wait_time}s before retry...") + await asyncio.sleep(wait_time) + else: + logger.error(f"❌ Failed after {max_retries} attempts (timeout): {track_name}") + return False + + except TelegramError as e: + error_msg = str(e) + logger.error(f"⚠️ Telegram error on attempt {attempt}/{max_retries}: {error_msg}") + + # Handle specific Telegram errors + if "file is too big" in error_msg.lower(): + logger.error(f"❌ File too large: {track_name} ({file_size:.2f} MB)") + return False + elif "chat not found" in error_msg.lower() or "channel" in error_msg.lower(): + logger.error(f"❌ Channel not found or bot not admin: {channel_id}") + return False + elif "flood" in error_msg.lower() or "too many requests" in error_msg.lower(): + # Rate limiting + wait_time = 60 * attempt # Wait longer for rate limits + logger.warning(f"🚫 Rate limited. Waiting {wait_time}s...") + await asyncio.sleep(wait_time) + if attempt == max_retries: + return False + else: + # Other Telegram errors - retry + if attempt < max_retries: + wait_time = attempt * 10 + logger.info(f"⏳ Retrying in {wait_time}s...") + await asyncio.sleep(wait_time) + else: + logger.error(f"❌ Failed after {max_retries} attempts: {track_name}") + return False + + except Exception as e: + logger.error(f"❌ Unexpected error on attempt {attempt}/{max_retries}: {type(e).__name__}: {e}") + if attempt < max_retries: + wait_time = attempt * 5 + logger.info(f"⏳ Retrying in {wait_time}s...") + await asyncio.sleep(wait_time) + else: + logger.error(f"❌ Failed after {max_retries} attempts: {track_name}") + return False + + return False async def check_playlist(self, playlist_url: str): """Check a single playlist for new tracks""" @@ -1080,11 +1154,13 @@ async def check_playlist(self, playlist_url: str): playlist_data = self.tracks_db[playlist_url] new_tracks = [] + unsent_tracks = [] - # Find new tracks + # Find new tracks and unsent tracks for track in tracks: track_id = track['id'] if track_id not in playlist_data['tracks']: + # This is a completely new track new_tracks.append(track) playlist_data['tracks'][track_id] = { 'name': track['name'], @@ -1092,6 +1168,9 @@ async def check_playlist(self, playlist_url: str): 'added_at': datetime.now().isoformat(), 'sent': False } + elif not playlist_data['tracks'][track_id].get('sent', False): + # This track exists but hasn't been sent yet + unsent_tracks.append(track) playlist_data['total_tracks'] = len(tracks) self._save_tracks_db() @@ -1099,11 +1178,17 @@ async def check_playlist(self, playlist_url: str): # Update config self.config_manager.update_playlist_check(playlist_url, len(tracks)) - if new_tracks: - logger.info(f"🎵 Found {len(new_tracks)} new tracks") - await self.process_new_tracks(playlist_url, new_tracks) + # Combine new and unsent tracks + tracks_to_process = new_tracks + unsent_tracks + + if tracks_to_process: + if new_tracks: + logger.info(f"🎵 Found {len(new_tracks)} new tracks") + if unsent_tracks: + logger.info(f"📤 Found {len(unsent_tracks)} unsent tracks") + await self.process_new_tracks(playlist_url, tracks_to_process) else: - logger.info("✅ No new tracks") + logger.info("✅ No new or unsent tracks") except Exception as e: logger.error(f"Error checking playlist: {e}", exc_info=True) @@ -1111,6 +1196,8 @@ async def check_playlist(self, playlist_url: str): async def process_new_tracks(self, playlist_url: str, tracks: List[dict]): """Download and send new tracks""" try: + logger.info(f"🎬 Processing {len(tracks)} tracks for {playlist_url}") + # Check if ARL is configured if not self.deezer_arl: logger.error("❌ Deezer ARL not configured!") @@ -1127,12 +1214,33 @@ async def process_new_tracks(self, playlist_url: str, tracks: List[dict]): pass return + # Get channel ID for this playlist + channel_id = self.config_manager.get_playlist_channel(playlist_url) + if not channel_id: + logger.error(f"❌ No channel configured for playlist: {playlist_url}") + # Notify admin + for admin_id in self.admin_ids: + try: + bot = Bot(token=self.telegram_token) + await bot.send_message( + chat_id=admin_id, + text=f"❌ خطا: چنلی برای این پلی‌لیست تنظیم نشده است!\n\n" + f"URL: {playlist_url}\n\n" + f"از دستور /linkplaylist برای تنظیم چنل استفاده کنید." + ) + except: + pass + return + + logger.info(f"📺 Target channel: {channel_id}") + tracks_to_download = [ (track['name'], ', '.join(track['artists'])) for track in tracks ] logger.info(f"📥 Downloading {len(tracks_to_download)} tracks...") + logger.info(f"📋 Tracks to download: {[t[0] for t in tracks_to_download]}") # Download tracks downloaded = await asyncio.to_thread( @@ -1144,31 +1252,47 @@ async def process_new_tracks(self, playlist_url: str, tracks: List[dict]): if not downloaded: logger.warning("⚠️ No tracks were downloaded") + logger.warning("⚠️ This might be due to Deezer download issues or ARL problems") return - logger.info(f"✅ Downloaded {len(downloaded)} tracks") + logger.info(f"✅ Downloaded {len(downloaded)} tracks: {[t[0] for t in downloaded]}") # Send to channel playlist_data = self.tracks_db[playlist_url] success_count = 0 + failed_count = 0 + failed_tracks = [] - # Get channel ID for this playlist - channel_id = self.config_manager.get_playlist_channel(playlist_url) - if not channel_id: - logger.error(f"❌ No channel configured for playlist: {playlist_url}") - return + logger.info(f"📤 Starting to send {len(downloaded)} tracks to channel {channel_id}...") for track_name, artist_name, file_path in downloaded: - if await self.send_audio_to_channel(file_path, track_name, artist_name, channel_id): + logger.info(f"📤 Sending: {track_name} - {artist_name}") + logger.info(f"📁 File path: {file_path}") + + # Try to send with retry mechanism + send_success = await self.send_audio_to_channel( + file_path, track_name, artist_name, channel_id + ) + + if send_success: # Mark as sent for track in tracks: if track['name'] == track_name: playlist_data['tracks'][track['id']]['sent'] = True playlist_data['sent_tracks'] += 1 success_count += 1 + logger.info(f"✅ Marked as sent: {track_name}") break - await asyncio.sleep(2) # Rate limiting + # Rate limiting between successful sends + await asyncio.sleep(3) + else: + failed_count += 1 + failed_tracks.append(f"{track_name} - {artist_name}") + logger.error(f"❌ Failed to send after all retries: {track_name}") + + # Still wait a bit before next track + await asyncio.sleep(5) # Clean up downloaded file try: @@ -1176,10 +1300,41 @@ async def process_new_tracks(self, playlist_url: str, tracks: List[dict]): os.remove(file_path) logger.info(f"🗑️ Cleaned up: {file_path}") except Exception as e: - logger.warning(f"Failed to delete file {file_path}: {e}") + logger.warning(f"⚠️ Failed to delete file {file_path}: {e}") + # Save database self._save_tracks_db() - logger.info(f"✅ Successfully sent {success_count}/{len(downloaded)} tracks to {channel_id}") + + # Log summary + logger.info(f"📊 Summary: {success_count} succeeded, {failed_count} failed out of {len(downloaded)} tracks") + + if success_count > 0: + logger.info(f"✅ Successfully sent {success_count} tracks to {channel_id}") + + # Notify admin about failures if any + if failed_count > 0: + logger.error(f"❌ Failed to send {failed_count} tracks") + error_message = ( + f"⚠️ گزارش ارسال آهنگ‌ها:\n\n" + f"✅ موفق: {success_count}\n" + f"❌ ناموفق: {failed_count}\n\n" + f"آهنگ‌های ناموفق:\n" + ) + for track in failed_tracks[:5]: # Only show first 5 + error_message += f"• {track}\n" + + if len(failed_tracks) > 5: + error_message += f"\n... و {len(failed_tracks) - 5} آهنگ دیگر" + + for admin_id in self.admin_ids: + try: + bot = Bot(token=self.telegram_token) + await bot.send_message( + chat_id=admin_id, + text=error_message + ) + except Exception as notify_error: + logger.error(f"Failed to notify admin {admin_id}: {notify_error}") except Exception as e: logger.error(f"Error processing tracks: {e}", exc_info=True) @@ -1222,11 +1377,55 @@ async def periodic_check(self): logger.error(f"Error in periodic check: {e}", exc_info=True) await asyncio.sleep(3600) # Retry in 1 hour + async def error_handler(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + """Global error handler""" + try: + logger.error(f"Exception while handling an update: {context.error}", exc_info=context.error) + + # Get error details + error_message = str(context.error) + error_type = type(context.error).__name__ + + # Notify user if possible + if update and update.effective_message: + try: + await update.effective_message.reply_text( + "❌ متأسفانه خطایی رخ داد. لطفا دوباره تلاش کنید.\n\n" + "در صورت تکرار، به ادمین اطلاع دهید." + ) + except: + pass + + # Notify admin for critical errors + critical_errors = [ + 'NetworkError', 'TimedOut', 'RetryAfter', + 'Conflict', 'Unauthorized' + ] + + if error_type in critical_errors: + for admin_id in self.admin_ids: + try: + bot = Bot(token=self.telegram_token) + await bot.send_message( + chat_id=admin_id, + text=f"🚨 خطای حیاتی:\n\n" + f"نوع: {error_type}\n" + f"پیام: {error_message[:200]}" + ) + except: + pass + + except Exception as e: + logger.error(f"Error in error handler: {e}", exc_info=True) + def run(self): """Run the bot""" # Create application app = Application.builder().token(self.telegram_token).build() + # Add error handler + app.add_error_handler(self.error_handler) + # Add handlers app.add_handler(CommandHandler("start", self.start_command)) app.add_handler(CommandHandler("help", self.help_command)) From 09ed5c1c8f6b9777d1d30d85a4de50adb87b3c01 Mon Sep 17 00:00:00 2001 From: 11linear11 <11linearlin11@gmail.com> Date: Wed, 15 Oct 2025 15:40:44 -0400 Subject: [PATCH 3/3] ui added --- UI_DESIGN.md | 406 +++++++++++++++++++++++++++++++++++++ bot.py | 563 ++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 850 insertions(+), 119 deletions(-) create mode 100644 UI_DESIGN.md diff --git a/UI_DESIGN.md b/UI_DESIGN.md new file mode 100644 index 0000000..a7822b9 --- /dev/null +++ b/UI_DESIGN.md @@ -0,0 +1,406 @@ +# 🎨 رابط کاربری جدید ربات اسپاتیفای + +## 🌟 نسخه 3.0 - Professional UI + +رابط کاربری ربات به طور کامل بازطراحی شده است تا تجربه‌ای حرفه‌ای‌تر و کاربرپسندتر ارائه دهد. + +--- + +## 📱 منوی اصلی + +### پیام خوش‌آمد جدید + +``` +╭─────────────────────╮ +│ 🎵 ربات اسپاتیفای │ +╰─────────────────────╯ + +👋 سلام کاربر عزیز! + +🤖 من ربات مدیریت و اشتراک‌گذاری پلی‌لیست‌های اسپاتیفای هستم. + +🔐 دسترسی: ادمین + +┌ 🎯 قابلیت‌های من: +├─ 📡 مانیتورینگ خودکار پلی‌لیست‌ها +├─ 🎼 دانلود و ارسال آهنگ‌های جدید +├─ 📺 پشتیبانی از چند چنل +└─ ⏰ بررسی هر 6 ساعت + +💫 از دکمه‌های زیر برای شروع استفاده کنید: +``` + +### دکمه‌های منوی اصلی (ادمین) + +``` +┌──────────────────────────┐ +│ 🎵 افزودن پلی‌لیست │ +├──────────────────────────┤ +│ 📋 مشاهده │ 🔗 ارتباط │ +│ پلی‌لیست‌ها │ چنل‌ها │ +├──────────────────────────┤ +│ 🔄 چک فوری │ 📊 آمار و │ +│ │ وضعیت │ +├──────────────────────────┤ +│ ⚙️ مدیریت │ 💡 راهنما │ +└──────────────────────────┘ +``` + +### دکمه‌های منوی اصلی (کاربر عادی) + +``` +┌────────────────────┐ +│ 📋 پلی‌ │ 📊 آمار │ +│ لیست‌ها │ │ +├────────────────────┤ +│ 💡 راهنما │ +└────────────────────┘ +``` + +--- + +## 🎵 افزودن پلی‌لیست (3 مرحله) + +### مرحله 1: لینک پلی‌لیست + +``` +╭──────────────────────╮ +│ 🎵 افزودن پلی‌لیست │ +╰──────────────────────╯ + +مرحله 1 از 3: لینک پلی‌لیست + +📎 لطفا لینک پلی‌لیست اسپاتیفای را ارسال کنید: + +مثال: +https://open.spotify.com/playlist/37i9dQZF1DX... + +💡 لینک را از اپلیکیشن Spotify کپی کنید + +❌ برای لغو: /cancel +``` + +### مرحله 2: نام پلی‌لیست + +``` +✅ لینک دریافت شد! + +╭──────────────────────╮ +│ 🎵 افزودن پلی‌لیست │ +╰──────────────────────╯ + +مرحله 2 از 3: نام پلی‌لیست + +📝 یک نام دلخواه برای این پلی‌لیست وارد کنید: + +مثال: پلی‌لیست راک من + +❌ برای لغو: /cancel +``` + +### مرحله 3: چنل مقصد + +``` +✅ نام دریافت شد! + +╭──────────────────────╮ +│ 🎵 افزودن پلی‌لیست │ +╰──────────────────────╯ + +مرحله 3 از 3: چنل مقصد + +📺 ID چنل مورد نظر را ارسال کنید: + +فرمت‌های مجاز: +▫️ @channelname (چنل عمومی) +▫️ -1001234567890 (چنل خصوصی) + +💡 ربات باید در چنل Admin باشد + +❌ برای لغو: /cancel +``` + +### پیام موفقیت + +``` +╭─────────────────╮ +│ ✅ موفقیت‌آمیز! │ +╰─────────────────╯ + +🎵 پلی‌لیست: پلی‌لیست راک من +📺 چنل: @rockchannel +📡 وضعیت: فعال + +✨ پلی‌لیست با موفقیت اضافه شد! + +💫 آهنگ‌های جدید به صورت خودکار +به چنل شما ارسال می‌شوند. + +🔽 می‌توانید الان هم چک کنید: +[دکمه: 📤 ارسال فوری] +[دکمه: 📋 مشاهده پلی‌لیست‌ها] +``` + +--- + +## 📊 صفحه آمار و وضعیت + +``` +╭────────────────────╮ +│ 📊 آمار و وضعیت │ +╰────────────────────╯ + +📈 آمار کلی: +┌───────────────────── +├─ 🎵 پلی‌لیست‌ها: 5 +├─ 📺 چنل‌ها: 3 +├─ 🎼 کل آهنگ‌ها: 250 +├─ ✅ ارسال شده: 230 +├─ ⏳ در انتظار: 20 +└─ 📊 نرخ موفقیت: 92.0% + +⚙️ تنظیمات: +┌───────────────────── +├─ ⏰ بازه چک: 6 ساعت +├─ 🎚️ کیفیت: 128kbps +├─ 🎧 Deezer ARL: 🟢 فعال +└─ 🤖 وضعیت: 🟢 فعال + +💡 نکته: ربات به صورت خودکار +هر 6 ساعت پلی‌لیست‌ها را بررسی می‌کند. + +[دکمه: 🔄 رفرش] [دکمه: 📋 پلی‌لیست‌ها] +[دکمه: 🔙 بازگشت] +``` + +--- + +## 💡 صفحه راهنما + +``` +╭──────────────────╮ +│ 💡 راهنمای کامل │ +╰──────────────────╯ + +📚 دستورات اصلی: + +🎵 افزودن پلی‌لیست +└─ /addplaylist + ورودی‌ها: لینک، نام، چنل + +🔗 مدیریت چنل‌ها +├─ /linkplaylist → تغییر چنل +├─ /showlinks → مشاهده ارتباطات +└─ /setchannel → تنظیم چنل + +📋 مشاهده اطلاعات +├─ /listplaylists → لیست پلی‌لیست‌ها +├─ /stats → آمار کامل +└─ /help → این راهنما + +⚙️ دستورات مدیریتی: + +🗑 حذف → /removeplaylist +🔄 چک فوری → /checkplaylists +🎧 تنظیم ARL → /setuparl + +┌───────────────────── +│ ℹ️ اطلاعات: +├─ ⏰ چک خودکار هر 6 ساعت +├─ 🎼 کیفیت 128kbps +├─ 📺 پشتیبانی چند چنل +└─ 💾 ذخیره خودکار + +💬 از دکمه‌های منو استفاده کنید! + +[دکمه: 🔙 بازگشت به منو] +``` + +--- + +## ⚙️ پنل مدیریت + +``` +╭────────────────╮ +│ ⚙️ پنل مدیریت │ +╰────────────────╯ + +عملیات مدیریتی: + +▫️ افزودن پلی‌لیست جدید +▫️ حذف پلی‌لیست +▫️ تنظیم و تغییر چنل +▫️ چک و ارسال فوری + +💡 یکی از گزینه‌های زیر را انتخاب کنید: + +┌──────────────────────────┐ +│ ➕ افزودن │ 🗑 حذف │ +│ پلی‌لیست │ پلی‌لیست │ +├──────────────────────────┤ +│ 🔗 تنظیم │ 🔄 چک فوری │ +│ چنل │ │ +├──────────────────────────┤ +│ 🔙 بازگشت │ +└──────────────────────────┘ +``` + +--- + +## 🎨 ویژگی‌های طراحی + +### ✨ استفاده از Box Drawing + +``` +╭─────────────╮ ┌─────────────┐ +│ عنوان │ │ عنوان │ +╰─────────────╯ └─────────────┘ +``` + +### 📊 نمایش سلسله‌مراتبی + +``` +┌─ مورد اول +├─ مورد دوم +├─ مورد سوم +└─ مورد آخر +``` + +### 🎯 دکمه‌های منظم + +- دکمه‌های 2 ستونه برای بهترین استفاده از فضا +- متن واضح و کوتاه روی هر دکمه +- ایموجی مناسب برای هر عملیات + +### 🌈 رنگ‌بندی با ایموجی + +- 🟢 سبز: فعال / موفق +- 🔴 قرمز: غیرفعال / خطا +- 🟡 زرد: هشدار / انتظار +- 🔵 آبی: اطلاعات + +--- + +## 📱 تجربه کاربری + +### مزایا + +✅ **واضح و خوانا**: متن‌های ساختارمند و مرتب +✅ **راهنمایی گام‌به‌گام**: فرآیند 3 مرحله‌ای ساده +✅ **بازخورد فوری**: پیام‌های تایید و خطا واضح +✅ **ناوبری آسان**: دکمه بازگشت در همه جا +✅ **تعاملی**: دکمه‌های inline برای سرعت بیشتر +✅ **حرفه‌ای**: طراحی تمیز و مدرن + +### راهنماهای بصری + +📎 لینک → 📝 نام → 📺 چنل → ✅ موفقیت + +``` +مرحله 1 مرحله 2 مرحله 3 تکمیل + │ │ │ │ + ●───────────●───────────●──────────● +``` + +--- + +## 🔧 تنظیمات ظاهری + +### فونت‌ها + +- استفاده از HTML formatting: `` برای bold +- استفاده از `` برای کدها +- استفاده از `` برای متن‌های کمکی + +### ایموجی‌ها + +| دسته | ایموجی‌ها | +|------|-----------| +| موسیقی | 🎵 🎼 🎤 🎧 🎸 | +| وضعیت | ✅ ❌ ⚠️ ⏳ 🔄 | +| عملیات | ➕ 🗑 📤 📥 🔗 | +| اطلاعات | 📊 📋 📺 💡 ⚙️ | +| رنگ‌ها | 🟢 🔴 🟡 🔵 ⚪️ | + +--- + +## 🎯 نکات برای توسعه‌دهندگان + +### ساختار پیام + +```python +message = """ +╭──────────────╮ +│ عنوان │ +╰──────────────╯ + +بخش اصلی + +┌───────────── +├─ آیتم 1 +├─ آیتم 2 +└─ آیتم 3 + +💡 نکته مهم +""" +``` + +### دکمه‌سازی + +```python +keyboard = [ + [ + InlineKeyboardButton("🎵 متن", callback_data="action1"), + InlineKeyboardButton("📊 متن", callback_data="action2") + ], + [ + InlineKeyboardButton("🔙 بازگشت", callback_data="back") + ] +] +``` + +### پیام‌های خطا + +```python +await update.message.reply_text( + "❌ عنوان خطا\n\n" + "توضیح خطا\n\n" + "💡 راهنمایی", + parse_mode='HTML' +) +``` + +--- + +## 📈 مقایسه قبل و بعد + +### قبل ❌ + +``` +سلام! +من یک ربات هستم. +دستورات: +/start - شروع +/help - کمک +``` + +### بعد ✅ + +``` +╭─────────────────────╮ +│ 🎵 ربات اسپاتیفای │ +╰─────────────────────╯ + +👋 سلام کاربر عزیز! + +🤖 من ربات مدیریت و اشتراک‌گذاری +پلی‌لیست‌های اسپاتیفای هستم. + +💫 از دکمه‌های زیر استفاده کنید: +``` + +--- + +**نسخه**: 3.0 +**تاریخ**: اکتبر 2025 +**وضعیت**: ✅ فعال و آماده استفاده diff --git a/bot.py b/bot.py index 4ac4658..b3b7b72 100644 --- a/bot.py +++ b/bot.py @@ -189,115 +189,176 @@ def is_admin(self, user_id: int) -> bool: async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """Handle /start command""" user = update.effective_user + is_admin = self.is_admin(user.id) # Create inline keyboard with quick actions - keyboard = [] - - if self.is_admin(user.id): + if is_admin: keyboard = [ [ - InlineKeyboardButton("➕ افزودن پلی‌لیست", callback_data="add_playlist"), - InlineKeyboardButton("📋 لیست پلی‌لیست‌ها", callback_data="list_playlists") + InlineKeyboardButton("🎵 افزودن پلی‌لیست", callback_data="add_playlist"), ], [ - InlineKeyboardButton("🔄 چک و ارسال فوری", callback_data="check_now"), - InlineKeyboardButton("📊 آمار", callback_data="show_stats") + InlineKeyboardButton("📋 مشاهده پلی‌لیست‌ها", callback_data="list_playlists"), + InlineKeyboardButton("🔗 ارتباط چنل‌ها", callback_data="show_all_links") ], [ - InlineKeyboardButton("❌ حذف پلی‌لیست", callback_data="remove_playlist"), - InlineKeyboardButton("❓ راهنما", callback_data="show_help") + InlineKeyboardButton("🔄 چک فوری", callback_data="check_now"), + InlineKeyboardButton("📊 آمار و وضعیت", callback_data="show_stats") + ], + [ + InlineKeyboardButton("⚙️ مدیریت", callback_data="show_management"), + InlineKeyboardButton("💡 راهنما", callback_data="show_help") ] ] else: keyboard = [ [ - InlineKeyboardButton("📋 لیست پلی‌لیست‌ها", callback_data="list_playlists"), + InlineKeyboardButton("📋 پلی‌لیست‌ها", callback_data="list_playlists"), InlineKeyboardButton("📊 آمار", callback_data="show_stats") ], [ - InlineKeyboardButton("❓ راهنما", callback_data="show_help") + InlineKeyboardButton("💡 راهنما", callback_data="show_help") ] ] reply_markup = InlineKeyboardMarkup(keyboard) - welcome_message = f""" -👋 سلام {user.first_name}! + # Create beautiful welcome message + access_level = "🔐 دسترسی: ادمین" if is_admin else "👤 دسترسی: کاربر" + + welcome_message = f"""╭─────────────────────╮ +│ 🎵 ربات اسپاتیفای │ +╰─────────────────────╯ + +👋 سلام {user.first_name} عزیز! + +🤖 من ربات مدیریت و اشتراک‌گذاری پلی‌لیست‌های اسپاتیفای هستم. -من یک ربات مدیریت پلی‌لیست اسپاتیفای هستم. +{access_level} -📋 دستورات موجود: -/addplaylist - افزودن پلی‌لیست جدید -/linkplaylist - ارتباط پلی‌لیست به چنل خاص -/showlinks - نمایش ارتباط پلی‌لیست‌ها با چنل‌ها -/setchannel - تنظیم چنل برای پلی‌لیست -/listplaylists - نمایش پلی‌لیست‌ها -/removeplaylist - حذف پلی‌لیست -/checkplaylists - چک دستی پلی‌لیست‌ها -/stats - آمار ربات -/help - راهنما +┌ 🎯 قابلیت‌های من: +├─ 📡 مانیتورینگ خودکار پلی‌لیست‌ها +├─ 🎼 دانلود و ارسال آهنگ‌های جدید +├─ 📺 پشتیبانی از چند چنل +└─ ⏰ بررسی هر 6 ساعت -{'⚠️ شما دسترسی ادمین ندارید.' if not self.is_admin(user.id) else '✅ شما ادمین هستید - از دکمه‌های زیر استفاده کنید:'} +💫 از دکمه‌های زیر برای شروع استفاده کنید: """ - await update.message.reply_text(welcome_message, reply_markup=reply_markup) + + # Add admin commands if user is admin + if is_admin: + welcome_message = welcome_message.replace( + "💫 از دکمه‌های زیر برای شروع استفاده کنید:", + """ +┌ ⚡️ دستورات سریع: +├─ /addplaylist → افزودن پلی‌لیست +├─ /linkplaylist → ارتباط به چنل +├─ /showlinks → مشاهده ارتباطات +└─ /checkplaylists → چک فوری + +💫 از دکمه‌های زیر برای شروع استفاده کنید:""" + ) + + await update.message.reply_text( + welcome_message, + reply_markup=reply_markup, + parse_mode='HTML' + ) async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """Handle /help command""" + is_admin = self.is_admin(update.effective_user.id) + help_text = """ -📖 راهنمای استفاده: +╭──────────────────╮ +│ � راهنمای کامل │ +╰──────────────────╯ -1️⃣ افزودن پلی‌لیست: -/addplaylist -سپس لینک پلی‌لیست، نام و ID چنل را ارسال کنید. +📚 دستورات اصلی: -2️⃣ ارتباط پلی‌لیست به چنل: -/linkplaylist -برای تغییر چنل یک پلی‌لیست موجود +🎵 افزودن پلی‌لیست +└─ /addplaylist + ورودی‌ها: لینک، نام، چنل -3️⃣ نمایش ارتباط پلی‌لیست‌ها با چنل‌ها: -/showlinks -برای مشاهده همه پلی‌لیست‌ها و چنل‌های مرتبط +🔗 مدیریت چنل‌ها +├─ /linkplaylist → تغییر چنل پلی‌لیست +├─ /showlinks → مشاهده ارتباطات +└─ /setchannel → تنظیم چنل (روش قدیم) -4️⃣ تنظیم چنل برای پلی‌لیست: -/setchannel -سپس شماره پلی‌لیست و ID چنل را وارد کنید. +📋 مشاهده اطلاعات +├─ /listplaylists → لیست پلی‌لیست‌ها +├─ /stats → آمار کامل ربات +└─ /help → این راهنما -5️⃣ مشاهده پلی‌لیست‌ها: -/listplaylists +""" + + if is_admin: + help_text += """⚙️ دستورات مدیریتی: -6️⃣ حذف پلی‌لیست: -/removeplaylist -سپس شماره پلی‌لیست را وارد کنید. +🗑 حذف پلی‌لیست +└─ /removeplaylist + حذف یک پلی‌لیست -7️⃣ چک دستی: -/checkplaylists -برای چک فوری تمام پلی‌لیست‌ها +🔄 چک دستی +└─ /checkplaylists + بررسی فوری همه پلی‌لیست‌ها -8️⃣ آمار: -/stats -برای مشاهده آمار ربات +🎧 تنظیم Deezer +├─ /setuparl → راهنمای دریافت ARL +└─ /setarl [TOKEN] → ثبت توکن -9️⃣ تنظیم Deezer ARL: -/setuparl - راهنمای دریافت ARL -/setarl TOKEN - تنظیم ARL token +""" + + help_text += """ +┌───────────────────── +│ ℹ️ اطلاعات مفید: +├─ ⏰ چک خودکار هر 6 ساعت +├─ 🎼 دانلود با کیفیت 128kbps +├─ 📺 پشتیبانی چند چنل +└─ 💾 ذخیره‌سازی خودکار -⏰ ربات هر 6 ساعت به صورت خودکار پلی‌لیست‌ها را چک می‌کند. -🎵 می‌توانید با دکمه‌های inline هر پلی‌لیست را جداگانه ارسال کنید. -📺 هر پلی‌لیست می‌تواند به چنل مخصوص خود متصل باشد. +💬 نیاز به کمک؟ +از دکمه‌های منو استفاده کنید! """ - await update.message.reply_text(help_text) + + keyboard = [[ + InlineKeyboardButton("🔙 بازگشت به منو", callback_data="back_to_start") + ]] + reply_markup = InlineKeyboardMarkup(keyboard) + + await update.message.reply_text( + help_text, + reply_markup=reply_markup, + parse_mode='HTML' + ) async def add_playlist_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """Handle /addplaylist command""" if not self.is_admin(update.effective_user.id): - await update.message.reply_text("⛔️ فقط ادمین‌ها می‌توانند پلی‌لیست اضافه کنند.") + await update.message.reply_text( + "⛔️ دسترسی محدود\n\n" + "فقط ادمین‌ها می‌توانند پلی‌لیست اضافه کنند.", + parse_mode='HTML' + ) return - await update.message.reply_text( - "لطفا لینک پلی‌لیست اسپاتیفای را ارسال کنید:\n" - "مثال: https://open.spotify.com/playlist/...\n\n" - "یا /cancel برای لغو" - ) + message = """ +╭──────────────────────╮ +│ 🎵 افزودن پلی‌لیست │ +╰──────────────────────╯ + +مرحله 1 از 3: لینک پلی‌لیست + +📎 لطفا لینک پلی‌لیست اسپاتیفای را ارسال کنید: + +مثال: +https://open.spotify.com/playlist/37i9dQZF1DX... + +💡 لینک را از اپلیکیشن Spotify کپی کنید + +❌ برای لغو: /cancel +""" + await update.message.reply_text(message, parse_mode='HTML') context.user_data['awaiting_playlist_url'] = True async def set_channel_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -465,21 +526,58 @@ async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE for p in playlists) total_sent = sum(self.tracks_db.get(p['url'], {}).get('sent_tracks', 0) for p in playlists) + pending_tracks = total_tracks - total_sent # Check ARL status - arl_status = "✅ فعال" if self.deezer_arl else "❌ تنظیم نشده" + arl_status = "🟢 فعال" if self.deezer_arl else "🔴 غیرفعال" + + # Calculate success rate + success_rate = (total_sent / total_tracks * 100) if total_tracks > 0 else 0 + + # Get unique channels + channels = set(p.get('channel_id', '') for p in playlists if p.get('channel_id')) stats_message = f""" -📊 آمار ربات: +╭────────────────────╮ +│ 📊 آمار و وضعیت │ +╰────────────────────╯ -🎵 تعداد پلی‌لیست‌ها: {len(playlists)} -🎼 کل آهنگ‌ها: {total_tracks} -✅ آهنگ‌های ارسال شده: {total_sent} -⏰ بازه چک: 6 ساعت -🎚️ کیفیت: 128kbps -🎧 Deezer ARL: {arl_status} +📈 آمار کلی: +┌───────────────────── +├─ 🎵 پلی‌لیست‌ها: {len(playlists)} +├─ 📺 چنل‌ها: {len(channels)} +├─ 🎼 کل آهنگ‌ها: {total_tracks} +├─ ✅ ارسال شده: {total_sent} +├─ ⏳ در انتظار: {pending_tracks} +└─ 📊 نرخ موفقیت: {success_rate:.1f}% + +⚙️ تنظیمات: +┌───────────────────── +├─ ⏰ بازه چک: 6 ساعت +├─ 🎚️ کیفیت: 128kbps +├─ 🎧 Deezer ARL: {arl_status} +└─ 🤖 وضعیت: 🟢 فعال + +💡 نکته: ربات به صورت خودکار +هر 6 ساعت پلی‌لیست‌ها را بررسی می‌کند. """ - await update.message.reply_text(stats_message) + + keyboard = [ + [ + InlineKeyboardButton("🔄 رفرش", callback_data="show_stats"), + InlineKeyboardButton("📋 پلی‌لیست‌ها", callback_data="list_playlists") + ], + [ + InlineKeyboardButton("🔙 بازگشت", callback_data="back_to_start") + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await update.message.reply_text( + stats_message, + reply_markup=reply_markup, + parse_mode='HTML' + ) async def setup_arl_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): """Handle /setuparl command""" @@ -580,13 +678,30 @@ async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYP # Handle playlist URL input if user_data.get('awaiting_playlist_url'): if 'spotify.com/playlist' not in text: - await update.message.reply_text("❌ لینک نامعتبر است. لطفا یک لینک معتبر ارسال کنید.") + await update.message.reply_text( + "❌ لینک نامعتبر\n\n" + "لطفا یک لینک معتبر اسپاتیفای ارسال کنید.\n\n" + "مثال صحیح:\n" + "https://open.spotify.com/playlist/...", + parse_mode='HTML' + ) return user_data['playlist_url'] = text user_data['awaiting_playlist_url'] = False user_data['awaiting_playlist_name'] = True - await update.message.reply_text("✅ لینک دریافت شد.\nحالا یک نام برای این پلی‌لیست وارد کنید:") + + await update.message.reply_text( + "✅ لینک دریافت شد!\n\n" + "╭──────────────────────╮\n" + "│ 🎵 افزودن پلی‌لیست │\n" + "╰──────────────────────╯\n\n" + "مرحله 2 از 3: نام پلی‌لیست\n\n" + "📝 یک نام دلخواه برای این پلی‌لیست وارد کنید:\n\n" + "مثال: پلی‌لیست راک من\n\n" + "❌ برای لغو: /cancel", + parse_mode='HTML' + ) return # Handle playlist name input @@ -599,10 +714,18 @@ async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYP user_data['awaiting_playlist_channel'] = True await update.message.reply_text( - f"✅ لینک و نام دریافت شد.\n" - f"حالا ID چنل مورد نظر را ارسال کنید:\n" - f"مثال: @channelname یا -1001234567890\n\n" - f"یا /cancel برای لغو" + "✅ نام دریافت شد!\n\n" + "╭──────────────────────╮\n" + "│ 🎵 افزودن پلی‌لیست │\n" + "╰──────────────────────╯\n\n" + "مرحله 3 از 3: چنل مقصد\n\n" + "📺 ID چنل مورد نظر را ارسال کنید:\n\n" + "فرمت‌های مجاز:\n" + "▫️ @channelname (چنل عمومی)\n" + "▫️ -1001234567890 (چنل خصوصی)\n\n" + "💡 ربات باید در چنل Admin باشد\n\n" + "❌ برای لغو: /cancel", + parse_mode='HTML' ) return @@ -616,26 +739,50 @@ async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYP # Basic validation if not channel_id or len(channel_id) < 3: - await update.message.reply_text("❌ ID چنل نامعتبر است.") + await update.message.reply_text( + "❌ ID چنل نامعتبر\n\n" + "لطفا یک ID معتبر وارد کنید:\n" + "▫️ @channelname\n" + "▫️ -1001234567890", + parse_mode='HTML' + ) return url = user_data.get('playlist_url') name = user_data.get('playlist_name') if self.config_manager.add_playlist(url, name, update.effective_user.id, channel_id): - keyboard = [[ - InlineKeyboardButton("📤 ارسال فوری آهنگ‌های این پلی‌لیست", callback_data="send_latest_playlist") - ]] + keyboard = [ + [ + InlineKeyboardButton("📤 ارسال فوری", callback_data="send_latest_playlist") + ], + [ + InlineKeyboardButton("📋 مشاهده پلی‌لیست‌ها", callback_data="list_playlists") + ] + ] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text( - f"✅ پلی‌لیست '{name}' با موفقیت اضافه شد!\n" - f"چنل تنظیم شده: {channel_id}\n\n" - f"می‌خواهید الان آهنگ‌های آن را چک و ارسال کنید؟", - reply_markup=reply_markup + "╭─────────────────╮\n" + "│ ✅ موفقیت‌آمیز! │\n" + "╰─────────────────╯\n\n" + f"🎵 پلی‌لیست: {name}\n" + f"📺 چنل: {channel_id}\n" + f"📡 وضعیت: فعال\n\n" + "✨ پلی‌لیست با موفقیت اضافه شد!\n\n" + "💫 آهنگ‌های جدید به صورت خودکار\n" + "به چنل شما ارسال می‌شوند.\n\n" + "🔽 می‌توانید الان هم چک کنید:", + reply_markup=reply_markup, + parse_mode='HTML' ) else: - await update.message.reply_text("⚠️ این پلی‌لیست قبلا اضافه شده است.") + await update.message.reply_text( + "⚠️ توجه!\n\n" + "این پلی‌لیست قبلاً اضافه شده است.\n\n" + "از /listplaylists برای مشاهده لیست استفاده کنید.", + parse_mode='HTML' + ) user_data.clear() return @@ -764,19 +911,121 @@ async def button_callback(self, update: Update, context: ContextTypes.DEFAULT_TY data = query.data # Check admin for restricted actions - if data in ['add_playlist', 'remove_playlist', 'check_now', 'show_link_menu'] or \ + if data in ['add_playlist', 'remove_playlist', 'check_now', 'show_link_menu', 'show_management'] or \ data.startswith('send_playlist_') or data.startswith('link_playlist_'): if not self.is_admin(user_id): - await query.edit_message_text("⛔️ فقط ادمین‌ها می‌توانند این عملیات را انجام دهند.") + await query.edit_message_text( + "⛔️ دسترسی محدود\n\n" + "این عملیات فقط برای ادمین‌ها مجاز است.", + parse_mode='HTML' + ) return # Handle different callbacks - if data == "add_playlist": + if data == "back_to_start": + # Show start menu again + user = query.from_user + is_admin = self.is_admin(user.id) + + if is_admin: + keyboard = [ + [ + InlineKeyboardButton("🎵 افزودن پلی‌لیست", callback_data="add_playlist"), + ], + [ + InlineKeyboardButton("📋 مشاهده پلی‌لیست‌ها", callback_data="list_playlists"), + InlineKeyboardButton("🔗 ارتباط چنل‌ها", callback_data="show_all_links") + ], + [ + InlineKeyboardButton("🔄 چک فوری", callback_data="check_now"), + InlineKeyboardButton("📊 آمار و وضعیت", callback_data="show_stats") + ], + [ + InlineKeyboardButton("⚙️ مدیریت", callback_data="show_management"), + InlineKeyboardButton("💡 راهنما", callback_data="show_help") + ] + ] + else: + keyboard = [ + [ + InlineKeyboardButton("📋 پلی‌لیست‌ها", callback_data="list_playlists"), + InlineKeyboardButton("📊 آمار", callback_data="show_stats") + ], + [ + InlineKeyboardButton("💡 راهنما", callback_data="show_help") + ] + ] + + reply_markup = InlineKeyboardMarkup(keyboard) + + welcome_message = f""" +╭─────────────────────╮ +│ 🎵 ربات اسپاتیفای │ +╰─────────────────────╯ + +👋 سلام {user.first_name} عزیز! + +🤖 من ربات مدیریت و اشتراک‌گذاری پلی‌لیست‌های اسپاتیفای هستم. + +{'🔐 دسترسی: ادمین' if is_admin else '👤 دسترسی: کاربر'} + +💫 از دکمه‌های زیر برای شروع استفاده کنید: +""" await query.edit_message_text( - "لطفا لینک پلی‌لیست اسپاتیفای را ارسال کنید:\n" - "مثال: https://open.spotify.com/playlist/...\n\n" - "یا /cancel برای لغو" + welcome_message, + reply_markup=reply_markup, + parse_mode='HTML' ) + + elif data == "show_management": + # Show management menu + keyboard = [ + [ + InlineKeyboardButton("➕ افزودن پلی‌لیست", callback_data="add_playlist"), + InlineKeyboardButton("🗑 حذف پلی‌لیست", callback_data="remove_playlist") + ], + [ + InlineKeyboardButton("🔗 تنظیم چنل", callback_data="show_link_menu"), + InlineKeyboardButton("🔄 چک فوری", callback_data="check_now") + ], + [ + InlineKeyboardButton("🔙 بازگشت", callback_data="back_to_start") + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await query.edit_message_text( + "╭────────────────╮\n" + "│ ⚙️ پنل مدیریت │\n" + "╰────────────────╯\n\n" + "عملیات مدیریتی:\n\n" + "▫️ افزودن پلی‌لیست جدید\n" + "▫️ حذف پلی‌لیست\n" + "▫️ تنظیم و تغییر چنل\n" + "▫️ چک و ارسال فوری\n\n" + "💡 یکی از گزینه‌های زیر را انتخاب کنید:", + reply_markup=reply_markup, + parse_mode='HTML' + ) + + elif data == "add_playlist": + message = """ +╭──────────────────────╮ +│ 🎵 افزودن پلی‌لیست │ +╰──────────────────────╯ + +مرحله 1 از 3: لینک پلی‌لیست + +📎 لطفا لینک پلی‌لیست اسپاتیفای را ارسال کنید: + +مثال: +https://open.spotify.com/playlist/37i9dQZF1DX... + +💡 لینک را از اپلیکیشن Spotify کپی کنید + +❌ برای لغو: /cancel +""" + await query.edit_message_text(message, parse_mode='HTML') context.user_data['awaiting_playlist_url'] = True elif data == "list_playlists": @@ -786,9 +1035,22 @@ async def button_callback(self, update: Update, context: ContextTypes.DEFAULT_TY await self.handle_remove_callback(query, context) elif data == "check_now": - await query.edit_message_text("🔄 در حال چک و ارسال تمام پلی‌لیست‌ها...") + await query.edit_message_text( + "🔄 در حال بررسی پلی‌لیست‌ها...\n\n" + "لطفا صبر کنید، این کار ممکن است چند دقیقه طول بکشد.", + parse_mode='HTML' + ) await self.check_all_playlists() - await query.message.reply_text("✅ چک و ارسال تمام پلی‌لیست‌ها تکمیل شد.") + + keyboard = [[InlineKeyboardButton("🔙 بازگشت", callback_data="back_to_start")]] + reply_markup = InlineKeyboardMarkup(keyboard) + + await query.message.reply_text( + "✅ چک تکمیل شد!\n\n" + "تمام پلی‌لیست‌ها بررسی شدند و آهنگ‌های جدید ارسال شدند.", + reply_markup=reply_markup, + parse_mode='HTML' + ) elif data == "show_stats": await self.show_stats_callback(query) @@ -884,53 +1146,116 @@ async def show_stats_callback(self, query): for p in playlists) total_sent = sum(self.tracks_db.get(p['url'], {}).get('sent_tracks', 0) for p in playlists) + pending_tracks = total_tracks - total_sent + + # Check ARL status + arl_status = "🟢 فعال" if self.deezer_arl else "🔴 غیرفعال" + + # Calculate success rate + success_rate = (total_sent / total_tracks * 100) if total_tracks > 0 else 0 + + # Get unique channels + channels = set(p.get('channel_id', '') for p in playlists if p.get('channel_id')) stats_message = f""" -📊 آمار ربات: +╭────────────────────╮ +│ 📊 آمار و وضعیت │ +╰────────────────────╯ -🎵 تعداد پلی‌لیست‌ها: {len(playlists)} -🎼 کل آهنگ‌ها: {total_tracks} -✅ آهنگ‌های ارسال شده: {total_sent} -⏰ بازه چک: 6 ساعت -🎚️ کیفیت: 128kbps +📈 آمار کلی: +┌───────────────────── +├─ 🎵 پلی‌لیست‌ها: {len(playlists)} +├─ 📺 چنل‌ها: {len(channels)} +├─ 🎼 کل آهنگ‌ها: {total_tracks} +├─ ✅ ارسال شده: {total_sent} +├─ ⏳ در انتظار: {pending_tracks} +└─ 📊 نرخ موفقیت: {success_rate:.1f}% + +⚙️ تنظیمات: +┌───────────────────── +├─ ⏰ بازه چک: 6 ساعت +├─ 🎚️ کیفیت: 128kbps +├─ 🎧 Deezer ARL: {arl_status} +└─ 🤖 وضعیت: 🟢 فعال + +💡 نکته: ربات به صورت خودکار +هر 6 ساعت پلی‌لیست‌ها را بررسی می‌کند. """ - keyboard = [[ - InlineKeyboardButton("🔙 بازگشت", callback_data="list_playlists") - ]] + keyboard = [ + [ + InlineKeyboardButton("� رفرش", callback_data="show_stats"), + InlineKeyboardButton("📋 پلی‌لیست‌ها", callback_data="list_playlists") + ], + [ + InlineKeyboardButton("🔙 بازگشت", callback_data="back_to_start") + ] + ] reply_markup = InlineKeyboardMarkup(keyboard) - await query.edit_message_text(stats_message, reply_markup=reply_markup) + await query.edit_message_text( + stats_message, + reply_markup=reply_markup, + parse_mode='HTML' + ) async def show_help_callback(self, query): """Show help via callback""" + is_admin = self.is_admin(query.from_user.id) + help_text = """ -📖 راهنمای استفاده: +╭──────────────────╮ +│ � راهنمای کامل │ +╰──────────────────╯ + +📚 دستورات اصلی: -1️⃣ افزودن پلی‌لیست: -/addplaylist یا دکمه "افزودن پلی‌لیست" +🎵 افزودن پلی‌لیست +└─ /addplaylist + ورودی‌ها: لینک، نام، چنل -2️⃣ تنظیم چنل: -/setchannel یا دکمه "تنظیم چنل" +🔗 مدیریت چنل‌ها +├─ /linkplaylist → تغییر چنل +├─ /showlinks → مشاهده ارتباطات +└─ /setchannel → تنظیم چنل -3️⃣ مشاهده پلی‌لیست‌ها: -/listplaylists یا دکمه "لیست پلی‌لیست‌ها" +📋 مشاهده اطلاعات +├─ /listplaylists → لیست پلی‌لیست‌ها +├─ /stats → آمار کامل +└─ /help → این راهنما -4️⃣ ارسال فوری: -از دکمه "ارسال فوری" کنار هر پلی‌لیست +""" + + if is_admin: + help_text += """⚙️ دستورات مدیریتی: -5️⃣ چک همه: -دکمه "چک و ارسال همه" +🗑 حذف → /removeplaylist +🔄 چک فوری → /checkplaylists +🎧 تنظیم ARL → /setuparl + +""" + + help_text += """ +┌───────────────────── +│ ℹ️ اطلاعات: +├─ ⏰ چک خودکار هر 6 ساعت +├─ 🎼 کیفیت 128kbps +├─ 📺 پشتیبانی چند چنل +└─ 💾 ذخیره خودکار -⏰ ربات هر 6 ساعت به صورت خودکار چک می‌کند. +💬 از دکمه‌های منو استفاده کنید! """ keyboard = [[ - InlineKeyboardButton("🔙 بازگشت به منو", callback_data="show_main_menu") + InlineKeyboardButton("🔙 بازگشت به منو", callback_data="back_to_start") ]] reply_markup = InlineKeyboardMarkup(keyboard) - await query.edit_message_text(help_text, reply_markup=reply_markup) + await query.edit_message_text( + help_text, + reply_markup=reply_markup, + parse_mode='HTML' + ) async def show_link_menu_callback(self, query, context): """نمایش منوی ارتباط پلی‌لیست به چنل"""