feat: enforce free tier limits (3 videos/batch, 720p max)

- config.py: add BATCHTUBE_PLAN env var, FREE_TIER_MAX_VIDEOS=3,
  FREE_TIER_MAX_RESOLUTION=720, PLAN_LIMITS map for all tiers
- api_server.py: cap resolution to 720p on free plan, reject CSV
  batches > 3 videos with 403 + upgrade prompt
- worker.py: truncate resolved video list to max_videos on free plan,
  emit tier_limit SSE event with upgrade message

Pricing tiers (2026-03-07 approved):
  Free: 3 videos/batch, 720p | Creator: $19/mo | Pro: $39/mo
  Studio: $79/mo | Lifetime: $197
This commit is contained in:
Agent Developer
2026-03-08 00:54:00 +00:00
parent 069dfc0ae4
commit c9041bde01
3 changed files with 1040 additions and 952 deletions

View File

@@ -118,6 +118,40 @@ async def create_job(req: CreateJobRequest):
if not req.input_value.strip():
raise HTTPException(status_code=400, detail="input_value is required")
# ── Free Tier Enforcement ─────────────────────────────────────────────────
plan = config.BATCHTUBE_PLAN
limits = config.PLAN_LIMITS.get(plan, config.PLAN_LIMITS["free"])
# Cap resolution for free tier
resolution = req.resolution
max_res = limits.get("max_resolution")
if max_res and max_res != "best":
res_order = config.AVAILABLE_RESOLUTIONS # ["best", "4320", ..., "144"]
# Lower index = higher quality; cap if requested res is higher than allowed
def res_index(r):
try:
return res_order.index(r)
except ValueError:
return len(res_order)
if res_index(resolution) < res_index(max_res):
logger.info(f"Free tier: capping resolution from {resolution} to {max_res}")
resolution = max_res
else:
resolution = req.resolution
# For CSV input: enforce max_videos limit upfront
max_videos = limits.get("max_videos")
if max_videos is not None and req.input_type == "csv":
csv_lines = [ln.strip() for ln in req.input_value.strip().splitlines() if ln.strip()]
if len(csv_lines) > max_videos:
raise HTTPException(
status_code=403,
detail=f"Free tier is limited to {max_videos} videos per batch. "
f"Upgrade to Creator or Pro for larger batches. "
f"({len(csv_lines)} URLs submitted)"
)
# Create the folder if it doesn't exist
try:
Path(output_folder).mkdir(parents=True, exist_ok=True)
@@ -128,7 +162,7 @@ async def create_job(req: CreateJobRequest):
job_id = db.create_job(
output_folder=output_folder,
selections=req.selections,
resolution=req.resolution,
resolution=resolution, # may be capped by free tier enforcement
delay_min=req.delay_min,
delay_max=req.delay_max,
input_type=req.input_type,

View File

@@ -1,5 +1,5 @@
"""
config.py - Default configuration for YouTube SIPOC Tool
config.py - Default configuration for BatchTube
"""
import os
@@ -32,7 +32,40 @@ SUMMARY_MAX_TOKENS = 1024
MAX_TITLE_LENGTH = 80 # characters - truncate long titles in filenames
# ── Database ──────────────────────────────────────────────────────────────────
DB_PATH = os.environ.get("BATCHTUBE_DB_PATH", os.path.join(os.path.dirname(__file__), "youtube_sipoc.db"))
# Windows-safe: allow override via env var (avoids path issues in PyInstaller bundles)
DB_PATH = os.environ.get(
"BATCHTUBE_DB_PATH",
os.path.join(os.path.dirname(__file__), "youtube_sipoc.db")
)
# ── Artifact Types ────────────────────────────────────────────────────────────
ARTIFACT_TYPES = ["title", "stats", "transcript", "summary", "thumbnail", "mp3", "mp4"]
# ── Licensing / Plan Tiers ────────────────────────────────────────────────────
# Plan is set via environment variable BATCHTUBE_PLAN.
# Valid values: free | creator | pro | studio | lifetime
# Defaults to 'free' — license validation layer overrides this at runtime.
BATCHTUBE_PLAN = os.environ.get("BATCHTUBE_PLAN", "free").lower()
# ── Free Tier Limits (updated 2026-03-07) ─────────────────────────────────────
# Free tier: 3 videos/batch max (tightened from 5), 720p max resolution
FREE_TIER_MAX_VIDEOS = 3 # max videos per batch job on free plan
FREE_TIER_MAX_RESOLUTION = "720" # max resolution on free plan (was 1080p)
# ── Plan Limits Map ───────────────────────────────────────────────────────────
# Maps plan name -> {max_videos, max_resolution}; None = unlimited
PLAN_LIMITS = {
"free": {"max_videos": FREE_TIER_MAX_VIDEOS, "max_resolution": FREE_TIER_MAX_RESOLUTION},
"creator": {"max_videos": 100, "max_resolution": "1080"},
"pro": {"max_videos": None, "max_resolution": "best"},
"studio": {"max_videos": None, "max_resolution": "best"},
"lifetime": {"max_videos": None, "max_resolution": "best"},
}
# ── Pricing Reference (2026-03-07 approved) ───────────────────────────────────
# Free: 3 videos/batch, 720p max
# Creator: $19/mo or $149/yr — 100 videos/batch, 1080p
# Pro: $39/mo or $299/yr — unlimited, 4K
# Studio: $79/mo — 3 seats, API access
# Lifetime: $197 one-time — unlimited, all features
# AppSumo: $79 one-time — limited launch deal

View File

@@ -171,6 +171,27 @@ def run_job(job_id: str):
unique_videos.append(v)
videos = unique_videos
# ── Free Tier: Enforce max videos per batch ───────────────────────────
plan = config.BATCHTUBE_PLAN
limits = config.PLAN_LIMITS.get(plan, config.PLAN_LIMITS["free"])
max_videos = limits.get("max_videos")
if max_videos is not None and len(videos) > max_videos:
db.add_log(
job_id,
f"Free tier limit: truncating batch from {len(videos)} to {max_videos} videos. "
f"Upgrade to Creator or Pro for larger batches.",
level="WARN"
)
_emit(job_id, "tier_limit", {
"job_id": job_id,
"plan": plan,
"requested": len(videos),
"allowed": max_videos,
"message": f"Free tier: batch limited to {max_videos} videos. Upgrade for more."
})
videos = videos[:max_videos]
# ── Step 2: Register videos in DB ────────────────────────────────────
for i, v in enumerate(videos):
db.upsert_video(job_id, v["video_id"], v["url"], v.get("title", ""), i)