Showing
5 changed files
with
93 additions
and
31 deletions
| @@ -11,3 +11,7 @@ PUBLIC_ZIMAGE_PORT="39009" | @@ -11,3 +11,7 @@ PUBLIC_ZIMAGE_PORT="39009" | ||
| 11 | # Local Ports (Internal Bind) | 11 | # Local Ports (Internal Bind) |
| 12 | LOCAL_BACKEND_PORT="7000" | 12 | LOCAL_BACKEND_PORT="7000" |
| 13 | LOCAL_FRONTEND_PORT="7001" | 13 | LOCAL_FRONTEND_PORT="7001" |
| 14 | + | ||
| 15 | +# Business Logic Configuration | ||
| 16 | +VIDEO_GENERATION_LIMIT="1" | ||
| 17 | +LIKES_FOR_REWARD="5" |
| @@ -4,9 +4,10 @@ import os | @@ -4,9 +4,10 @@ import os | ||
| 4 | import secrets | 4 | import secrets |
| 5 | import time | 5 | import time |
| 6 | import fcntl | 6 | import fcntl |
| 7 | +import re | ||
| 7 | from pathlib import Path | 8 | from pathlib import Path |
| 8 | from threading import Lock, RLock | 9 | from threading import Lock, RLock |
| 9 | -from typing import List, Literal, Optional | 10 | +from typing import List, Literal, Optional, Dict, Any |
| 10 | 11 | ||
| 11 | import httpx | 12 | import httpx |
| 12 | from fastapi import FastAPI, HTTPException, Query | 13 | from fastapi import FastAPI, HTTPException, Query |
| @@ -25,6 +26,34 @@ GALLERY_MAX_ITEMS = int(os.getenv("GALLERY_MAX_ITEMS", "500")) | @@ -25,6 +26,34 @@ GALLERY_MAX_ITEMS = int(os.getenv("GALLERY_MAX_ITEMS", "500")) | ||
| 25 | WHITELIST_PATH = Path(os.getenv("WHITELIST_PATH", Path(__file__).with_name("whitelist.txt"))) | 26 | WHITELIST_PATH = Path(os.getenv("WHITELIST_PATH", Path(__file__).with_name("whitelist.txt"))) |
| 26 | ADMIN_ID = "86427531" | 27 | ADMIN_ID = "86427531" |
| 27 | 28 | ||
| 29 | +# Load dynamic limits from config.js | ||
| 30 | +CONFIG_JS_PATH = Path(__file__).parent.parent / "public" / "config.js" | ||
| 31 | + | ||
| 32 | +def load_limits_from_config() -> dict: | ||
| 33 | + defaults = {"VIDEO_GENERATION_LIMIT": 1, "LIKES_FOR_REWARD": 5} | ||
| 34 | + try: | ||
| 35 | + if not CONFIG_JS_PATH.exists(): | ||
| 36 | + return defaults | ||
| 37 | + | ||
| 38 | + content = CONFIG_JS_PATH.read_text(encoding="utf-8") | ||
| 39 | + | ||
| 40 | + # Simple regex to extract values from JS object | ||
| 41 | + # Looking for: VIDEO_GENERATION_LIMIT: 1 | ||
| 42 | + limit_match = re.search(r'VIDEO_GENERATION_LIMIT\s*:\s*(\d+)', content) | ||
| 43 | + reward_match = re.search(r'LIKES_FOR_REWARD\s*:\s*(\d+)', content) | ||
| 44 | + | ||
| 45 | + if limit_match: | ||
| 46 | + defaults["VIDEO_GENERATION_LIMIT"] = int(limit_match.group(1)) | ||
| 47 | + if reward_match: | ||
| 48 | + defaults["LIKES_FOR_REWARD"] = int(reward_match.group(1)) | ||
| 49 | + | ||
| 50 | + return defaults | ||
| 51 | + except Exception as e: | ||
| 52 | + logger.error(f"Failed to load config.js: {e}") | ||
| 53 | + return defaults | ||
| 54 | + | ||
| 55 | +LIMITS = load_limits_from_config() | ||
| 56 | + | ||
| 28 | # --- Usage Store --- | 57 | # --- Usage Store --- |
| 29 | class UsageStore: | 58 | class UsageStore: |
| 30 | def __init__(self, path: Path): | 59 | def __init__(self, path: Path): |
| @@ -191,14 +220,41 @@ class JsonStore: | @@ -191,14 +220,41 @@ class JsonStore: | ||
| 191 | if not target_item: return None | 220 | if not target_item: return None |
| 192 | liked_by = target_item.get("likedBy", []) | 221 | liked_by = target_item.get("likedBy", []) |
| 193 | if not isinstance(liked_by, list): liked_by = [] | 222 | if not isinstance(liked_by, list): liked_by = [] |
| 223 | + | ||
| 224 | + # --- New Reward Logic --- | ||
| 225 | + # 1. Check current likes BEFORE change | ||
| 226 | + current_likes_count = target_item.get("likes", 0) | ||
| 227 | + author_id = target_item.get("authorId") | ||
| 228 | + | ||
| 229 | + is_liked_after = False | ||
| 230 | + | ||
| 194 | if user_id in liked_by: | 231 | if user_id in liked_by: |
| 232 | + # UNLIKE | ||
| 195 | liked_by.remove(user_id) | 233 | liked_by.remove(user_id) |
| 196 | - target_item["likes"] = max(0, target_item.get("likes", 0) - 1) | 234 | + new_likes_count = max(0, current_likes_count - 1) |
| 235 | + is_liked_after = False | ||
| 197 | else: | 236 | else: |
| 237 | + # LIKE | ||
| 198 | liked_by.append(user_id) | 238 | liked_by.append(user_id) |
| 199 | - target_item["likes"] = target_item.get("likes", 0) + 1 | 239 | + new_likes_count = current_likes_count + 1 |
| 240 | + is_liked_after = True | ||
| 241 | + | ||
| 242 | + target_item["likes"] = new_likes_count | ||
| 200 | target_item["likedBy"] = liked_by | 243 | target_item["likedBy"] = liked_by |
| 201 | self._write(data) | 244 | self._write(data) |
| 245 | + | ||
| 246 | + # Reward Check: Only reward author when crossing threshold (e.g. 5, 10, 15...) | ||
| 247 | + # We check if the NEW count is a multiple of LIKES_FOR_REWARD and we just increased it. | ||
| 248 | + # (Simple version: Every N likes = 1 generation credit) | ||
| 249 | + if author_id and author_id != "OFFICIAL" and author_id != ADMIN_ID: | ||
| 250 | + limit = LIMITS["LIKES_FOR_REWARD"] | ||
| 251 | + # Only reward on LIKE action, not unlike | ||
| 252 | + if is_liked_after: | ||
| 253 | + # Check if we just hit a multiple of the limit (5, 10, 15...) | ||
| 254 | + if new_likes_count > 0 and new_likes_count % limit == 0: | ||
| 255 | + logger.info(f"User {author_id} reached {new_likes_count} likes! Adding bonus.") | ||
| 256 | + usage_store.update_bonus(author_id, 1) | ||
| 257 | + | ||
| 202 | return target_item | 258 | return target_item |
| 203 | 259 | ||
| 204 | def delete_item(self, item_id: str) -> bool: | 260 | def delete_item(self, item_id: str) -> bool: |
| @@ -230,7 +286,13 @@ app.add_middleware( | @@ -230,7 +286,13 @@ app.add_middleware( | ||
| 230 | ) | 286 | ) |
| 231 | @app.on_event("startup") | 287 | @app.on_event("startup") |
| 232 | 288 | ||
| 233 | -async def startup(): app.state.http = httpx.AsyncClient(timeout=httpx.Timeout(REQUEST_TIMEOUT_SECONDS, connect=5.0)) | 289 | +async def startup(): |
| 290 | + # Reload limits on startup to ensure fresh config | ||
| 291 | + global LIMITS | ||
| 292 | + LIMITS = load_limits_from_config() | ||
| 293 | + logger.info(f"Loaded limits: {LIMITS}") | ||
| 294 | + app.state.http = httpx.AsyncClient(timeout=httpx.Timeout(REQUEST_TIMEOUT_SECONDS, connect=5.0)) | ||
| 295 | + | ||
| 234 | @app.on_event("shutdown") | 296 | @app.on_event("shutdown") |
| 235 | async def shutdown(): await app.state.http.aclose() | 297 | async def shutdown(): await app.state.http.aclose() |
| 236 | @app.get("/health") | 298 | @app.get("/health") |
| @@ -244,29 +306,14 @@ async def login(user_id: str = Query(..., alias="userId")): | @@ -244,29 +306,14 @@ async def login(user_id: str = Query(..., alias="userId")): | ||
| 244 | 306 | ||
| 245 | @app.post("/likes/{item_id}") | 307 | @app.post("/likes/{item_id}") |
| 246 | async def toggle_like(item_id: str, user_id: str = Query(..., alias="userId")): | 308 | async def toggle_like(item_id: str, user_id: str = Query(..., alias="userId")): |
| 247 | - is_liked_before = False | ||
| 248 | - items = image_store.list_items() | ||
| 249 | - target_item = next((i for i in items if i.get("id") == item_id), None) | ||
| 250 | - if target_item: | ||
| 251 | - is_liked_before = user_id in target_item.get("likedBy", []) | ||
| 252 | - | 309 | + # Try images first |
| 253 | updated_item = image_store.toggle_like(item_id, user_id) | 310 | updated_item = image_store.toggle_like(item_id, user_id) |
| 254 | - if updated_item: | ||
| 255 | - is_liked_after = user_id in updated_item.get("likedBy", []) | ||
| 256 | - if is_liked_after and not is_liked_before: | ||
| 257 | - usage_store.update_bonus(user_id, 1) | ||
| 258 | - elif not is_liked_after and is_liked_before: | ||
| 259 | - usage_store.update_bonus(user_id, -1) | ||
| 260 | - return updated_item | 311 | + if updated_item: return updated_item |
| 261 | 312 | ||
| 313 | + # Then videos | ||
| 262 | updated_item = video_store.toggle_like(item_id, user_id) | 314 | updated_item = video_store.toggle_like(item_id, user_id) |
| 263 | - if updated_item: | ||
| 264 | - is_liked_after = user_id in updated_item.get("likedBy", []) | ||
| 265 | - if is_liked_after and not is_liked_before: | ||
| 266 | - usage_store.update_bonus(user_id, 1) | ||
| 267 | - elif not is_liked_after and is_liked_before: | ||
| 268 | - usage_store.update_bonus(user_id, -1) | ||
| 269 | - return updated_item | 315 | + if updated_item: return updated_item |
| 316 | + | ||
| 270 | raise HTTPException(status_code=404, detail="Item not found") | 317 | raise HTTPException(status_code=404, detail="Item not found") |
| 271 | 318 | ||
| 272 | @app.get("/usage/{user_id}") | 319 | @app.get("/usage/{user_id}") |
| @@ -274,11 +321,17 @@ async def get_user_usage(user_id: str): | @@ -274,11 +321,17 @@ async def get_user_usage(user_id: str): | ||
| 274 | try: | 321 | try: |
| 275 | usage = usage_store.get_usage(user_id) | 322 | usage = usage_store.get_usage(user_id) |
| 276 | is_admin = user_id == ADMIN_ID | 323 | is_admin = user_id == ADMIN_ID |
| 277 | - remaining = (2 - usage["daily_used"]) + usage["bonus_count"] if not is_admin else 999999 | 324 | + |
| 325 | + limit = LIMITS["VIDEO_GENERATION_LIMIT"] | ||
| 326 | + | ||
| 327 | + # Logic: base_limit - daily_used + bonus | ||
| 328 | + remaining = (limit - usage["daily_used"]) + usage["bonus_count"] | ||
| 329 | + if is_admin: remaining = 999999 | ||
| 330 | + | ||
| 278 | return { | 331 | return { |
| 279 | "daily_used": usage["daily_used"], | 332 | "daily_used": usage["daily_used"], |
| 280 | "bonus_count": usage["bonus_count"], | 333 | "bonus_count": usage["bonus_count"], |
| 281 | - "base_limit": 2, | 334 | + "base_limit": limit, |
| 282 | "remaining": max(0, remaining), | 335 | "remaining": max(0, remaining), |
| 283 | "is_admin": is_admin | 336 | "is_admin": is_admin |
| 284 | } | 337 | } |
| @@ -287,8 +340,8 @@ async def get_user_usage(user_id: str): | @@ -287,8 +340,8 @@ async def get_user_usage(user_id: str): | ||
| 287 | return { | 340 | return { |
| 288 | "daily_used": 0, | 341 | "daily_used": 0, |
| 289 | "bonus_count": 0, | 342 | "bonus_count": 0, |
| 290 | - "base_limit": 2, | ||
| 291 | - "remaining": 2, | 343 | + "base_limit": LIMITS["VIDEO_GENERATION_LIMIT"], |
| 344 | + "remaining": LIMITS["VIDEO_GENERATION_LIMIT"], | ||
| 292 | "is_admin": user_id == ADMIN_ID | 345 | "is_admin": user_id == ADMIN_ID |
| 293 | } | 346 | } |
| 294 | 347 |
| @@ -2,5 +2,7 @@ window.APP_CONFIG = { | @@ -2,5 +2,7 @@ window.APP_CONFIG = { | ||
| 2 | Z_IMAGE_DIRECT_BASE_URL: "http://106.120.52.146:39009", | 2 | Z_IMAGE_DIRECT_BASE_URL: "http://106.120.52.146:39009", |
| 3 | TURBO_DIFFUSION_API_URL: "http://106.120.52.146:37002", | 3 | TURBO_DIFFUSION_API_URL: "http://106.120.52.146:37002", |
| 4 | VIDEO_OSS_BASE_URL: "http://106.120.52.146:34000", | 4 | VIDEO_OSS_BASE_URL: "http://106.120.52.146:34000", |
| 5 | - API_BASE_URL: "http://106.120.52.146:37000" | 5 | + API_BASE_URL: "http://106.120.52.146:37000", |
| 6 | + VIDEO_GENERATION_LIMIT: 1, | ||
| 7 | + LIKES_FOR_REWARD: 5 | ||
| 6 | }; | 8 | }; |
| @@ -12,7 +12,7 @@ fi | @@ -12,7 +12,7 @@ fi | ||
| 12 | FRONTEND_DIR="$BASE_DIR/z-image-generator" | 12 | FRONTEND_DIR="$BASE_DIR/z-image-generator" |
| 13 | BACKEND_DIR="$BASE_DIR" | 13 | BACKEND_DIR="$BASE_DIR" |
| 14 | CONSTANTS_FILE="$FRONTEND_DIR/constants.ts" | 14 | CONSTANTS_FILE="$FRONTEND_DIR/constants.ts" |
| 15 | -CONFIG_JS_FILE="$FRONTEND_DIR/public/config.js" | 15 | +CONFIG_JS_FILE="$BASE_DIR/public/config.js" |
| 16 | LOGS_DIR="$BASE_DIR/logs" | 16 | LOGS_DIR="$BASE_DIR/logs" |
| 17 | 17 | ||
| 18 | # Ensure logs directory exists | 18 | # Ensure logs directory exists |
| @@ -45,7 +45,9 @@ window.APP_CONFIG = { | @@ -45,7 +45,9 @@ window.APP_CONFIG = { | ||
| 45 | Z_IMAGE_DIRECT_BASE_URL: "http://$PUBLIC_IP:$PUBLIC_ZIMAGE_PORT", | 45 | Z_IMAGE_DIRECT_BASE_URL: "http://$PUBLIC_IP:$PUBLIC_ZIMAGE_PORT", |
| 46 | TURBO_DIFFUSION_API_URL: "http://$PUBLIC_IP:$PUBLIC_TURBO_PORT", | 46 | TURBO_DIFFUSION_API_URL: "http://$PUBLIC_IP:$PUBLIC_TURBO_PORT", |
| 47 | VIDEO_OSS_BASE_URL: "http://$PUBLIC_IP:$PUBLIC_OSS_PORT", | 47 | VIDEO_OSS_BASE_URL: "http://$PUBLIC_IP:$PUBLIC_OSS_PORT", |
| 48 | - API_BASE_URL: "http://$PUBLIC_IP:$PUBLIC_BACKEND_PORT" | 48 | + API_BASE_URL: "http://$PUBLIC_IP:$PUBLIC_BACKEND_PORT", |
| 49 | + VIDEO_GENERATION_LIMIT: ${VIDEO_GENERATION_LIMIT:-1}, | ||
| 50 | + LIKES_FOR_REWARD: ${LIKES_FOR_REWARD:-5} | ||
| 49 | }; | 51 | }; |
| 50 | EOF | 52 | EOF |
| 51 | 53 |
| @@ -9,6 +9,7 @@ export default defineConfig(({ mode }) => { | @@ -9,6 +9,7 @@ export default defineConfig(({ mode }) => { | ||
| 9 | port: 3000, | 9 | port: 3000, |
| 10 | host: '0.0.0.0', | 10 | host: '0.0.0.0', |
| 11 | }, | 11 | }, |
| 12 | + publicDir: '../public', | ||
| 12 | plugins: [react()], | 13 | plugins: [react()], |
| 13 | define: { | 14 | define: { |
| 14 | 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), | 15 | 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), |
-
Please register or login to post a comment