Merhaba,
Python kaynak kodu olarak ücretsiz paylaşıyorum. İsteyen exe derleyebilir. Nasıl kullanırım diye PM atanlara cevap verilmeyecektir.
Sıradaki ücretsiz yazılım için takipte kalın
import re
import requests
from bs4 import BeautifulSoup
import customtkinter as ctk
from tkinter import messagebox
import threading
from datetime import datetime
import json
import concurrent.futures
import pandas as pd
import os


class YouTubeTrackerNoApiApp(ctk.CTk):
    def __init__(self):
        super().__init__()


        self.title("Wisex Kanal Kontrol")
        self.geometry("1400x800")
        ctk.set_appearance_mode("dark")
        ctk.set_default_color_theme("blue")


        # Değişkenler
        self.channel_urls = []
        self.request_count = 0
        self.max_requests = 100
        self.is_fetching = False
        self.results_data = []
        self.successful_channels = 0


        # Ana frame
        self.main_frame = ctk.CTkFrame(self)
        self.main_frame.pack(fill="both", expand=True, padx=10, pady=10)


        # Sol Frame: Girişler
        self.left_frame = ctk.CTkFrame(self.main_frame)
        self.left_frame.pack(side="left", fill="y", padx=5, pady=5)


        # Çoklu URL Ekleme
        ctk.CTkLabel(self.left_frame, text="Kanal Linkleri (her satıra bir link):").pack(pady=5)
        self.url_textbox = ctk.CTkTextbox(self.left_frame, height=150, width=400)
        self.url_textbox.pack(pady=5)
        
        self.example_button = ctk.CTkButton(self.left_frame, text="Örnek URL Ekle", command=self.add_example_urls)
        self.example_button.pack(pady=5)


        button_frame = ctk.CTkFrame(self.left_frame)
        button_frame.pack(pady=5)
        
        self.add_button = ctk.CTkButton(button_frame, text="Linkleri Ekle", command=self.add_channels)
        self.add_button.pack(side="left", padx=5)
        
        self.clear_button = ctk.CTkButton(button_frame, text="Listeyi Temizle", command=self.clear_channels)
        self.clear_button.pack(side="left", padx=5)


        ctk.CTkLabel(self.left_frame, text="Eklenen Kanallar:").pack(pady=5)
        self.channel_listbox = ctk.CTkTextbox(self.left_frame, height=200, width=400)
        self.channel_listbox.pack(pady=5, fill="both", expand=True)


        # Sağ Frame
        self.right_frame = ctk.CTkFrame(self.main_frame)
        self.right_frame.pack(side="right", fill="both", expand=True, padx=5, pady=5)


        control_frame = ctk.CTkFrame(self.right_frame)
        control_frame.pack(pady=10, fill="x")
        
        self.fetch_button = ctk.CTkButton(control_frame, text="Bilgileri Çek", command=self.start_fetch_info)
        self.fetch_button.pack(side="left", padx=5)
        
        self.export_button = ctk.CTkButton(control_frame, text="TXT Aktar", command=self.export_results_txt)
        self.export_button.pack(side="left", padx=5)
        
        self.export_excel_button = ctk.CTkButton(control_frame, text="Excel Aktar", command=self.export_results_excel)
        self.export_excel_button.pack(side="left", padx=5)
        
        self.reset_counter_button = ctk.CTkButton(control_frame, text="Sayacı Sıfırla", command=self.reset_counter)
        self.reset_counter_button.pack(side="left", padx=5)


        request_frame = ctk.CTkFrame(self.right_frame)
        request_frame.pack(pady=5, fill="x")
        
        ctk.CTkLabel(request_frame, text="Maksimum İstek Limiti:").pack(side="left", padx=5)
        self.request_limit_entry = ctk.CTkEntry(request_frame, width=80)
        self.request_limit_entry.insert(0, str(self.max_requests))
        self.request_limit_entry.pack(side="left", padx=5)
        
        self.set_request_limit_button = ctk.CTkButton(request_frame, text="Ayarla", command=self.set_request_limit)
        self.set_request_limit_button.pack(side="left", padx=5)


        ctk.CTkLabel(request_frame, text="Paralel İstek:").pack(side="left", padx=(20,5))
        self.parallel_entry = ctk.CTkEntry(request_frame, width=50)
        self.parallel_entry.insert(0, "3")
        self.parallel_entry.pack(side="left", padx=5)


        ctk.CTkLabel(self.right_frame, text="HTTP İstek Kullanımı:").pack(pady=5)
        self.request_progress = ctk.CTkProgressBar(self.right_frame, width=400)
        self.request_progress.pack(pady=5)
        self.request_label = ctk.CTkLabel(self.right_frame, text=f"Kullanılan: 0 / {self.max_requests}")
        self.request_label.pack(pady=5)


        self.success_label = ctk.CTkLabel(self.right_frame, text="Başarılı Kanallar: 0")
        self.success_label.pack(pady=5)


        ctk.CTkLabel(self.right_frame, text="Kanal Bilgileri:").pack(pady=5)
        self.result_frame = ctk.CTkFrame(self.right_frame)
        self.result_frame.pack(fill="both", expand=True, pady=5)
        
        headers_frame = ctk.CTkFrame(self.result_frame)
        headers_frame.pack(fill="x", padx=5, pady=2)
        
        ctk.CTkLabel(headers_frame, text="Kanal Adı", width=180, anchor="w").pack(side="left", padx=2)
        ctk.CTkLabel(headers_frame, text="Kanal Linki", width=150, anchor="w").pack(side="left", padx=2)
        ctk.CTkLabel(headers_frame, text="Abone", width=80, anchor="w").pack(side="left", padx=2)
        ctk.CTkLabel(headers_frame, text="Video", width=60, anchor="w").pack(side="left", padx=2)
        ctk.CTkLabel(headers_frame, text="Görüntülenme", width=90, anchor="w").pack(side="left", padx=2)
        ctk.CTkLabel(headers_frame, text="Kuruluş", width=90, anchor="w").pack(side="left", padx=2)
        ctk.CTkLabel(headers_frame, text="Durum", width=70, anchor="w").pack(side="left", padx=2)


        self.results_container = ctk.CTkScrollableFrame(self.result_frame, height=400)
        self.results_container.pack(fill="both", expand=True, padx=5, pady=5)


        self.status_label = ctk.CTkLabel(self.right_frame, text="Hazır")
        self.status_label.pack(pady=5)


    def add_example_urls(self):
        example_urls = """https://www.youtube.com/@SenAnlatKaradeniz
https://www.youtube.com/@KVPEfsaneSahneler
https://www.youtube.com/c/example2
https://www.youtube.com/channel/UCqfo40MC0FrR90_gpbagHjg"""
        self.url_textbox.delete("1.0", "end")
        self.url_textbox.insert("1.0", example_urls)


    def add_channels(self):
        urls_text = self.url_textbox.get("1.0", "end").strip()
        if not urls_text:
            messagebox.showwarning("Uyarı", "Lütfen kanal linkleri girin.")
            return
            
        urls = [url.strip() for url in urls_text.split('\n') if url.strip()]
        new_urls = []
        
        for url in urls:
            normalized_url = self.normalize_url(url)
            if normalized_url and normalized_url not in self.channel_urls:
                self.channel_urls.append(normalized_url)
                new_urls.append(normalized_url)
        
        if new_urls:
            self.update_channel_list()
            messagebox.showinfo("Başarılı", f"{len(new_urls)} yeni kanal eklendi!")
        else:
            messagebox.showinfo("Bilgi", "Tüm kanallar zaten listede mevcut.")


    def normalize_url(self, url):
        if '/about' in url:
            return url.split('?')[0]
        if '/channel/' in url or '/@' in url or '/c/' in url:
            base_url = url.split('?')[0]
            return base_url + '/about' if not base_url.endswith('/about') else base_url
        return url


    def clear_channels(self):
        self.channel_urls.clear()
        self.update_channel_list()
        self.url_textbox.delete("1.0", "end")


    def update_channel_list(self):
        self.channel_listbox.delete("1.0", "end")
        for i, url in enumerate(self.channel_urls, 1):
            self.channel_listbox.insert("end", f"{i}. {url}\n")


    def set_request_limit(self):
        try:
            self.max_requests = int(self.request_limit_entry.get())
            self.update_request_display()
            messagebox.showinfo("Bilgi", f"Maksimum istek limiti {self.max_requests} olarak ayarlandı.")
        except ValueError:
            messagebox.showerror("Hata", "Lütfen geçerli bir sayı girin.")


    def reset_counter(self):
        self.request_count = 0
        self.successful_channels = 0
        self.update_request_display()
        self.update_success_counter()
        messagebox.showinfo("Bilgi", "İstek sayacı sıfırlandı.")


    def start_fetch_info(self):
        if not self.channel_urls:
            messagebox.showerror("Hata", "Lütfen en az bir kanal ekleyin.")
            return
            
        if self.is_fetching:
            messagebox.showwarning("Uyarı", "Zaten veri çekiliyor...")
            return
            
        self.is_fetching = True
        self.fetch_button.configure(state="disabled")
        self.status_label.configure(text="Veriler çekiliyor...")
        self.results_data = []
        self.successful_channels = 0
        self.update_success_counter()
        
        for widget in self.results_container.winfo_children():
            widget.destroy()
        
        thread = threading.Thread(target=self.fetch_info_parallel)
        thread.daemon = True
        thread.start()


    def fetch_info_parallel(self):
        try:
            parallel_workers = min(int(self.parallel_entry.get()), 5)
        except:
            parallel_workers = 3


        with concurrent.futures.ThreadPoolExecutor(max_workers=parallel_workers) as executor:
            future_to_url = {
                executor.submit(self.fetch_single_channel, url, i): (url, i) 
                for i, url in enumerate(self.channel_urls) 
                if self.request_count < self.max_requests
            }
            
            for future in concurrent.futures.as_completed(future_to_url):
                url, index = future_to_url[future]
                try:
                    channel_info = future.result()
                    if channel_info and self.is_valid_channel(channel_info):
                        self.results_data.append(channel_info)
                        self.add_result_row(channel_info)
                        self.successful_channels += 1
                        self.update_success_counter()
                except Exception:
                    continue


        self.is_fetching = False
        self.fetch_button.configure(state="normal")
        self.show_status(f"Tamamlandı! {self.successful_channels} başarılı kanal bulundu.")


    def is_valid_channel(self, channel_info):
        if channel_info['name'] in ['Bilinmiyor', 'Hata', 'HTTP Hatası', 'Bağlantı Hatası']:
            return False
        if channel_info['status'] != 'Başarılı':
            return False
        return True


    def fetch_single_channel(self, url, index):
        if self.request_count >= self.max_requests:
            return None
            
        self.show_status(f"Çekiliyor: {index+1}/{len(self.channel_urls)} - {self.get_short_url(url)}")
        
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
            'Accept-Language': 'tr-TR,tr;q=0.8,en-US;q=0.5,en;q=0.3',
            'Connection': 'keep-alive',
        }


        try:
            response = requests.get(url, headers=headers, timeout=8)
            self.request_count += 1
            self.update_request_display()


            if response.status_code == 200:
                channel_info = self.parse_channel_info_fast(response.text, url)
                if channel_info['name'] == 'Bilinmiyor':
                    return None
                return channel_info
            else:
                return None
        except Exception:
            return None


    def get_short_url(self, url):
        if '/@' in url:
            return '@' + url.split('/@')[-1].split('/')[0]
        elif '/channel/' in url:
            channel_id = url.split('/channel/')[-1].split('/')[0]
            return f"channel:{channel_id[:10]}..."
        elif '/c/' in url:
            return 'c/' + url.split('/c/')[-1].split('/')[0]
        else:
            return url[-15:]


    # -------------------- GÜNCELLENMİŞ PARSE FONKSİYONU (TABLO ODAKLI) --------------------
    def parse_channel_info_fast(self, html_content, url):
        """
        Kanal bilgilerini doğrudan ytd-about-channel-renderer tablosundan alır.
        Bulamazsa regex fallback kullanır.
        """
        soup = BeautifulSoup(html_content, 'html.parser')
        
        # 1. Kanal adını al (meta/başlık)
        channel_name = self.extract_channel_name_fast(html_content)
        if channel_name == 'Bilinmiyor':
            return {
                'name': 'Bilinmiyor',
                'short_url': self.get_short_url(url),
                'full_url': url,
                'subscribers': 'N/A',
                'video_count': 'N/A',
                'views': 'N/A',
                'join_date': 'N/A',
                'status': 'Başarısız',
                'timestamp': datetime.now().strftime("%H:%M:%S"),
                'url': url
            }


        # 2. Tabloyu bul
        table = soup.find('table', class_='style-scope ytd-about-channel-renderer')
        details = {'subscribers': None, 'video_count': None, 'views': None, 'join_date': None}
        
        if table:
            # Tablo içindeki her satırı gez (hidden olmayanları)
            for tr in table.find_all('tr', class_='description-item'):
                # hidden attribute'u kontrol et (true, false veya boş)
                if tr.get('hidden') is not None:
                    continue
                
                tds = tr.find_all('td')
                if len(tds) < 2:
                    continue
                
                icon_td = tds[0]
                text_td = tds[1]
                
                yt_icon = icon_td.find('yt-icon')
                if not yt_icon:
                    continue
                
                icon_name = yt_icon.get('icon', '')
                # Metni temizle (&nbsp; vs.)
                text = text_td.get_text(strip=True)
                
                if icon_name == 'person_radar':
                    details['subscribers'] = text
                elif icon_name == 'my_videos':
                    details['video_count'] = text
                elif icon_name == 'trending_up':
                    details['views'] = text
                elif icon_name == 'info_outline':
                    details['join_date'] = text


        # Eğer tablodan hiçbir şey gelmediyse regex fallback
        if not any(details.values()):
            details = {
                'subscribers': self.extract_subscribers_fast(html_content),
                'video_count': self.extract_video_count_fast(html_content),
                'views': self.extract_total_views_corrected(html_content),
                'join_date': self.extract_join_date_fast(html_content)
            }


        return {
            'name': channel_name,
            'short_url': self.get_short_url(url),
            'full_url': url,
            'subscribers': details.get('subscribers', 'Bilinmiyor'),
            'video_count': details.get('video_count', 'Bilinmiyor'),
            'views': details.get('views', 'Bilinmiyor'),
            'join_date': details.get('join_date', 'Bilinmiyor'),
            'status': 'Başarılı',
            'timestamp': datetime.now().strftime("%H:%M:%S"),
            'url': url
        }


    # -------------------- REGEX FALLBACK METOTLARI --------------------
    def extract_channel_name_fast(self, html_content):
        meta_match = re.search(r'<meta property="og:title" content="([^"]+)"', html_content)
        if meta_match:
            name = meta_match.group(1).replace(' - YouTube', '').strip()
            if name and name != "YouTube" and len(name) > 1:
                return name[:30]
        title_match = re.search(r'<title>([^<]+)</title>', html_content)
        if title_match:
            name = title_match.group(1).replace(' - YouTube', '').strip()
            if name and name != "YouTube" and len(name) > 1:
                return name[:30]
        json_ld_match = re.search(r'<script type="application/ld\+json">([^<]+)</script>', html_content)
        if json_ld_match:
            try:
                json_data = json.loads(json_ld_match.group(1))
                if isinstance(json_data, list):
                    json_data = json_data[0]
                if json_data.get('name'):
                    name = json_data['name']
                    if name and name != "YouTube" and len(name) > 1:
                        return name[:30]
            except:
                pass
        return "Bilinmiyor"


    def extract_subscribers_fast(self, html_content):
        patterns = [
            r'([\d.,]+\s*(?:[Mm]n|[Mm]ilyon)?)\s*abone',
            r'aria-label="([\d,]+)\s+abone"',
            r'<span[^>]*>([\d,]+)\s+abone</span>',
            r'"subscriberCountText"[^}]*"simpleText":"([\d,]+)"',
            r'"subscribers":"([\d,]+)"',
            r'(\d+(?:,\d+)*)\s*abone',
        ]
        for pattern in patterns:
            match = re.search(pattern, html_content, re.IGNORECASE)
            if match:
                return match.group(1).strip()
        return "Gizli"


    def extract_video_count_fast(self, html_content):
        patterns = [
            r'([\d.,]+)\s*video',
            r'aria-label="([\d,]+)\s+video"',
            r'<span[^>]*>([\d,]+)\s+video</span>',
            r'"videosCountText"[^}]*"runs":[^[]*\[[^]]*"text":"([\d,]+)"',
            r'"videoCount":"([\d,]+)"',
            r'(\d+(?:,\d+)*)\s*video',
        ]
        for pattern in patterns:
            match = re.search(pattern, html_content, re.IGNORECASE)
            if match:
                return match.group(1).strip()
        return "Bilinmiyor"


    def extract_total_views_corrected(self, html_content):
        patterns = [
            r'([\d.,]+)\s*görüntüleme',
            r'YtmAboutChannelRendererChannelDetail[^>]*>[\s\S]*?([\d,\.]+)\s*görüntüleme',
            r'engagement-panel-section-list[^>]*>[\s\S]*?([\d,\.]+)\s*görüntüleme',
            r'"viewCountText"[^}]*"simpleText":"([\d,\.]+)"',
            r'totalViews["\s:]*([\d,\.]+)',
        ]
        for pattern in patterns:
            match = re.search(pattern, html_content, re.IGNORECASE)
            if match:
                views = match.group(1).strip()
                numeric_views = views.replace(',', '').replace('.', '')
                if numeric_views.isdigit() and len(numeric_views) >= 3:
                    return views
        return "Bilinmiyor"


    def extract_join_date_fast(self, html_content):
        patterns = [
            r'(\d{1,2}\s+\w+\s+\d{4})\s*tarihinde katıldı',
            r'Joined\s+(\w+\s+\d{1,2},\s+\d{4})',
        ]
        for pattern in patterns:
            match = re.search(pattern, html_content)
            if match:
                return match.group(1)
        return "Bilinmiyor"


    # -------------------- UI METOTLARI (değişmedi) --------------------
    def add_result_row(self, channel_info):
        self.after(0, self._add_result_row_ui, channel_info)


    def _add_result_row_ui(self, channel_info):
        row_frame = ctk.CTkFrame(self.results_container)
        row_frame.pack(fill="x", padx=2, pady=1)
        
        ctk.CTkLabel(row_frame, text=channel_info['name'], width=180, anchor="w").pack(side="left", padx=2)
        ctk.CTkLabel(row_frame, text=channel_info['short_url'], width=150, anchor="w").pack(side="left", padx=2)
        ctk.CTkLabel(row_frame, text=channel_info['subscribers'], width=80, anchor="w").pack(side="left", padx=2)
        ctk.CTkLabel(row_frame, text=channel_info['video_count'], width=60, anchor="w").pack(side="left", padx=2)
        ctk.CTkLabel(row_frame, text=channel_info['views'], width=90, anchor="w").pack(side="left", padx=2)
        ctk.CTkLabel(row_frame, text=channel_info['join_date'], width=90, anchor="w").pack(side="left", padx=2)
        
        status_color = "green" if channel_info['status'] == 'Başarılı' else "red"
        ctk.CTkLabel(row_frame, text=channel_info['status'], width=70, anchor="w", 
                     text_color=status_color).pack(side="left", padx=2)


    def show_status(self, message):
        self.after(0, lambda: self.status_label.configure(text=message))


    def update_request_display(self):
        progress = min(self.request_count / self.max_requests, 1.0)
        self.request_progress.set(progress)
        self.request_label.configure(text=f"Kullanılan: {self.request_count} / {self.max_requests}")
        if progress > 0.8:
            self.request_label.configure(text_color="red")
        elif progress > 0.6:
            self.request_label.configure(text_color="orange")
        else:
            self.request_label.configure(text_color="white")


    def update_success_counter(self):
        self.success_label.configure(text=f"Başarılı Kanallar: {self.successful_channels}")


    def export_results_txt(self):
        if not self.results_data:
            messagebox.showwarning("Uyarı", "Dışa aktarılacak veri bulunamadı.\nLütfen önce 'Bilgileri Çek' butonuna tıklayın.")
            return
        try:
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            filename = f"youtube_kanal_sonuclari_{timestamp}.txt"
            with open(filename, 'w', encoding='utf-8') as f:
                f.write("YouTube Kanal Bilgileri\n")
                f.write("=" * 150 + "\n")
                f.write(f"Oluşturulma: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
                f.write(f"Toplam Kanal: {len(self.channel_urls)}\n")
                f.write(f"Başarılı Kanallar: {self.successful_channels}\n")
                f.write(f"Kullanılan İstek: {self.request_count}\n")
                f.write("=" * 150 + "\n\n")
                f.write(f"{'Kanal Adı':<25} {'Tam Kanal Linki':<50} {'Abone':<10} {'Video':<8} {'Görüntülenme':<12} {'Kuruluş':<12} {'Durum':<10}\n")
                f.write("-" * 150 + "\n")
                for data in self.results_data:
                    f.write(f"{data['name']:<25} {data['full_url']:<50} {data['subscribers']:<10} {data['video_count']:<8} {data['views']:<12} {data['join_date']:<12} {data['status']:<10}\n")
            messagebox.showinfo("Başarılı", f"Sonuçlar TXT dosyasına kaydedildi!\nDosya: {filename}")
        except Exception as e:
            messagebox.showerror("Hata", f"TXT dosyası kaydedilemedi: {str(e)}")


    def export_results_excel(self):
        if not self.results_data:
            messagebox.showwarning("Uyarı", "Dışa aktarılacak veri bulunamadı.\nLütfen önce 'Bilgileri Çek' butonuna tıklayın.")
            return
        try:
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            filename = f"youtube_kanal_sonuclari_{timestamp}.xlsx"
            df_data = []
            for data in self.results_data:
                df_data.append({
                    'Kanal Adı': data['name'],
                    'Tam Kanal Linki': data['full_url'],
                    'Abone Sayısı': data['subscribers'],
                    'Video Sayısı': data['video_count'],
                    'Toplam Görüntülenme': data['views'],
                    'Katılma Tarihi': data['join_date'],
                    'Durum': data['status'],
                    'Son Güncelleme': data['timestamp']
                })
            df = pd.DataFrame(df_data)
            with pd.ExcelWriter(filename, engine='openpyxl') as writer:
                df.to_excel(writer, sheet_name='YouTube Kanalları', index=False)
                workbook = writer.book
                worksheet = writer.sheets['YouTube Kanalları']
                worksheet.column_dimensions['A'].width = 25
                worksheet.column_dimensions['B'].width = 50
                worksheet.column_dimensions['C'].width = 15
                worksheet.column_dimensions['D'].width = 12
                worksheet.column_dimensions['E'].width = 18
                worksheet.column_dimensions['F'].width = 15
                worksheet.column_dimensions['G'].width = 12
                worksheet.column_dimensions['H'].width = 15
                for cell in worksheet[1]:
                    cell.font = cell.font.copy(bold=True)
            messagebox.showinfo("Başarılı", f"Sonuçlar Excel dosyasına kaydedildi!\nDosya: {filename}")
        except ImportError:
            messagebox.showerror("Hata", "Excel için pandas ve openpyxl gerekli.\npip install pandas openpyxl")
        except Exception as e:
            messagebox.showerror("Hata", f"Excel dosyası kaydedilemedi: {str(e)}")


if __name__ == "__main__":
    app = YouTubeTrackerNoApiApp()
    app.mainloop()