File size: 16,686 Bytes
a3fa32f 8a9eef3 a3fa32f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 | #!/usr/bin/env python3
"""
VYNL Token & User System
- Demo mode: 3 free tokens, 5-min track limit
- Licensed mode: 300 tokens/month, full access
- Admin token distribution via simple text file
"""
import json
import hashlib
import os
from pathlib import Path
from datetime import datetime, timedelta
from typing import Optional, Tuple, Dict
# ============================================================================
# CONFIGURATION
# ============================================================================
DATA_DIR = Path(os.environ.get('VYNL_DATA_DIR', Path.home() / '.vynl_data'))
DATA_DIR.mkdir(parents=True, exist_ok=True)
USERS_FILE = DATA_DIR / 'users.json'
TOKENS_FILE = DATA_DIR / 'token_grants.txt'
SESSIONS_FILE = DATA_DIR / 'sessions.json'
# Token costs
TOKEN_COSTS = {
'song_analysis': 1, # Full stem + chord + DAW
'stem_only': 1, # Just stems
'chord_only': 1, # Just chords
'ai_generate': 2, # GROOVES generation
'bulk_song': 1, # Per song in bulk
}
# Limits
DEMO_TOKENS = 3
DEMO_MAX_DURATION = 300 # 5 minutes in seconds
LICENSED_MONTHLY_TOKENS = 300
LICENSED_MAX_DURATION = None # Unlimited
# ============================================================================
# VALID LICENSES (from license_system)
# ============================================================================
VALID_LICENSES = {
# Creator licenses - unlimited
"VYNL-IY2M-KV47-AT7J-C74V": {"name": "R.T. Lackey", "email": "rlackey.seattle@gmail.com", "type": "CREATOR", "unlimited": True},
"VYNL-INZW-JNZY-Y4O2-WOEB": {"name": "R.T. Lackey", "email": "rlackey.seattle@gmail.com", "type": "CREATOR", "unlimited": True},
# Universal Licenses - 300 tokens/month, no duration limit
# These can be distributed to users
"VYNL-UNIV-2026-ALPHA-001A": {"name": "Universal License", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-UNIV-2026-ALPHA-002B": {"name": "Universal License", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-UNIV-2026-ALPHA-003C": {"name": "Universal License", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-UNIV-2026-ALPHA-004D": {"name": "Universal License", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-UNIV-2026-ALPHA-005E": {"name": "Universal License", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-UNIV-2026-BETA-001F": {"name": "Universal License", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-UNIV-2026-BETA-002G": {"name": "Universal License", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-UNIV-2026-BETA-003H": {"name": "Universal License", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-UNIV-2026-BETA-004J": {"name": "Universal License", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-UNIV-2026-BETA-005K": {"name": "Universal License", "type": "PROFESSIONAL", "tokens": 300},
# Premium Licenses - 1000 tokens/month
"VYNL-PREM-2026-GOLD-001A": {"name": "Premium License", "type": "PREMIUM", "tokens": 1000},
"VYNL-PREM-2026-GOLD-002B": {"name": "Premium License", "type": "PREMIUM", "tokens": 1000},
"VYNL-PREM-2026-GOLD-003C": {"name": "Premium License", "type": "PREMIUM", "tokens": 1000},
"VYNL-PREM-2026-GOLD-004D": {"name": "Premium License", "type": "PREMIUM", "tokens": 1000},
"VYNL-PREM-2026-GOLD-005E": {"name": "Premium License", "type": "PREMIUM", "tokens": 1000},
# Demo licenses (original 15)
"VYNL-WAFV-HBGQ-UMAY-UKRD": {"name": "Demo User 01", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-5M73-VSUB-CP5L-PABM": {"name": "Demo User 02", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-VURV-P5NN-N2IK-EV44": {"name": "Demo User 03", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-7TH6-NWHM-LNC2-KMG7": {"name": "Demo User 04", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-4W2G-NYRK-LDW7-554E": {"name": "Demo User 05", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-GGAD-AMOO-TLVQ-5O6M": {"name": "Demo User 06", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-PJM4-PRRG-AID3-VFEA": {"name": "Demo User 07", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-G45E-OBGJ-7LB6-3BKZ": {"name": "Demo User 08", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-WT7Y-ICDE-WN43-SU4B": {"name": "Demo User 09", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-J3DM-Y2KY-GLTN-PNM4": {"name": "Demo User 10", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-3FVE-RTMT-LAOJ-NH3P": {"name": "Demo User 11", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-YOS6-LESJ-WGIB-AOVM": {"name": "Demo User 12", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-ST6S-4GUY-WXVL-JWM6": {"name": "Demo User 13", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-RFRG-YUXL-7AX4-7FPY": {"name": "Demo User 14", "type": "PROFESSIONAL", "tokens": 300},
"VYNL-54HA-343P-V5AT-6RJL": {"name": "Demo User 15", "type": "PROFESSIONAL", "tokens": 300},
}
# ============================================================================
# USER DATABASE
# ============================================================================
def load_users() -> Dict:
"""Load user database"""
if USERS_FILE.exists():
return json.loads(USERS_FILE.read_text())
return {}
def save_users(users: Dict):
"""Save user database"""
USERS_FILE.write_text(json.dumps(users, indent=2))
def hash_password(password: str) -> str:
"""Hash password for storage"""
return hashlib.sha256(password.encode()).hexdigest()
def get_month_key() -> str:
"""Get current month key for token tracking"""
return datetime.now().strftime('%Y-%m')
# ============================================================================
# TOKEN GRANTS FILE
# ============================================================================
def load_token_grants() -> Dict[str, int]:
"""
Load token grants from simple text file
Format: email,tokens (one per line)
Example:
john@example.com,100
jane@example.com,50
"""
grants = {}
if TOKENS_FILE.exists():
for line in TOKENS_FILE.read_text().strip().split('\n'):
line = line.strip()
if line and ',' in line and not line.startswith('#'):
parts = line.split(',')
if len(parts) >= 2:
email = parts[0].strip().lower()
try:
tokens = int(parts[1].strip())
grants[email] = grants.get(email, 0) + tokens
except ValueError:
pass
return grants
# ============================================================================
# USER MANAGEMENT
# ============================================================================
class UserManager:
def __init__(self):
self.users = load_users()
self.token_grants = load_token_grants()
def reload_grants(self):
"""Reload token grants from file"""
self.token_grants = load_token_grants()
def create_account(self, email: str, password: str, name: str = "") -> Tuple[bool, str]:
"""Create new user account"""
email = email.strip().lower()
if not email or '@' not in email:
return False, "Invalid email address"
if not password or len(password) < 6:
return False, "Password must be at least 6 characters"
if email in self.users:
return False, "Account already exists"
self.users[email] = {
'email': email,
'name': name or email.split('@')[0],
'password_hash': hash_password(password),
'created': datetime.now().isoformat(),
'license_key': None,
'license_type': 'DEMO',
'tokens_used': {}, # {month_key: count}
'bonus_tokens': 0,
'total_songs_processed': 0,
}
save_users(self.users)
return True, "Account created successfully"
def login(self, email: str, password: str) -> Tuple[bool, Optional[Dict]]:
"""Login user"""
email = email.strip().lower()
if email not in self.users:
return False, None
user = self.users[email]
if user['password_hash'] != hash_password(password):
return False, None
return True, user
def activate_license(self, email: str, license_key: str) -> Tuple[bool, str]:
"""Activate license for user"""
email = email.strip().lower()
license_key = license_key.strip().upper()
if email not in self.users:
return False, "User not found"
if license_key not in VALID_LICENSES:
return False, "Invalid license key"
license_info = VALID_LICENSES[license_key]
self.users[email]['license_key'] = license_key
self.users[email]['license_type'] = license_info['type']
self.users[email]['license_activated'] = datetime.now().isoformat()
save_users(self.users)
return True, f"License activated: {license_info['type']}"
def get_user_status(self, email: str) -> Dict:
"""Get complete user status including tokens"""
email = email.strip().lower()
if email not in self.users:
# Demo user (not registered)
return {
'registered': False,
'license_type': 'DEMO',
'tokens_remaining': DEMO_TOKENS,
'tokens_used': 0,
'max_duration': DEMO_MAX_DURATION,
'unlimited': False,
}
user = self.users[email]
month_key = get_month_key()
tokens_used_this_month = user['tokens_used'].get(month_key, 0)
# Check for bonus tokens from grants file
self.reload_grants()
bonus_from_grants = self.token_grants.get(email, 0)
# Calculate based on license type
if user['license_type'] == 'CREATOR':
return {
'registered': True,
'email': email,
'name': user['name'],
'license_type': 'CREATOR',
'tokens_remaining': 999999,
'tokens_used': tokens_used_this_month,
'max_duration': None,
'unlimited': True,
}
elif user['license_key']:
# Licensed user
base_tokens = LICENSED_MONTHLY_TOKENS
total_available = base_tokens + user.get('bonus_tokens', 0) + bonus_from_grants
tokens_remaining = max(0, total_available - tokens_used_this_month)
return {
'registered': True,
'email': email,
'name': user['name'],
'license_type': user['license_type'],
'tokens_remaining': tokens_remaining,
'tokens_used': tokens_used_this_month,
'monthly_limit': base_tokens,
'bonus_tokens': user.get('bonus_tokens', 0) + bonus_from_grants,
'max_duration': LICENSED_MAX_DURATION,
'unlimited': False,
}
else:
# Registered but no license (demo)
return {
'registered': True,
'email': email,
'name': user['name'],
'license_type': 'DEMO',
'tokens_remaining': max(0, DEMO_TOKENS - tokens_used_this_month),
'tokens_used': tokens_used_this_month,
'max_duration': DEMO_MAX_DURATION,
'unlimited': False,
}
def use_tokens(self, email: str, amount: int, action: str = 'song_analysis') -> Tuple[bool, str]:
"""Deduct tokens for an action"""
email = email.strip().lower()
status = self.get_user_status(email)
if status['unlimited']:
return True, "Unlimited access"
if status['tokens_remaining'] < amount:
return False, f"Insufficient tokens. Need {amount}, have {status['tokens_remaining']}"
# Deduct tokens
if email in self.users:
month_key = get_month_key()
if month_key not in self.users[email]['tokens_used']:
self.users[email]['tokens_used'][month_key] = 0
self.users[email]['tokens_used'][month_key] += amount
self.users[email]['total_songs_processed'] += 1
save_users(self.users)
remaining = status['tokens_remaining'] - amount
return True, f"Token used. {remaining} remaining"
def check_duration_limit(self, email: str, duration_seconds: float) -> Tuple[bool, str]:
"""Check if track duration is within limits"""
status = self.get_user_status(email)
if status['max_duration'] is None:
return True, "No duration limit"
if duration_seconds > status['max_duration']:
max_mins = status['max_duration'] // 60
return False, f"Track exceeds {max_mins}-minute limit for demo mode. Upgrade to process longer tracks."
return True, "Duration OK"
def add_bonus_tokens(self, email: str, amount: int) -> Tuple[bool, str]:
"""Add bonus tokens to user account"""
email = email.strip().lower()
if email not in self.users:
return False, "User not found"
self.users[email]['bonus_tokens'] = self.users[email].get('bonus_tokens', 0) + amount
save_users(self.users)
return True, f"Added {amount} bonus tokens"
# ============================================================================
# SINGLETON INSTANCE
# ============================================================================
user_manager = UserManager()
# ============================================================================
# HELPER FUNCTIONS FOR GRADIO
# ============================================================================
def check_can_process(email: str, duration_seconds: float = 0) -> Tuple[bool, str, Dict]:
"""
Check if user can process a song
Returns: (can_process, message, status_dict)
"""
status = user_manager.get_user_status(email)
# Check tokens
if status['tokens_remaining'] <= 0 and not status['unlimited']:
return False, "No tokens remaining. Please upgrade or wait for monthly reset.", status
# Check duration
if duration_seconds > 0:
ok, msg = user_manager.check_duration_limit(email, duration_seconds)
if not ok:
return False, msg, status
return True, "Ready to process", status
def deduct_token(email: str) -> Tuple[bool, str]:
"""Deduct one token after successful processing"""
return user_manager.use_tokens(email, 1)
def get_status_display(email: str) -> str:
"""Get formatted status for UI display"""
if not email:
return "DEMO MODE: 3 free tokens | 5-min track limit | Enter email to track usage"
status = user_manager.get_user_status(email)
if status['unlimited']:
return f"CREATOR: {status['name']} | UNLIMITED ACCESS"
if status['license_type'] != 'DEMO':
return f"LICENSED ({status['license_type']}): {status['tokens_remaining']} tokens remaining this month"
return f"DEMO: {status['tokens_remaining']}/{DEMO_TOKENS} tokens | 5-min limit | Upgrade for full access"
# ============================================================================
# CLI FOR TESTING
# ============================================================================
if __name__ == "__main__":
import sys
print("VYNL Token System")
print("=" * 50)
if len(sys.argv) < 2:
print("""
Commands:
status <email> Check user status
create <email> <pass> Create account
grant <email> <tokens> Add tokens to grants file
activate <email> <key> Activate license
""")
sys.exit(0)
cmd = sys.argv[1]
if cmd == "status" and len(sys.argv) >= 3:
email = sys.argv[2]
status = user_manager.get_user_status(email)
print(json.dumps(status, indent=2))
elif cmd == "create" and len(sys.argv) >= 4:
email, password = sys.argv[2], sys.argv[3]
ok, msg = user_manager.create_account(email, password)
print(f"{'Success' if ok else 'Failed'}: {msg}")
elif cmd == "grant" and len(sys.argv) >= 4:
email, tokens = sys.argv[2], sys.argv[3]
# Append to grants file
with open(TOKENS_FILE, 'a') as f:
f.write(f"{email},{tokens}\n")
print(f"Added {tokens} tokens for {email}")
elif cmd == "activate" and len(sys.argv) >= 4:
email, key = sys.argv[2], sys.argv[3]
ok, msg = user_manager.activate_license(email, key)
print(f"{'Success' if ok else 'Failed'}: {msg}")
|