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]