| | |
| | """ |
| | 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 |
| |
|
| | |
| | |
| | |
| |
|
| | 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 = { |
| | 'song_analysis': 1, |
| | 'stem_only': 1, |
| | 'chord_only': 1, |
| | 'ai_generate': 2, |
| | 'bulk_song': 1, |
| | } |
| |
|
| | |
| | DEMO_TOKENS = 3 |
| | DEMO_MAX_DURATION = 300 |
| | LICENSED_MONTHLY_TOKENS = 300 |
| | LICENSED_MAX_DURATION = None |
| |
|
| | |
| | |
| | |
| |
|
| | VALID_LICENSES = { |
| | |
| | "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}, |
| |
|
| | |
| | |
| | "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}, |
| |
|
| | |
| | "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}, |
| |
|
| | |
| | "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}, |
| | } |
| |
|
| | |
| | |
| | |
| |
|
| | 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') |
| |
|
| | |
| | |
| | |
| |
|
| | 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 |
| |
|
| | |
| | |
| | |
| |
|
| | 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': {}, |
| | '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: |
| | |
| | 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) |
| |
|
| | |
| | self.reload_grants() |
| | bonus_from_grants = self.token_grants.get(email, 0) |
| |
|
| | |
| | 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']: |
| | |
| | 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: |
| | |
| | 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']}" |
| |
|
| | |
| | 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" |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | user_manager = UserManager() |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | 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) |
| |
|
| | |
| | if status['tokens_remaining'] <= 0 and not status['unlimited']: |
| | return False, "No tokens remaining. Please upgrade or wait for monthly reset.", status |
| |
|
| | |
| | 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" |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | 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] |
| | |
| | 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}") |
| |
|