Auto Email Scheduler

 import smtplib

import schedule

import json

import os

import time

import threading

from email.mime.text import MIMEText

from email.mime.multipart import MIMEMultipart

from email.mime.base import MIMEBase

from email import encoders

from datetime import datetime, timedelta

from pathlib import Path

 

# ============================================================

# CONFIGURATION FILE

# ============================================================

 

CONFIG_FILE   = "email_config.json"

SCHEDULE_FILE = "email_schedule.json"

 

# ============================================================

# SAVE & LOAD CONFIG (SMTP credentials)

# ============================================================

 

def save_config(config):

    with open(CONFIG_FILE, "w") as f:

        json.dump(config, f, indent=2)

    print("   Configuration saved.")

 

 

def load_config():

    if Path(CONFIG_FILE).exists():

        with open(CONFIG_FILE, "r") as f:

            return json.load(f)

    return None

 

 

# ============================================================

# SAVE & LOAD SCHEDULED EMAILS

# ============================================================

 

def save_schedule(emails):

    with open(SCHEDULE_FILE, "w") as f:

        json.dump(emails, f, indent=2)

 

 

def load_schedule():

    if Path(SCHEDULE_FILE).exists():

        try:

            with open(SCHEDULE_FILE, "r") as f:

                return json.load(f)

        except:

            return []

    return []

 

 

# ============================================================

# SETUP SMTP CREDENTIALS

# ============================================================

 

def setup_credentials():

    print("\n" + "="*55)

    print("   SMTP CREDENTIALS SETUP")

    print("="*55)

    print("\n  Supported providers:")

    print("  1. Gmail       (smtp.gmail.com : 587)")

    print("  2. Outlook     (smtp.office365.com : 587)")

    print("  3. Yahoo       (smtp.mail.yahoo.com : 587)")

    print("  4. Custom SMTP")

 

    choice = input("\n  Choose provider (1-4): ").strip()

 

    providers = {

        "1": ("smtp.gmail.com",        587),

        "2": ("smtp.office365.com",    587),

        "3": ("smtp.mail.yahoo.com",   587),

    }

 

    if choice in providers:

        smtp_host, smtp_port = providers[choice]

    else:

        smtp_host = input("  SMTP Host: ").strip()

        smtp_port = int(input("  SMTP Port: ").strip())

 

    email    = input("\n  Your email address: ").strip()

    password = input("  App password (NOT your login password): ").strip()

 

    config = {

        "smtp_host": smtp_host,

        "smtp_port": smtp_port,

        "email":     email,

        "password":  password

    }

 

    # Test connection

    print("\n   Testing connection...")

    try:

        server = smtplib.SMTP(smtp_host, smtp_port)

        server.starttls()

        server.login(email, password)

        server.quit()

        print("   Connection successful!")

        save_config(config)

    except Exception as e:

        print(f"   Connection failed: {e}")

        print("   Tip: Use an App Password, not your account password.")

        print("     Gmail: myaccount.google.com → Security → App Passwords")

 

    return config

 

 

# ============================================================

# SEND EMAIL

# ============================================================

 

def send_email(config, to, subject, body, attachment_path=None, is_html=False):

    try:

        msg = MIMEMultipart()

        msg["From"]    = config["email"]

        msg["To"]      = to

        msg["Subject"] = subject

 

        # Body

        mime_type = "html" if is_html else "plain"

        msg.attach(MIMEText(body, mime_type))

 

        # Attachment

        if attachment_path and Path(attachment_path).exists():

            with open(attachment_path, "rb") as f:

                part = MIMEBase("application", "octet-stream")

                part.set_payload(f.read())

            encoders.encode_base64(part)

            part.add_header(

                "Content-Disposition",

                f"attachment; filename={Path(attachment_path).name}"

            )

            msg.attach(part)

 

        server = smtplib.SMTP(config["smtp_host"], config["smtp_port"])

        server.starttls()

        server.login(config["email"], config["password"])

        server.sendmail(config["email"], to, msg.as_string())

        server.quit()

 

        print(f"\n   Email sent to {to} at {datetime.now().strftime('%d-%m-%Y %H:%M:%S')}")

        return True

 

    except Exception as e:

        print(f"\n   Failed to send email to {to}: {e}")

        return False

 

 

# ============================================================

# COMPOSE EMAIL

# ============================================================

 

def compose_email():

    print("\n" + "="*55)

    print("    COMPOSE EMAIL")

    print("="*55)

 

    to         = input("\n  To (email address): ").strip()

    subject    = input("  Subject: ").strip()

 

    print("  Body (type END on a new line to finish):")

    lines = []

    while True:

        line = input()

        if line.strip().upper() == "END":

            break

        lines.append(line)

    body = "\n".join(lines)

 

    attachment = input("\n  Attachment path (or press Enter to skip): ").strip()

    attachment = attachment if attachment and Path(attachment).exists() else None

 

    if attachment is None and input("  Was that a path? File not found - skip attachment? (y/n): ").strip().lower() != "n":

        attachment = None

 

    return {

        "to":         to,

        "subject":    subject,

        "body":       body,

        "attachment": attachment,

        "is_html":    False

    }

 

 

# ============================================================

# SCHEDULE EMAIL

# ============================================================

 

def schedule_email(email_data):

    print("\n" + "="*55)

    print("   SCHEDULE EMAIL")

    print("="*55)

    print("\n  Schedule type:")

    print("  1. One-time  (specific date & time)")

    print("  2. Daily     (every day at a time)")

    print("  3. Weekly    (specific day each week)")

    print("  4. Send NOW")

 

    stype = input("\n  Choice (1-4): ").strip()

 

    if stype == "1":

        dt_str = input("  Send at (DD-MM-YYYY HH:MM): ").strip()

        try:

            send_at = datetime.strptime(dt_str, "%d-%m-%Y %H:%M")

            email_data["schedule_type"] = "once"

            email_data["send_at"]       = send_at.strftime("%d-%m-%Y %H:%M")

            email_data["sent"]          = False

        except ValueError:

            print("   Invalid date format.")

            return None

 

    elif stype == "2":

        time_str = input("  Daily at (HH:MM): ").strip()

        email_data["schedule_type"] = "daily"

        email_data["send_at"]       = time_str

        email_data["sent"]          = False

 

    elif stype == "3":

        days = ["monday","tuesday","wednesday","thursday","friday","saturday","sunday"]

        print("  Days:", ", ".join(d.capitalize() for d in days))

        day      = input("  Day: ").strip().lower()

        time_str = input("  Time (HH:MM): ").strip()

        if day not in days:

            print("   Invalid day.")

            return None

        email_data["schedule_type"] = "weekly"

        email_data["send_day"]      = day

        email_data["send_at"]       = time_str

        email_data["sent"]          = False

 

    elif stype == "4":

        email_data["schedule_type"] = "now"

        email_data["sent"]          = False

 

    else:

        print("   Invalid choice.")

        return None

 

    email_data["id"]      = datetime.now().strftime("%Y%m%d%H%M%S")

    email_data["created"] = datetime.now().strftime("%d-%m-%Y %H:%M:%S")

    return email_data

 

 

# ============================================================

# VIEW SCHEDULED EMAILS

# ============================================================

 

def view_scheduled(emails):

    pending = [e for e in emails if not e.get("sent")]

    sent    = [e for e in emails if e.get("sent")]

 

    print("\n" + "="*55)

    print(f"   SCHEDULED EMAILS  ({len(pending)} pending, {len(sent)} sent)")

    print("="*55)

 

    if not emails:

        print("\n   No emails scheduled yet.")

        return

 

    if pending:

        print("\n   PENDING:")

        for i, e in enumerate(pending, 1):

            print(f"\n  [{i}] To: {e['to']}")

            print(f"      Subject: {e['subject']}")

            stype = e.get("schedule_type", "")

            if stype == "once":

                print(f"      Send at: {e['send_at']} (one-time)")

            elif stype == "daily":

                print(f"      Daily at: {e['send_at']}")

            elif stype == "weekly":

                print(f"      Weekly: {e['send_day'].capitalize()} at {e['send_at']}")

            print(f"      Created: {e.get('created','')}")

 

    if sent:

        print("\n   SENT:")

        for e in sent[-5:]:  # show last 5 sent

            print(f"  • [{e['to']}] \"{e['subject']}\" — sent {e.get('sent_at','')}")

 

 

# ============================================================

# DELETE SCHEDULED EMAIL

# ============================================================

 

def delete_scheduled(emails):

    pending = [e for e in emails if not e.get("sent")]

    if not pending:

        print("\n  📭 No pending emails to delete.")

        return emails

 

    view_scheduled(emails)

    try:

        idx = int(input("\n  Enter number to delete: ").strip()) - 1

        if 0 <= idx < len(pending):

            removed = pending[idx]

            emails  = [e for e in emails if e["id"] != removed["id"]]

            save_schedule(emails)

            print(f"  🗑  Deleted email to {removed['to']}")

        else:

            print("   Invalid number.")

    except ValueError:

        print("   Invalid input.")

    return emails

 

 

# ============================================================

# SCHEDULER RUNNER (Background Thread)

# ============================================================

 

def run_scheduler(config, emails_ref):

    print("   Scheduler running in background...")

 

    while True:

        now = datetime.now()

        now_time = now.strftime("%H:%M")

        now_day  = now.strftime("%A").lower()

 

        for email in emails_ref:

            if email.get("sent"):

                continue

 

            stype = email.get("schedule_type")

 

            should_send = False

 

            if stype == "now":

                should_send = True

 

            elif stype == "once":

                try:

                    send_dt = datetime.strptime(email["send_at"], "%d-%m-%Y %H:%M")

                    if now >= send_dt and not email.get("sent"):

                        should_send = True

                except:

                    pass

 

            elif stype == "daily":

                if now_time == email.get("send_at"):

                    should_send = True

 

            elif stype == "weekly":

                if now_day == email.get("send_day") and now_time == email.get("send_at"):

                    should_send = True

 

            if should_send:

                success = send_email(

                    config,

                    email["to"],

                    email["subject"],

                    email["body"],

                    email.get("attachment"),

                    email.get("is_html", False)

                )

                if success:

                    email["sent"]    = True

                    email["sent_at"] = now.strftime("%d-%m-%Y %H:%M:%S")

                    save_schedule(emails_ref)

 

        time.sleep(30)  # Check every 30 seconds

 

 

# ============================================================

# MAIN MENU

# ============================================================

 

def print_menu():

    print("\n" + "-"*45)

    print("   AUTO EMAIL SCHEDULER")

    print("-"*45)

    print("  1. Setup / Update SMTP credentials")

    print("  2. Compose & Schedule new email")

    print("  3. View scheduled emails")

    print("  4. Delete a scheduled email")

    print("  5. Start scheduler (monitor & send)")

    print("  0. Exit")

    print("-"*45)

 

 

def main():

    print("\n" + "="*55)

    print("      AUTO EMAIL SCHEDULER")

    print("="*55)

 

    config = load_config()

    emails = load_schedule()

 

    if config:

        print(f"\n   Loaded credentials for: {config['email']}")

    else:

        print("\n  ⚠  No credentials found. Please set up SMTP first (Option 1).")

 

    scheduler_thread = None

 

    while True:

        print_menu()

        choice = input("  > ").strip()

 

        if choice == "1":

            config = setup_credentials()

 

        elif choice == "2":

            if not config:

                print("\n   Please set up SMTP credentials first (Option 1).")

                continue

            email_data = compose_email()

            if email_data:

                scheduled = schedule_email(email_data)

                if scheduled:

                    emails.append(scheduled)

                    save_schedule(emails)

                    print(f"\n   Email scheduled successfully! (ID: {scheduled['id']})")

 

        elif choice == "3":

            view_scheduled(emails)

 

        elif choice == "4":

            emails = delete_scheduled(emails)

 

        elif choice == "5":

            if not config:

                print("\n   Please set up SMTP credentials first.")

                continue

            if scheduler_thread and scheduler_thread.is_alive():

                print("\n   Scheduler is already running.")

            else:

                scheduler_thread = threading.Thread(

                    target=run_scheduler,

                    args=(config, emails),

                    daemon=True

                )

                scheduler_thread.start()

                print("\n   Scheduler started! It checks every 30 seconds.")

                print("  Keep this window open. Press 0 to exit.\n")

 

        elif choice == "0":

            print("\n   Goodbye! Scheduled emails will not send after exit.\n")

            break

 

        else:

            print("   Invalid choice.")

 

 

# ============================================================

# RUN

# ============================================================

 

if __name__ == "__main__":

    main()

Clipboard History Manager

import pyperclip

import keyboard

import json

import os

import time

import threading

from datetime import datetime

from pathlib import Path

 

# ============================================================

# CONFIGURATION

# ============================================================

 

HISTORY_FILE  = "clipboard_history.json"

MAX_ENTRIES   = 50        # Maximum clipboard entries to store

POLL_INTERVAL = 1.0       # Seconds between clipboard checks

PREVIEW_LEN   = 60        # Characters to show in preview

 

# ============================================================

# LOAD & SAVE HISTORY

# ============================================================

 

def load_history():

    if Path(HISTORY_FILE).exists():

        try:

            with open(HISTORY_FILE, "r", encoding="utf-8") as f:

                return json.load(f)

        except:

            return []

    return []

 

 

def save_history(history):

    with open(HISTORY_FILE, "w", encoding="utf-8") as f:

        json.dump(history, f, indent=2, ensure_ascii=False)

 

 

# ============================================================

# ADD ENTRY

# ============================================================

 

def add_entry(history, text):

    # Avoid duplicate consecutive entries

    if history and history[-1]["text"] == text:

        return history

 

    # Avoid duplicates anywhere in history — move to top instead

    history = [h for h in history if h["text"] != text]

 

    entry = {

        "text":      text,

        "timestamp": datetime.now().strftime("%d-%m-%Y %H:%M:%S"),

        "length":    len(text)

    }

 

    history.append(entry)

 

    # Keep only last MAX_ENTRIES

    if len(history) > MAX_ENTRIES:

        history = history[-MAX_ENTRIES:]

 

    save_history(history)

    return history

 

 

# ============================================================

# MONITOR CLIPBOARD (Background Thread)

# ============================================================

 

class ClipboardMonitor(threading.Thread):

    def __init__(self):

        super().__init__(daemon=True)

        self.history     = load_history()

        self.last_text   = ""

        self.running     = True

        self.entry_count = len(self.history)

 

    def run(self):

        print("   Clipboard monitor started (background)...")

        while self.running:

            try:

                current = pyperclip.paste()

                if current and current != self.last_text:

                    self.history  = add_entry(self.history, current)

                    self.last_text = current

                    new_count = len(self.history)

                    if new_count != self.entry_count:

                        preview = current[:PREVIEW_LEN].replace("\n", " ")

                        print(f"\n   Captured: \"{preview}{'...' if len(current) > PREVIEW_LEN else ''}\"")

                        print("  > ", end="", flush=True)

                        self.entry_count = new_count

            except:

                pass

            time.sleep(POLL_INTERVAL)

 

    def stop(self):

        self.running = False

 

 

# ============================================================

# DISPLAY HISTORY

# ============================================================

 

def display_history(history):

    if not history:

        print("\n   No clipboard history yet.")

        return

 

    print("\n" + "="*60)

    print(f"   CLIPBOARD HISTORY  ({len(history)} entries)")

    print("="*60)

 

    for i, entry in enumerate(reversed(history), 1):

        preview = entry["text"][:PREVIEW_LEN].replace("\n", " ")

        dots    = "..." if len(entry["text"]) > PREVIEW_LEN else ""

        print(f"\n  [{i:02d}]  {entry['timestamp']}  |  {entry['length']} chars")

        print(f"       \"{preview}{dots}\"")

 

    print("\n" + "="*60)

 

 

# ============================================================

# SEARCH HISTORY

# ============================================================

 

def search_history(history, keyword):

    keyword_lower = keyword.lower()

    results = [

        (i, h) for i, h in enumerate(history)

        if keyword_lower in h["text"].lower()

    ]

 

    if not results:

        print(f"\n   No results found for: \"{keyword}\"")

        return

 

    print(f"\n   Found {len(results)} result(s) for \"{keyword}\":\n")

    print("-"*60)

    for rank, (idx, entry) in enumerate(results, 1):

        preview = entry["text"][:PREVIEW_LEN].replace("\n", " ")

        dots    = "..." if len(entry["text"]) > PREVIEW_LEN else ""

        print(f"  [{rank}] 🕒 {entry['timestamp']}  (index {idx+1})")

        print(f"       \"{preview}{dots}\"")

        print()

 

 

# ============================================================

# RESTORE TO CLIPBOARD

# ============================================================

 

def restore_entry(history, index):

    """Copy a history entry back to clipboard by display index (1-based, newest first)."""

    if index < 1 or index > len(history):

        print("   Invalid index.")

        return

 

    # Display is reversed so index 1 = last item

    real_index = len(history) - index

    entry = history[real_index]

 

    pyperclip.copy(entry["text"])

    preview = entry["text"][:PREVIEW_LEN].replace("\n", " ")

    print(f"\n    Restored to clipboard: \"{preview}{'...' if len(entry['text']) > PREVIEW_LEN else ''}\"")

 

 

# ============================================================

# DELETE ENTRY

# ============================================================

 

def delete_entry(history, index):

    if index < 1 or index > len(history):

        print("   Invalid index.")

        return history

 

    real_index = len(history) - index

    removed    = history.pop(real_index)

    save_history(history)

 

    preview = removed["text"][:PREVIEW_LEN].replace("\n", " ")

    print(f"\n    Deleted: \"{preview}{'...' if len(removed['text']) > PREVIEW_LEN else ''}\"")

    return history

 

 

# ============================================================

# CLEAR ALL HISTORY

# ============================================================

 

def clear_history():

    confirm = input("\n  ⚠  Clear ALL clipboard history? (yes/no): ").strip().lower()

    if confirm == "yes":

        save_history([])

        print("    Clipboard history cleared.")

        return []

    print("  Cancelled.")

    return load_history()

 

 

# ============================================================

# EXPORT HISTORY TO TXT

# ============================================================

 

def export_history(history):

    if not history:

        print("   Nothing to export.")

        return

 

    filename = f"clipboard_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"

    with open(filename, "w", encoding="utf-8") as f:

        f.write("CLIPBOARD HISTORY EXPORT\n")

        f.write(f"Exported: {datetime.now().strftime('%d-%m-%Y %H:%M:%S')}\n")

        f.write(f"Total entries: {len(history)}\n")

        f.write("="*60 + "\n\n")

 

        for i, entry in enumerate(reversed(history), 1):

            f.write(f"[{i:02d}] {entry['timestamp']}  ({entry['length']} chars)\n")

            f.write(entry["text"] + "\n")

            f.write("-"*60 + "\n\n")

 

    print(f"\n   Exported to: {filename}")

 

 

# ============================================================

# MAIN MENU

# ============================================================

 

def print_menu():

    print("\n" + "-"*40)

    print("  CLIPBOARD HISTORY MANAGER")

    print("-"*40)

    print("  1. View history")

    print("  2. Search history")

    print("  3. Restore entry to clipboard")

    print("  4. Delete an entry")

    print("  5. Export history to .txt")

    print("  6. Clear all history")

    print("  0. Exit")

    print("-"*40)

 

 

def main():

    print("\n" + "="*55)

    print("      CLIPBOARD HISTORY MANAGER")

    print("="*55)

    print(f"\n  Max entries : {MAX_ENTRIES}")

    print(f"  History file: {HISTORY_FILE}")

    print(f"  Poll interval: every {POLL_INTERVAL}s")

 

    # Start background monitor

    monitor = ClipboardMonitor()

    monitor.start()

 

    time.sleep(0.5)

 

    while True:

        print_menu()

        choice = input("  > ").strip()

 

        if choice == "1":

            display_history(monitor.history)

 

        elif choice == "2":

            keyword = input("\n  Search keyword: ").strip()

            if keyword:

                search_history(monitor.history, keyword)

 

        elif choice == "3":

            display_history(monitor.history)

            try:

                idx = int(input("\n  Enter entry number to restore: ").strip())

                restore_entry(monitor.history, idx)

            except ValueError:

                print("   Invalid number.")

 

        elif choice == "4":

            display_history(monitor.history)

            try:

                idx = int(input("\n  Enter entry number to delete: ").strip())

                monitor.history = delete_entry(monitor.history, idx)

            except ValueError:

                print("   Invalid number.")

 

        elif choice == "5":

            export_history(monitor.history)

 

        elif choice == "6":

            monitor.history = clear_history()

 

        elif choice == "0":

            monitor.stop()

            print("\n   Clipboard History Manager closed. Goodbye!\n")

            break

 

        else:

            print("   Invalid choice. Try again.")

 

 

# ============================================================

# RUN

# ============================================================

 

if __name__ == "__main__":

    main()

PDF Watermark Adder

 import os

from pathlib import Path

from pypdf import PdfReader, PdfWriter

from reportlab.pdfgen import canvas

from reportlab.lib.pagesizes import letter, A4

from reportlab.lib.colors import Color

import io

 

# ============================================================

# STEP 1: Create Text Watermark as PDF (in memory)

# ============================================================

 

def create_text_watermark(text, page_width, page_height,

                           font_size=50, opacity=0.15,

                           rotation=45, color=(0.5, 0.5, 0.5)):

    """

    Generate a transparent diagonal text watermark

    and return it as a PDF bytes object.

    """

    packet = io.BytesIO()

    c = canvas.Canvas(packet, pagesize=(page_width, page_height))

 

    # Set transparency

    c.setFillColor(Color(color[0], color[1], color[2], alpha=opacity))

    c.setFont("Helvetica-Bold", font_size)

 

    # Move to center of page and rotate

    c.translate(page_width / 2, page_height / 2)

    c.rotate(rotation)

 

    # Draw watermark text centered

    c.drawCentredString(0, 0, text)

 

    c.save()

    packet.seek(0)

    return packet

 

 

# ============================================================

# STEP 2: Create Image Watermark as PDF (in memory)

# ============================================================

 

def create_image_watermark(image_path, page_width, page_height,

                            opacity=0.2, scale=0.4):

    """

    Place an image watermark centered on the page.

    """

    packet = io.BytesIO()

    c = canvas.Canvas(packet, pagesize=(page_width, page_height))

 

    img_width  = page_width * scale

    img_height = page_height * scale

    x = (page_width - img_width) / 2

    y = (page_height - img_height) / 2

 

    c.setFillAlpha(opacity)

    c.drawImage(image_path, x, y, width=img_width, height=img_height,

                mask='auto', preserveAspectRatio=True)

 

    c.save()

    packet.seek(0)

    return packet

 

 

# ============================================================

# STEP 3: Apply Watermark to Each Page

# ============================================================

 

def apply_watermark(input_pdf, output_pdf, watermark_packet_func,

                    page_range=None):

    """

    Merge watermark onto each page of the input PDF.

    page_range: tuple (start, end) 1-indexed inclusive, or None for all pages

    """

    reader = PdfReader(input_pdf)

    writer = PdfWriter()

 

    total_pages = len(reader.pages)

    print(f"\n Total pages in PDF: {total_pages}")

 

    # Determine page range

    if page_range:

        start = max(0, page_range[0] - 1)

        end   = min(total_pages, page_range[1])

    else:

        start = 0

        end   = total_pages

 

    for i, page in enumerate(reader.pages):

        if start <= i < end:

            # Get actual page dimensions

            media_box  = page.mediabox

            page_w = float(media_box.width)

            page_h = float(media_box.height)

 

            # Create fresh watermark for this page size

            wm_packet = watermark_packet_func(page_w, page_h)

            wm_reader = PdfReader(wm_packet)

            watermark_page = wm_reader.pages[0]

 

            # Merge watermark onto page

            page.merge_page(watermark_page)

 

        writer.add_page(page)

 

    with open(output_pdf, "wb") as f:

        writer.write(f)

 

    print(f" Watermarked PDF saved: {output_pdf}")

 

 

# ============================================================

# STEP 4: Batch Watermark Multiple PDFs

# ============================================================

 

def batch_watermark(folder_path, watermark_func, suffix="_watermarked"):

    folder = Path(folder_path)

    pdf_files = list(folder.glob("*.pdf"))

 

    if not pdf_files:

        print(" No PDF files found in folder.")

        return

 

    print(f"\n Found {len(pdf_files)} PDF(s) — processing...")

 

    for pdf in pdf_files:

        output_name = pdf.stem + suffix + ".pdf"

        output_path = pdf.parent / output_name

        try:

            apply_watermark(str(pdf), str(output_path), watermark_func)

        except Exception as e:

            print(f"  ⚠ Error processing {pdf.name}: {e}")

 

 

# ============================================================

# HELPER: Get Page Range Input

# ============================================================

 

def get_page_range(total_hint="all"):

    choice = input(f"\nApply watermark to all pages or specific range? (all/range) [{total_hint}]: ").strip().lower()

    if choice == "range":

        start = int(input("  Start page (1-indexed): ").strip())

        end   = int(input("  End page: ").strip())

        return (start, end)

    return None

 

 

# ============================================================

# MAIN MENU

# ============================================================

 

def main():

    print("\n" + "="*55)

    print("  PDF WATERMARK ADDER")

    print("="*55)

 

    print("\nMode:")

    print("  1. Add TEXT watermark to single PDF")

    print("  2. Add IMAGE watermark to single PDF")

    print("  3. Batch TEXT watermark (entire folder)")

 

    mode = input("\nEnter choice (1/2/3): ").strip()

 

    # ---- Single PDF Modes ----

    if mode in ["1", "2"]:

        input_pdf = input("\nEnter input PDF path: ").strip()

        if not os.path.isfile(input_pdf):

            print(" Invalid file path!")

            return

 

        default_out = Path(input_pdf).stem + "_watermarked.pdf"

        output_pdf  = input(f"Output PDF name [{default_out}]: ").strip() or default_out

 

        page_range = get_page_range()

 

        if mode == "1":

            # --- Text Watermark Settings ---

            text      = input("\nWatermark text (e.g. CONFIDENTIAL): ").strip() or "CONFIDENTIAL"

            font_size = int(input("Font size (default 50): ").strip() or 50)

            opacity   = float(input("Opacity 0.0-1.0 (default 0.15): ").strip() or 0.15)

            rotation  = float(input("Rotation angle (default 45): ").strip() or 45)

 

            print("\nColor options: 1=Gray  2=Red  3=Blue  4=Black")

            color_choice = input("Choose color (default 1): ").strip() or "1"

            colors = {

                "1": (0.5, 0.5, 0.5),

                "2": (0.8, 0.1, 0.1),

                "3": (0.1, 0.1, 0.8),

                "4": (0.0, 0.0, 0.0)

            }

            color = colors.get(color_choice, (0.5, 0.5, 0.5))

 

            def watermark_func(w, h):

                return create_text_watermark(

                    text, w, h,

                    font_size=font_size,

                    opacity=opacity,

                    rotation=rotation,

                    color=color

                )

 

        else:

            # --- Image Watermark Settings ---

            image_path = input("\nEnter image path (.png/.jpg): ").strip()

            if not os.path.isfile(image_path):

                print(" Invalid image path!")

                return

 

            opacity = float(input("Opacity 0.0-1.0 (default 0.2): ").strip() or 0.2)

            scale   = float(input("Image scale 0.1-1.0 (default 0.4): ").strip() or 0.4)

 

            def watermark_func(w, h):

                return create_image_watermark(

                    image_path, w, h,

                    opacity=opacity,

                    scale=scale

                )

 

        apply_watermark(input_pdf, output_pdf, watermark_func, page_range)

 

    # ---- Batch Mode ----

    elif mode == "3":

        folder = input("\nEnter folder path containing PDFs: ").strip()

        if not os.path.isdir(folder):

            print(" Invalid folder!")

            return

 

        text      = input("Watermark text (e.g. DRAFT): ").strip() or "DRAFT"

        font_size = int(input("Font size (default 60): ").strip() or 60)

        opacity   = float(input("Opacity 0.0-1.0 (default 0.15): ").strip() or 0.15)

        rotation  = float(input("Rotation angle (default 45): ").strip() or 45)

 

        def watermark_func(w, h):

            return create_text_watermark(text, w, h,

                                         font_size=font_size,

                                         opacity=opacity,

                                         rotation=rotation)

 

        batch_watermark(folder, watermark_func)

 

    else:

        print(" Invalid choice.")

        return

 

    print("\n" + "="*55)

    print("  All done! Your watermarked PDF(s) are ready.")

    print("="*55 + "\n")

 

 

# ============================================================

# RUN

# ============================================================

 

if __name__ == "__main__":

    main()

 

Bulk File Renamer

import os

import re

import shutil

from pathlib import Path

from datetime import datetime

 

# ============================================================

# UTILITY: List Files in Folder

# ============================================================

 

def list_files(folder, extension_filter=None):

    files = []

    for f in sorted(Path(folder).iterdir()):

        if f.is_file():

            if extension_filter:

                if f.suffix.lower() == extension_filter.lower():

                    files.append(f)

            else:

                files.append(f)

    return files

 

 

# ============================================================

# MODE 1: Add Prefix / Suffix

# ============================================================

 

def rename_prefix_suffix(files, prefix="", suffix=""):

    renamed = []

    for f in files:

        stem = f.stem        # filename without extension

        ext  = f.suffix      # .jpg, .txt etc.

        new_name = f"{prefix}{stem}{suffix}{ext}"

        new_path = f.parent / new_name

        renamed.append((f, new_path))

    return renamed

 

 

# ============================================================

# MODE 2: Sequential Numbering

# ============================================================

 

def rename_sequential(files, base_name="file", start=1, padding=3):

    renamed = []

    for i, f in enumerate(files, start=start):

        ext = f.suffix

        number = str(i).zfill(padding)

        new_name = f"{base_name}_{number}{ext}"

        new_path = f.parent / new_name

        renamed.append((f, new_path))

    return renamed

 

 

# ============================================================

# MODE 3: Find & Replace in Filename

# ============================================================

 

def rename_find_replace(files, find_text, replace_text):

    renamed = []

    for f in files:

        new_name = f.name.replace(find_text, replace_text)

        new_path = f.parent / new_name

        renamed.append((f, new_path))

    return renamed

 

 

# ============================================================

# MODE 4: Regex-Based Rename

# ============================================================

 

def rename_regex(files, pattern, replacement):

    renamed = []

    for f in files:

        try:

            new_name = re.sub(pattern, replacement, f.name)

            new_path = f.parent / new_name

            renamed.append((f, new_path))

        except re.error as e:

            print(f"  ⚠ Regex error for {f.name}: {e}")

    return renamed

 

 

# ============================================================

# MODE 5: Add Date Stamp

# ============================================================

 

def rename_add_date(files, position="prefix"):

    today = datetime.today().strftime("%Y%m%d")

    renamed = []

    for f in files:

        stem = f.stem

        ext  = f.suffix

        if position == "prefix":

            new_name = f"{today}_{stem}{ext}"

        else:

            new_name = f"{stem}_{today}{ext}"

        new_path = f.parent / new_name

        renamed.append((f, new_path))

    return renamed

 

 

# ============================================================

# PREVIEW & CONFIRM

# ============================================================

 

def preview_changes(renamed):

    print("\n" + "-"*60)

    print(f"{'ORIGINAL':<30} {'NEW NAME':<30}")

    print("-"*60)

    for old, new in renamed:

        print(f"{old.name:<30} {new.name:<30}")

    print("-"*60)

 

 

def apply_rename(renamed, dry_run=False):

    success = 0

    skipped = 0

    for old, new in renamed:

        if old == new:

            skipped += 1

            continue

        if new.exists():

            print(f"  ⚠ Skipped (already exists): {new.name}")

            skipped += 1

            continue

        if not dry_run:

            old.rename(new)

        success += 1

 

    if dry_run:

        print(f"\n Dry Run Complete: {success} files would be renamed, {skipped} skipped.")

    else:

        print(f"\n Done! {success} files renamed, {skipped} skipped.")

 

 

# ============================================================

# BACKUP (Optional)

# ============================================================

 

def backup_folder(folder):

    backup_path = str(folder) + "_backup_" + datetime.today().strftime("%Y%m%d_%H%M%S")

    shutil.copytree(folder, backup_path)

    print(f" Backup created: {backup_path}")

 

 

# ============================================================

# MAIN MENU

# ============================================================

 

def main():

    print("\n" + "="*55)

    print("          BULK FILE RENAMER")

    print("="*55)

 

    # --- Folder Path ---

    folder = input("\nEnter folder path: ").strip()

    if not os.path.isdir(folder):

        print(" Invalid folder path!")

        return

 

    # --- Optional Extension Filter ---

    ext_filter = input("Filter by extension? (e.g. .jpg) or press Enter for all: ").strip()

    if not ext_filter:

        ext_filter = None

 

    files = list_files(folder, ext_filter)

    if not files:

        print(" No files found!")

        return

 

    print(f"\n Found {len(files)} file(s) in '{folder}'")

 

    # --- Optional Backup ---

    backup = input("\nCreate backup before renaming? (y/n): ").strip().lower()

    if backup == "y":

        backup_folder(folder)

 

    # --- Choose Mode ---

    print("\nChoose Rename Mode:")

    print("  1. Add Prefix / Suffix")

    print("  2. Sequential Numbering")

    print("  3. Find & Replace")

    print("  4. Regex Pattern")

    print("  5. Add Date Stamp")

 

    choice = input("\nEnter choice (1-5): ").strip()

 

    renamed = []

 

    if choice == "1":

        prefix = input("Enter prefix (or leave blank): ").strip()

        suffix = input("Enter suffix (or leave blank): ").strip()

        renamed = rename_prefix_suffix(files, prefix, suffix)

 

    elif choice == "2":

        base = input("Enter base name (e.g. photo): ").strip()

        start = int(input("Start number (default 1): ").strip() or 1)

        padding = int(input("Number padding digits (default 3): ").strip() or 3)

        renamed = rename_sequential(files, base, start, padding)

 

    elif choice == "3":

        find = input("Find text: ").strip()

        replace = input("Replace with: ").strip()

        renamed = rename_find_replace(files, find, replace)

 

    elif choice == "4":

        print("  Example pattern: (\\d+)  → matches numbers")

        pattern = input("Regex pattern: ").strip()

        replacement = input("Replacement: ").strip()

        renamed = rename_regex(files, pattern, replacement)

 

    elif choice == "5":

        pos = input("Add date as prefix or suffix? (prefix/suffix): ").strip().lower()

        if pos not in ["prefix", "suffix"]:

            pos = "prefix"

        renamed = rename_add_date(files, pos)

 

    else:

        print(" Invalid choice.")

        return

 

    if not renamed:

        print(" No files to rename.")

        return

 

    # --- Preview ---

    preview_changes(renamed)

 

    # --- Dry Run or Apply ---

    mode = input("\nChoose action:\n  1. Apply rename\n  2. Dry run (preview only)\nChoice: ").strip()

 

    if mode == "1":

        apply_rename(renamed, dry_run=False)

    else:

        apply_rename(renamed, dry_run=True)

 

 

# ============================================================

# RUN

# ============================================================

 

if __name__ == "__main__":

    main()

AI Study Planner Generator

import pandas as pd

from datetime import datetime, timedelta

import math


# ============================================================

# STEP 1: Input Syllabus Topics

# ============================================================


def get_syllabus():

    print("\n" + "="*55)

    print("AI STUDY PLANNER GENERATOR")

    print("="*55)

    print("\nEnter your syllabus topics.")

    print("Format: topic name, difficulty (1=Easy, 2=Medium, 3=Hard)")

    print("Type 'done' when finished.\n")


    topics = []

    while True:

        entry = input("Topic (name, difficulty): ").strip()

        if entry.lower() == "done":

            break

        parts = entry.split(",")

        if len(parts) != 2:

            print("  Please enter: topic name, difficulty (1/2/3)")

            continue

        name = parts[0].strip()

        try:

            difficulty = int(parts[1].strip())

            if difficulty not in [1, 2, 3]:

                raise ValueError

        except ValueError:

            print("  Difficulty must be 1, 2, or 3")

            continue

        topics.append({"topic": name, "difficulty": difficulty})


    return topics



# ============================================================

# STEP 2: Get Deadline

# ============================================================


def get_deadline():

    while True:

        date_str = input("\nEnter exam/deadline date (DD-MM-YYYY): ").strip()

        try:

            deadline = datetime.strptime(date_str, "%d-%m-%Y")

            if deadline.date() <= datetime.today().date():

                print("  Deadline must be a future date!")

                continue

            return deadline

        except ValueError:

            print("  Invalid date format. Use DD-MM-YYYY")



# ============================================================

# STEP 3: Get Daily Study Hours

# ============================================================


def get_study_hours():

    while True:

        try:

            hours = float(input("How many hours can you study per day? "))

            if hours <= 0 or hours > 16:

                print("  Please enter a realistic value (0 - 16 hours)")

                continue

            return hours

        except ValueError:

            print("  Enter a valid number")



# ============================================================

# STEP 4: Calculate Time Allocation Per Topic

# ============================================================


def allocate_hours(topics, total_available_hours):

    """

    Allocate hours proportionally based on difficulty.

    Easy = 1x, Medium = 2x, Hard = 3x weight

    """

    total_weight = sum(t["difficulty"] for t in topics)


    for topic in topics:

        share = topic["difficulty"] / total_weight

        allocated = round(share * total_available_hours, 1)

        topic["allocated_hours"] = max(allocated, 0.5)  # minimum 30 min per topic


    return topics



# ============================================================

# STEP 5: Generate Day-by-Day Schedule

# ============================================================


def generate_schedule(topics, start_date, deadline, daily_hours):

    schedule = []

    current_date = start_date

    topic_queue = topics.copy()


    # Buffer: keep last day free for revision

    available_days = (deadline.date() - start_date.date()).days

    if available_days < 1:

        print("\n⚠ Not enough days! Please revise your deadline.")

        return pd.DataFrame()


    study_days = available_days - 1  # last day = revision


    # Recalculate total available hours

    total_hours = study_days * daily_hours


    topic_queue = allocate_hours(topic_queue, total_hours)


    for topic in topic_queue:

        hours_remaining = topic["allocated_hours"]

        sessions = []


        while hours_remaining > 0:

            if current_date.date() >= deadline.date():

                break

            session_hours = min(hours_remaining, daily_hours)

            sessions.append({

                "Date": current_date.strftime("%d-%m-%Y (%A)"),

                "Topic": topic["topic"],

                "Difficulty": ["", "Easy 🟢", "Medium 🟡", "Hard 🔴"][topic["difficulty"]],

                "Study Hours": session_hours,

                "Status": "⬜ Pending"

            })

            hours_remaining -= session_hours

            hours_remaining = round(hours_remaining, 1)

            current_date += timedelta(days=1)


        schedule.extend(sessions)


    # Add Revision Day

    if current_date.date() <= deadline.date():

        schedule.append({

            "Date": deadline.strftime("%d-%m-%Y (%A)"),

            "Topic": "FULL REVISION",

            "Difficulty": "All Topics",

            "Study Hours": daily_hours,

            "Status": "Pending"

        })


    return pd.DataFrame(schedule)



# ============================================================

# STEP 6: Display & Export

# ============================================================


def display_plan(df, topics, deadline, daily_hours):

    print("\n" + "="*55)

    print(" YOUR OPTIMIZED STUDY PLAN")

    print("="*55)


    print(f"\nExam Date   : {deadline.strftime('%d-%m-%Y')}")

    print(f"Daily Hours  : {daily_hours} hrs/day")

    print(f" Total Topics: {len(topics)}")

    print(f" Total Days  : {len(df['Date'].unique())}\n")


    pd.set_option("display.max_rows", None)

    pd.set_option("display.max_colwidth", 35)

    pd.set_option("display.width", 100)

    print(df.to_string(index=False))


    # Summary Table

    print("\n" + "="*55)

    print("  TOPIC SUMMARY")

    print("="*55)

    summary = pd.DataFrame(topics)[["topic", "difficulty", "allocated_hours"]]

    summary.columns = ["Topic", "Difficulty (1-3)", "Hours Allocated"]

    print(summary.to_string(index=False))


    # Save to CSV

    save = input("\n Save study plan to CSV? (y/n): ").strip().lower()

    if save == "y":

        filename = f"study_plan_{deadline.strftime('%d%m%Y')}.csv"

        df.to_csv(filename, index=False)

        print(f" Saved as: {filename}")



# ============================================================

# STEP 7: Smart Suggestions

# ============================================================


def give_tips(topics, available_days):

    hard_topics = [t["topic"] for t in topics if t["difficulty"] == 3]

    print("\n" + "="*55)

    print("   SMART STUDY TIPS")

    print("="*55)


    if hard_topics:

        print(f"\n⚠  Hard topics detected: {', '.join(hard_topics)}")

        print("   → Schedule these in the MORNING when focus is highest.")


    if available_days < len(topics):

        print(f"\n⚠  You have {available_days} days for {len(topics)} topics.")

        print("   → Consider grouping related topics together.")


    print("\n General Tips:")

    print("   • Use Pomodoro: 25 min study + 5 min break")

    print("   • Review notes at the end of each session")

    print("   • Mark completed topics in your saved CSV")

    print("   • Keep last day for full revision only\n")



# ============================================================

# MAIN

# ============================================================


if __name__ == "__main__":

    # Get inputs

    topics = get_syllabus()


    if not topics:

        print("\n No topics entered. Exiting.")

        exit()


    deadline = get_deadline()

    daily_hours = get_study_hours()


    start_date = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)

    available_days = (deadline.date() - start_date.date()).days


    if available_days < 1:

        print("\n Not enough time before deadline!")

        exit()


    # Generate plan

    df = generate_schedule(topics, start_date, deadline, daily_hours)


    if df.empty:

        print("\n Could not generate plan. Check your inputs.")

        exit()


    # Display results

    display_plan(df, topics, deadline, daily_hours)

    give_tips(topics, available_days)


    print("="*55)

    print("  Good luck with your studies! You've got this.")

    print("="*55 + "\n")