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):
"""نمایش منوی ارتباط پلیلیست به چنل"""