1
#!/usr/bin/env python3
"""
Launch a multireddit of Kenyan subreddits in your browser.

Usage:
    python reddit-kenya.py          # Show table and open in browser
    python reddit-kenya.py -t       # Show table only (don't open browser)
"""

import webbrowser
import sys
import argparse
import os
import configparser
import praw
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeRemainingColumn, TimeElapsedColumn
from rich.table import Table as RichTable
from rich.markdown import Markdown
import pyperclip
import time

console = Console()

def ensure_config_exists(config_path):
    """Create config file if it doesn't exist and open in editor."""
    if os.path.exists(config_path):
        return

    console.print(f"[yellow]Config file not found at: {config_path}[/yellow]")
    console.print("[cyan]Creating template config file...[/cyan]\n")

    # Create directory if it doesn't exist
    os.makedirs(os.path.dirname(config_path), exist_ok=True)

    # Create template config
    template_config = """[your_reddit_account]
client_id = your_client_id_here
client_secret = your_client_secret_here
user_agent = python:reddit-kenya:v1.0 (by /u/yourusername)
check_for_updates = false
username = your_username
password = your_password
"""

    with open(config_path, 'w') as f:
        f.write(template_config)

    console.print(f"[green]✓[/green] Created config file at: {config_path}")
    console.print("\n[yellow]Please edit the file with your Reddit credentials:[/yellow]")
    console.print(f"[cyan]1. Get client_id/secret from: https://www.reddit.com/prefs/apps[/cyan]")
    console.print("[cyan]2. Create a 'script' type application[/cyan]")
    console.print("[cyan]3. Fill in the credentials in the config file[/cyan]\n")

    # Open in editor
    editor = os.environ.get('EDITOR', 'notepad' if sys.platform == 'win32' else 'nano')
    console.print(f"[cyan]Opening config in {editor}...[/cyan]\n")
    os.system(f"{editor} \"{config_path}\"")

    console.print("[yellow]Press Enter when you've saved the config file...[/yellow]")
    input()

def load_reddit_config(config_path):
    """Load PRAW Reddit instance from config file, using first available section."""
    import configparser

    try:
        # Read the config file to find available sections
        config = configparser.ConfigParser()
        config.read(config_path)

        # Get all sections (excluding DEFAULT)
        sections = [s for s in config.sections() if s != 'DEFAULT']

        if not sections:
            print(f"No user sections found in {config_path}")
            sys.exit(1)

        # Use the first available section
        section_name = sections[0]
        print(f"Using Reddit config section: [{section_name}]")

        # Extract credentials from the section
        if not config.has_section(section_name):
            print(f"Section [{section_name}] not found in config file")
            sys.exit(1)

        client_id = config.get(section_name, 'client_id')
        client_secret = config.get(section_name, 'client_secret')
        user_agent = config.get(section_name, 'user_agent', fallback='python:reddit-kenya:v1.0 (by /u/sugarplow)')

        # Create Reddit instance directly with credentials
        reddit = praw.Reddit(
            client_id=client_id,
            client_secret=client_secret,
            user_agent=user_agent,
        )

        # Test the connection
        reddit.user.me()
        return reddit
    except Exception as e:
        print(f"Error loading Reddit config from {config_path}: {e}")
        sys.exit(1)

def fetch_single_subreddit(reddit, sub_name):
    """Fetch info for a single subreddit."""
    try:
        subreddit = reddit.subreddit(sub_name)
        subscribers = getattr(subreddit, 'subscribers', 'N/A')
        created_utc = getattr(subreddit, 'created_utc', None)
        over18 = getattr(subreddit, 'over18', False)

        if created_utc:
            created_year = datetime.fromtimestamp(created_utc).year
        else:
            created_year = 'N/A'

        return {
            'name': sub_name,
            'subscribers': subscribers,
            'created_year': created_year,
            'over18': over18,
            'success': True
        }
    except Exception as e:
        return {
            'name': sub_name,
            'subscribers': 'Error',
            'created_year': 'Error',
            'over18': False,
            'success': False,
            'error': str(e)
        }

def get_subreddit_info(reddit, subreddits, max_workers=5):
    """Get subscriber counts and creation dates for subreddits in parallel."""
    info = []
    failed = []
    start_time = time.time()

    with Progress(
        SpinnerColumn(),
        TextColumn("[progress.description]{task.description}"),
        BarColumn(),
        TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
        TimeRemainingColumn(),
        TextColumn("•"),
        TimeElapsedColumn(),
        console=console
    ) as progress:

        task = progress.add_task(
            "[cyan]Fetching subreddit info...[/cyan]",
            total=len(subreddits)
        )

        with ThreadPoolExecutor(max_workers=max_workers) as executor:
            # Submit all tasks
            future_to_sub = {
                executor.submit(fetch_single_subreddit, reddit, sub): sub
                for sub in subreddits
            }

            # Process completed tasks
            for future in as_completed(future_to_sub):
                result = future.result()
                info.append(result)

                if result['success']:
                    console.print(f"[green]✓[/green] r/{result['name']}")
                else:
                    console.print(f"[red]✗[/red] r/{result['name']}: {result['error']}")
                    failed.append({
                        'name': result['name'],
                        'error': result['error']
                    })

                progress.advance(task)

    elapsed_time = time.time() - start_time
    console.print(f"[green]✓[/green] Fetched [cyan]{len(info)}[/cyan] subreddits in [yellow]{elapsed_time:.2f}s[/yellow]")

    return info, failed

def display_subreddit_table(subreddit_info):
    """Display subreddit information as a compact Rich table and copy markdown to clipboard."""
    # Filter out failed entries
    successful_info = [info for info in subreddit_info if info['success']]

    # Sort by subscriber count (descending)
    sorted_info = sorted(
        successful_info,
        key=lambda x: x['subscribers'],
        reverse=True
    )

    # Create Rich table (compact, no dividers)
    table = RichTable(title="\n[bold cyan]Kenyan Subreddits[/bold cyan]", show_header=True, header_style="bold magenta")
    table.add_column("#", style="dim", width=3, justify="right")
    table.add_column("Subreddit", style="cyan", no_wrap=True)
    table.add_column("Subscribers", style="green", justify="right")
    table.add_column("Created", style="yellow", justify="right")
    table.add_column("NSFW", style="red", justify="center", width=6)

    # Build markdown table
    markdown_lines = ["# Kenyan Subreddits\n"]
    markdown_lines.append("| # | Subreddit | Subscribers | Created | NSFW |")
    markdown_lines.append("|---|-----------|-------------|---------|------|")

    for index, info in enumerate(sorted_info, 1):
        sub_count = info['subscribers']
        sub_count_str = f"{sub_count:,}" if isinstance(sub_count, int) else str(sub_count)

        # NSFW indicator
        nsfw_indicator = "[bold red]NSFW[/bold red]" if info.get('over18', False) else ""
        nsfw_markdown = "NGONO" if info.get('over18', False) else ""

        # Add to Rich table
        table.add_row(
            str(index),
            f"r/{info['name']}",
            sub_count_str,
            str(info['created_year']),
            nsfw_indicator
        )

        # Add to markdown
        markdown_lines.append(f"| {index} | r/{info['name']} | {sub_count_str} | {info['created_year']} | {nsfw_markdown} |")

    # Print Rich table
    console.print(table)

    # Count NSFW subreddits
    nsfw_count = sum(1 for info in sorted_info if info.get('over18', False))

    # Print summary
    total_subs = sum(info['subscribers'] for info in sorted_info)
    console.print(f"\n[green]✓[/green] Total successful subreddits: [bold cyan]{len(sorted_info)}[/bold cyan]")
    console.print(f"[green]✓[/green] Total subscribers across all subreddits: [bold cyan]{total_subs:,}[/bold cyan]")
    if nsfw_count > 0:
        console.print(f"[red]⚠[/red] NSFW subreddits: [bold red]{nsfw_count}[/bold red]")

    # Copy markdown to clipboard
    markdown_text = "\n".join(markdown_lines)
    try:
        pyperclip.copy(markdown_text)
        console.print(f"\n[green]✓[/green] [bold]Markdown table copied to clipboard![/bold]")
    except Exception as e:
        console.print(f"\n[yellow]⚠[/yellow] Could not copy to clipboard: {e}")

    return len(sorted_info), total_subs

def display_failed_summary(failed):
    """Display summary of failed subreddit fetches using Rich."""
    if not failed:
        return

    # Group by error type
    error_groups = {}
    for item in failed:
        error = item['error']
        if error not in error_groups:
            error_groups[error] = []
        error_groups[error].append(item['name'])

    console.print("\n" + "="*80)
    console.print(f"[bold red]FAILED SUBREDDITS ({len(failed)})[/bold red]")
    console.print("="*80 + "\n")

    for error, subreddits in sorted(error_groups.items(), key=lambda x: len(x[1]), reverse=True):
        console.print(f"[red]Error:[/red] {error}")
        console.print(f"[yellow]Count:[/yellow] {len(subreddits)}")

        sub_list = ', '.join([f'[dim]r/{s}[/dim]' for s in subreddits[:10]])
        if len(subreddits) > 10:
            sub_list += " [dim]...[/dim]"
        console.print(f"[dim]Subreddits:[/dim] {sub_list}")
        console.print()

def cleanlist(items):
    """Clean a list by removing duplicates and empty items while preserving order."""
    seen = set()
    result = []
    for item in items:
        if item and item.strip() and item not in seen:
            seen.add(item)
            result.append(item)
    return result

# List of Kenyan subreddits
KENYAN_SUBREDDITS = """

/r/254kenya
/r/254mental
/r/254sum
/r/africans
/r/animekenya
/r/anything_about_kenya
/r/askhornofafrica
/r/askkenyan
/r/bettingtipskenya
/r/buykenyabuildkenya
/r/carskenya
/r/charitythekenyan
/r/childfreekenya
/r/christianskenya
/r/coolstuffkenya
/r/cybersecuritykenya
/r/derc_ke
/r/designers_kenya
/r/eastafricasafari
/r/eldoret
/r/freekenyatalks
/r/freespeechkenya
/r/funkenyaw
/r/gaminginkenya
/r/gamingkenya
/r/group_kenya
/r/hookups_kenya
/r/iftravellingtokenya
/r/interlogixcomputers
/r/jobsinkenya
/r/jobskenya
/r/kahawapridefc
/r/kalenjin
/r/kegn
/r/kemusic
/r/kenya
/r/kenya_got_rides
/r/kenya_nightlife
/r/kenya_nsfw
/r/kenyaafterdark
/r/kenyabibleclub
/r/kenyablogs
/r/kenyabusinessgroup
/r/kenyabuyersbeware
/r/kenyacars
/r/kenyacasual
/r/kenyacycling
/r/kenyadatingcommunity
/r/kenyadestinations
/r/kenyaevents
/r/kenyafire
/r/kenyafriending
/r/kenyagrace
/r/kenyagw
/r/kenyahustlers
/r/kenyainvesting
/r/kenyalgbtqpluss
/r/kenyamedics
/r/kenyamemes
/r/kenyan_millennials
/r/kenyanart
/r/kenyancreators
/r/kenyancuisine
/r/kenyancyberhub
/r/kenyandatingcommunity
/r/kenyandevs
/r/kenyandudes
/r/kenyanentrepreneurs
/r/kenyanews
/r/kenyanfood
/r/kenyanfoodies
/r/kenyangaming
/r/kenyanhistory
/r/kenyaninthediaspora
/r/kenyanladies
/r/kenyanlobby
/r/kenyanlounge
/r/kenyanmemes
/r/kenyanmenonly
/r/kenyanmillennials
/r/kenyanmoms
/r/kenyanpcgamers
/r/kenyanpublicforum
/r/kenyanrelationships
/r/kenyanrottentomatoes
/r/kenyans
/r/kenyansandboas
/r/kenyansconfessions
/r/kenyansingermany
/r/kenyansintech
/r/kenyansinuk
/r/kenyansover25
/r/kenyansover30
/r/kenyansportsbetting
/r/kenyantechies
/r/kenyanteens
/r/kenyantwitter
/r/kenyanwriters
/r/kenyaofficial
/r/kenyaonsite
/r/kenyapetowners
/r/kenyapics
/r/kenyaprisonlife
/r/kenyaredditbookclub
/r/kenyarueda
/r/kenyasafaritips
/r/kenyasingles
/r/kenyastartups
/r/kenyastockmarket
/r/kenyasupplychain
/r/kenyatalk
/r/kenyatech
/r/kenyawest
/r/kenyawfh
/r/kenyawithoutsexposts
/r/kenyayote
/r/kikuyu
/r/kisiis
/r/kisumu
/r/lamu
/r/lesbianskenya
/r/longonoted
/r/malinditownkenya
/r/mentalhealthke
/r/mentalhealthkenya
/r/mombasa_
/r/mombasaconnect
/r/moneymoveskenya
/r/nairobi
/r/nairobitechies
/r/nakuru
/r/no_mods_kenya
/r/onlyinkenya
/r/opportunities_kenya
/r/ourkenya
/r/petownerskenya
/r/psilocybinkenya
/r/quarterlifekenya
/r/realestatejournal
/r/seokenya
/r/siasakenya
/r/stonersofkenya
/r/sweetbutsinful
/r/techinkenya
/r/techkenya
/r/techsupportkenya
/r/undergroundmusicke
/r/unhingedkenya
/r/utawala
/r/war_era_kenya
/r/webdevkenya
/r/youthofkenya
/r/zurikenyaappreciation

"""


def split_subs(subs):
    """Split string and extract subreddit names from various formats
    Supports formats like:
    - /r/subreddit
    - r/subreddit
    - subreddit
    - r/subreddit (description in parentheses)
    - https://old.reddit.com/r/subreddit+.../ | description
    - count,subreddit (e.g., 1679K,startups or 0558K,freelance)
    """
    lines = subs.split("\n")
    subreddit_names = []
    for line in lines:
        line = line.strip()
        if not line:
            continue
        # Handle URL format: https://old.reddit.com/r/sub1+sub2+.../ | description
        if "reddit.com/r/" in line:
            # Extract subreddit part from URL
            import re as re_module
            match = re_module.search(r'reddit\.com/r/([^/|]+)', line)
            if match:
                sub_part = match.group(1)
                # Split by '+' for multireddit URLs
                sub_names = sub_part.split('+')
                subreddit_names.extend(sub_names)
            continue
        # Handle /r/subreddit or r/subreddit format
        line_clean = line
        # Remove description after " | " if present
        if " | " in line:
            line_clean = line.split(" | ")[0].strip()
        # Remove description in parentheses if present
        if "(" in line_clean and ")" in line_clean:
            # Find the last opening parenthesis
            last_open = line_clean.rfind("(")
            # Only remove if it's at the end or followed by closing parenthesis
            if last_open > 0:
                line_clean = line_clean[:last_open].strip()
        # Handle comma-separated format: count,subreddit (e.g., 1679K,startups)
        if "," in line_clean and not line_clean.startswith("/"):
            # Split by comma and take the second part as subreddit name
            parts = line_clean.split(",")
            if len(parts) == 2:
                # First part might be a count like "1679K" or "0558K"
                # Second part is the subreddit name
                potential_sub = parts[1].strip()
                # Check if first part looks like a count (ends with K, M, or is numeric)
                first_part = parts[0].strip()
                if (first_part.endswith('K') or first_part.endswith('M') or
                    first_part.replace('.', '').replace(',', '').isdigit()):
                    line_clean = potential_sub
        # Extract subreddit name
        if line_clean.startswith("/r/"):
            subreddit_names.append(line_clean[3:])
        elif line_clean.startswith("r/"):
            subreddit_names.append(line_clean[2:])
        elif line_clean and not line_clean.startswith("http"):
            # Just the subreddit name
            subreddit_names.append(line_clean)
    # Clean and deduplicate
    subreddit_names = cleanlist(subreddit_names)
    # Handle priority prefixes if they exist (r/, etc format)
    processed = []
    for sub in subreddit_names:
        sub = sub.strip()
        if "/" in sub:
            parts = sub.split("/")
            if len(parts) == 2 and parts[0] in ["r", ""]:
                processed.append((parts[0] if parts[0] else "r", parts[1]))
            else:
                processed.append(("r", sub))
        else:
            processed.append(("r", sub))
    # Sort by prefix and deduplicate
    subs_dict = {kv[1]: kv[0] for kv in processed}
    subs = sorted(subs_dict.items(), key=lambda kv: kv[1], reverse=True)
    subs = [val[0] for val in subs]
    return subs

def main():
    # Start overall timer
    script_start = time.time()

    # Parse command line arguments
    parser = argparse.ArgumentParser(description='Display Kenyan subreddit information')
    parser.add_argument('-t', '--table-only', action='store_true',
                        help='Show table only without launching browser')
    args = parser.parse_args()

    # Path to PRAW config file
    config_path = os.path.expanduser(r"~\.config\mystuff\praw\default.praw.ini")

    # Ensure config file exists
    ensure_config_exists(config_path)

    # Parse subreddits
    subreddits = split_subs(KENYAN_SUBREDDITS)
    console.print(f"\n[green]✓[/green] Found [bold cyan]{len(subreddits)}[/bold cyan] unique Kenyan subreddits")

    if not subreddits:
        console.print("[red]No subreddits found![/red]")
        sys.exit(1)

    # Load Reddit config
    console.print("[cyan]Loading Reddit config...[/cyan]")
    reddit = load_reddit_config(config_path)
    console.print("[green]✓[/green] Reddit config loaded successfully!\n")

    # Fetch subreddit information
    subreddit_info, failed = get_subreddit_info(reddit, subreddits)

    # Display results as table
    successful_info = [info for info in subreddit_info if info['success']]
    nsfw_subreddits = [info['name'] for info in successful_info if info.get('over18', False)]
    display_subreddit_table(subreddit_info)

    # Display failed summary
    display_failed_summary(failed)

    # Calculate total elapsed time
    total_time = time.time() - script_start

    # Create multireddit URL and open in browser (unless -t flag is set)
    if not args.table_only:
        multireddit = '+'.join(subreddits)
        url = f'https://www.reddit.com/r/{multireddit}'

        console.print(f"\n[cyan]Opening Kenyan subreddits multireddit...[/cyan]")
        console.print(f"[dim]{url}[/dim]")

        try:
            webbrowser.open(url)
            console.print("[green]✓[/green] Opened in default browser.")
        except Exception as e:
            console.print(f"[red]Error opening browser:[/red] {e}")
            console.print(f"[yellow]Please visit manually:[/yellow] {url}")
            sys.exit(1)

    # Create and display NSFW multireddit
    if nsfw_subreddits:
        nsfw_multireddit = '+'.join(nsfw_subreddits)
        nsfw_url = f'https://www.reddit.com/r/{nsfw_multireddit}'

        console.print(f"\n[bold red]🔞 NSFW Multireddit ({len(nsfw_subreddits)} subreddits)[/bold red]")
        console.print(f"[red]{nsfw_url}[/red]")
        console.print("[dim]NOTE: Markdown table is already in clipboard (not overwritten by NSFW URL)[/dim]")

    # Print total time
    console.print(f"\n[green]✓[/green] [bold]Total time:[/bold] [yellow]{total_time:.2f}s[/yellow]\n")

if __name__ == "__main__":
    main()

For immediate assistance, please email our customer support: [email protected]

Download RAW File