Admin commited on
Commit
e42cbba
·
1 Parent(s): f9a6a84

feat: introduce core configuration management and MySQL database integration

Browse files
Files changed (4) hide show
  1. config/setting.toml +7 -0
  2. requirements.txt +3 -1
  3. src/core/config.py +27 -0
  4. src/core/database.py +253 -196
config/setting.toml CHANGED
@@ -3,6 +3,13 @@ api_key = "han1234"
3
  admin_username = "admin"
4
  admin_password = "admin"
5
 
 
 
 
 
 
 
 
6
  [sora]
7
  base_url = "https://sora.chatgpt.com/backend"
8
  timeout = 120
 
3
  admin_username = "admin"
4
  admin_password = "admin"
5
 
6
+ [database]
7
+ host = "xxxxxxx"
8
+ port = 3306
9
+ user = "xxxxxxx"
10
+ password = "xxxxxxx"
11
+ database = "xxxxxxx"
12
+
13
  [sora]
14
  base_url = "https://sora.chatgpt.com/backend"
15
  timeout = 120
requirements.txt CHANGED
@@ -12,4 +12,6 @@ tomli==2.2.1
12
  toml
13
  faker==24.0.0
14
  python-dateutil==2.8.2
15
- httpx==0.28.1
 
 
 
12
  toml
13
  faker==24.0.0
14
  python-dateutil==2.8.2
15
+ httpx==0.28.1
16
+ aiomysql
17
+ cryptography
src/core/config.py CHANGED
@@ -7,6 +7,8 @@ class Config:
7
  """Application configuration"""
8
 
9
  def __init__(self):
 
 
10
  self._config = self._load_config()
11
  self._admin_username: Optional[str] = None
12
  self._admin_password: Optional[str] = None
@@ -41,6 +43,31 @@ class Config:
41
  """Set admin username from database"""
42
  self._admin_username = username
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  @property
45
  def sora_base_url(self) -> str:
46
  return self._config["sora"]["base_url"]
 
7
  """Application configuration"""
8
 
9
  def __init__(self):
10
+ from dotenv import load_dotenv
11
+ load_dotenv()
12
  self._config = self._load_config()
13
  self._admin_username: Optional[str] = None
14
  self._admin_password: Optional[str] = None
 
43
  """Set admin username from database"""
44
  self._admin_username = username
45
 
46
+ @property
47
+ def db_host(self) -> str:
48
+ import os
49
+ return os.getenv("DB_HOST", self._config.get("database", {}).get("host", "localhost"))
50
+
51
+ @property
52
+ def db_port(self) -> int:
53
+ import os
54
+ return int(os.getenv("DB_PORT", self._config.get("database", {}).get("port", 3306)))
55
+
56
+ @property
57
+ def db_user(self) -> str:
58
+ import os
59
+ return os.getenv("DB_USER", self._config.get("database", {}).get("user", ""))
60
+
61
+ @property
62
+ def db_password(self) -> str:
63
+ import os
64
+ return os.getenv("DB_PASSWORD", self._config.get("database", {}).get("password", ""))
65
+
66
+ @property
67
+ def db_name(self) -> str:
68
+ import os
69
+ return os.getenv("DB_NAME", self._config.get("database", {}).get("database", ""))
70
+
71
  @property
72
  def sora_base_url(self) -> str:
73
  return self._config["sora"]["base_url"]
src/core/database.py CHANGED
@@ -1,55 +1,149 @@
1
  """Database storage layer"""
2
- import aiosqlite
3
  import json
4
  from datetime import datetime
5
- from typing import Optional, List
6
  from pathlib import Path
7
  from .models import Token, TokenStats, Task, RequestLog, AdminConfig, ProxyConfig, WatermarkFreeConfig, CacheConfig, GenerationConfig, TokenRefreshConfig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  class Database:
10
- """SQLite database manager"""
11
 
12
  def __init__(self, db_path: str = None):
13
- if db_path is None:
14
- # Store database in data directory
15
- data_dir = Path(__file__).parent.parent.parent / "data"
16
- data_dir.mkdir(exist_ok=True)
17
- db_path = str(data_dir / "hancat.db")
18
- self.db_path = db_path
19
-
20
- def db_exists(self) -> bool:
21
- """Check if database file exists"""
22
- return Path(self.db_path).exists()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  async def _table_exists(self, db, table_name: str) -> bool:
25
  """Check if a table exists in the database"""
 
26
  cursor = await db.execute(
27
- "SELECT name FROM sqlite_master WHERE type='table' AND name=?",
28
- (table_name,)
29
  )
30
  result = await cursor.fetchone()
31
- return result is not None
32
 
33
  async def _column_exists(self, db, table_name: str, column_name: str) -> bool:
34
  """Check if a column exists in a table"""
35
  try:
36
- cursor = await db.execute(f"PRAGMA table_info({table_name})")
37
- columns = await cursor.fetchall()
38
- return any(col[1] == column_name for col in columns)
 
 
 
39
  except:
40
  return False
41
 
42
  async def _ensure_config_rows(self, db, config_dict: dict = None):
43
- """Ensure all config tables have their default rows
44
-
45
- Args:
46
- db: Database connection
47
- config_dict: Configuration dictionary from setting.toml (optional)
48
- """
49
  # Ensure admin_config has a row
50
- cursor = await db.execute("SELECT COUNT(*) FROM admin_config")
51
  count = await cursor.fetchone()
52
- if count[0] == 0:
53
  # Get admin credentials from config_dict if provided, otherwise use defaults
54
  admin_username = "admin"
55
  admin_password = "admin"
@@ -69,9 +163,9 @@ class Database:
69
  """, (admin_username, admin_password, error_ban_threshold))
70
 
71
  # Ensure proxy_config has a row
72
- cursor = await db.execute("SELECT COUNT(*) FROM proxy_config")
73
  count = await cursor.fetchone()
74
- if count[0] == 0:
75
  # Get proxy config from config_dict if provided, otherwise use defaults
76
  proxy_enabled = False
77
  proxy_url = None
@@ -89,9 +183,9 @@ class Database:
89
  """, (proxy_enabled, proxy_url))
90
 
91
  # Ensure watermark_free_config has a row
92
- cursor = await db.execute("SELECT COUNT(*) FROM watermark_free_config")
93
  count = await cursor.fetchone()
94
- if count[0] == 0:
95
  # Get watermark-free config from config_dict if provided, otherwise use defaults
96
  watermark_free_enabled = False
97
  parse_method = "third_party"
@@ -115,9 +209,9 @@ class Database:
115
  """, (watermark_free_enabled, parse_method, custom_parse_url, custom_parse_token))
116
 
117
  # Ensure cache_config has a row
118
- cursor = await db.execute("SELECT COUNT(*) FROM cache_config")
119
  count = await cursor.fetchone()
120
- if count[0] == 0:
121
  # Get cache config from config_dict if provided, otherwise use defaults
122
  cache_enabled = False
123
  cache_timeout = 600
@@ -137,9 +231,9 @@ class Database:
137
  """, (cache_enabled, cache_timeout, cache_base_url))
138
 
139
  # Ensure generation_config has a row
140
- cursor = await db.execute("SELECT COUNT(*) FROM generation_config")
141
  count = await cursor.fetchone()
142
- if count[0] == 0:
143
  # Get generation config from config_dict if provided, otherwise use defaults
144
  image_timeout = 300
145
  video_timeout = 1500
@@ -155,9 +249,9 @@ class Database:
155
  """, (image_timeout, video_timeout))
156
 
157
  # Ensure token_refresh_config has a row
158
- cursor = await db.execute("SELECT COUNT(*) FROM token_refresh_config")
159
  count = await cursor.fetchone()
160
- if count[0] == 0:
161
  # Get token refresh config from config_dict if provided, otherwise use defaults
162
  at_auto_refresh_enabled = False
163
 
@@ -170,15 +264,9 @@ class Database:
170
  VALUES (1, ?)
171
  """, (at_auto_refresh_enabled,))
172
 
173
-
174
  async def check_and_migrate_db(self, config_dict: dict = None):
175
- """Check database integrity and perform migrations if needed
176
-
177
- Args:
178
- config_dict: Configuration dictionary from setting.toml (optional)
179
- Used to initialize new tables with values from setting.toml
180
- """
181
- async with aiosqlite.connect(self.db_path) as db:
182
  print("Checking database integrity and performing migrations...")
183
 
184
  # Check and add missing columns to tokens table
@@ -186,10 +274,10 @@ class Database:
186
  columns_to_add = [
187
  ("sora2_supported", "BOOLEAN"),
188
  ("sora2_invite_code", "TEXT"),
189
- ("sora2_redeemed_count", "INTEGER DEFAULT 0"),
190
- ("sora2_total_count", "INTEGER DEFAULT 0"),
191
- ("sora2_remaining_count", "INTEGER DEFAULT 0"),
192
- ("sora2_cooldown_until", "TIMESTAMP"),
193
  ("image_enabled", "BOOLEAN DEFAULT 1"),
194
  ("video_enabled", "BOOLEAN DEFAULT 1"),
195
  ]
@@ -205,8 +293,8 @@ class Database:
205
  # Check and add missing columns to admin_config table
206
  if await self._table_exists(db, "admin_config"):
207
  columns_to_add = [
208
- ("admin_username", "TEXT DEFAULT 'admin'"),
209
- ("admin_password", "TEXT DEFAULT 'admin'"),
210
  ]
211
 
212
  for col_name, col_type in columns_to_add:
@@ -220,7 +308,7 @@ class Database:
220
  # Check and add missing columns to watermark_free_config table
221
  if await self._table_exists(db, "watermark_free_config"):
222
  columns_to_add = [
223
- ("parse_method", "TEXT DEFAULT 'third_party'"),
224
  ("custom_parse_url", "TEXT"),
225
  ("custom_parse_token", "TEXT"),
226
  ]
@@ -234,7 +322,6 @@ class Database:
234
  print(f" ✗ Failed to add column '{col_name}': {e}")
235
 
236
  # Ensure all config tables have their default rows
237
- # Pass config_dict if available to initialize from setting.toml
238
  await self._ensure_config_rows(db, config_dict)
239
 
240
  await db.commit()
@@ -242,33 +329,33 @@ class Database:
242
 
243
  async def init_db(self):
244
  """Initialize database tables - creates all tables and ensures data integrity"""
245
- async with aiosqlite.connect(self.db_path) as db:
246
  # Tokens table
247
  await db.execute("""
248
  CREATE TABLE IF NOT EXISTS tokens (
249
- id INTEGER PRIMARY KEY AUTOINCREMENT,
250
- token TEXT UNIQUE NOT NULL,
251
- email TEXT NOT NULL,
252
- username TEXT NOT NULL,
253
- name TEXT NOT NULL,
254
  st TEXT,
255
  rt TEXT,
256
  remark TEXT,
257
- expiry_time TIMESTAMP,
258
  is_active BOOLEAN DEFAULT 1,
259
- cooled_until TIMESTAMP,
260
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
261
- last_used_at TIMESTAMP,
262
- use_count INTEGER DEFAULT 0,
263
- plan_type TEXT,
264
- plan_title TEXT,
265
- subscription_end TIMESTAMP,
266
  sora2_supported BOOLEAN,
267
- sora2_invite_code TEXT,
268
- sora2_redeemed_count INTEGER DEFAULT 0,
269
- sora2_total_count INTEGER DEFAULT 0,
270
- sora2_remaining_count INTEGER DEFAULT 0,
271
- sora2_cooldown_until TIMESTAMP,
272
  image_enabled BOOLEAN DEFAULT 1,
273
  video_enabled BOOLEAN DEFAULT 1
274
  )
@@ -277,12 +364,12 @@ class Database:
277
  # Token stats table
278
  await db.execute("""
279
  CREATE TABLE IF NOT EXISTS token_stats (
280
- id INTEGER PRIMARY KEY AUTOINCREMENT,
281
- token_id INTEGER NOT NULL,
282
- image_count INTEGER DEFAULT 0,
283
- video_count INTEGER DEFAULT 0,
284
- error_count INTEGER DEFAULT 0,
285
- last_error_at TIMESTAMP,
286
  FOREIGN KEY (token_id) REFERENCES tokens(id)
287
  )
288
  """)
@@ -290,17 +377,17 @@ class Database:
290
  # Tasks table
291
  await db.execute("""
292
  CREATE TABLE IF NOT EXISTS tasks (
293
- id INTEGER PRIMARY KEY AUTOINCREMENT,
294
- task_id TEXT UNIQUE NOT NULL,
295
- token_id INTEGER NOT NULL,
296
- model TEXT NOT NULL,
297
  prompt TEXT NOT NULL,
298
- status TEXT NOT NULL DEFAULT 'processing',
299
  progress FLOAT DEFAULT 0,
300
  result_urls TEXT,
301
  error_message TEXT,
302
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
303
- completed_at TIMESTAMP,
304
  FOREIGN KEY (token_id) REFERENCES tokens(id)
305
  )
306
  """)
@@ -308,12 +395,12 @@ class Database:
308
  # Request logs table
309
  await db.execute("""
310
  CREATE TABLE IF NOT EXISTS request_logs (
311
- id INTEGER PRIMARY KEY AUTOINCREMENT,
312
- token_id INTEGER,
313
- operation TEXT NOT NULL,
314
  request_body TEXT,
315
  response_body TEXT,
316
- status_code INTEGER NOT NULL,
317
  duration FLOAT NOT NULL,
318
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
319
  FOREIGN KEY (token_id) REFERENCES tokens(id)
@@ -323,88 +410,92 @@ class Database:
323
  # Admin config table
324
  await db.execute("""
325
  CREATE TABLE IF NOT EXISTS admin_config (
326
- id INTEGER PRIMARY KEY DEFAULT 1,
327
- admin_username TEXT DEFAULT 'admin',
328
- admin_password TEXT DEFAULT 'admin',
329
- error_ban_threshold INTEGER DEFAULT 3,
330
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
331
  )
332
  """)
333
 
334
  # Proxy config table
335
  await db.execute("""
336
  CREATE TABLE IF NOT EXISTS proxy_config (
337
- id INTEGER PRIMARY KEY DEFAULT 1,
338
  proxy_enabled BOOLEAN DEFAULT 0,
339
  proxy_url TEXT,
340
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
341
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
342
  )
343
  """)
344
 
345
  # Watermark-free config table
346
  await db.execute("""
347
  CREATE TABLE IF NOT EXISTS watermark_free_config (
348
- id INTEGER PRIMARY KEY DEFAULT 1,
349
  watermark_free_enabled BOOLEAN DEFAULT 0,
350
- parse_method TEXT DEFAULT 'third_party',
351
  custom_parse_url TEXT,
352
  custom_parse_token TEXT,
353
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
354
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
355
  )
356
  """)
357
 
358
  # Cache config table
359
  await db.execute("""
360
  CREATE TABLE IF NOT EXISTS cache_config (
361
- id INTEGER PRIMARY KEY DEFAULT 1,
362
  cache_enabled BOOLEAN DEFAULT 0,
363
- cache_timeout INTEGER DEFAULT 600,
364
  cache_base_url TEXT,
365
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
366
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
367
  )
368
  """)
369
 
370
  # Generation config table
371
  await db.execute("""
372
  CREATE TABLE IF NOT EXISTS generation_config (
373
- id INTEGER PRIMARY KEY DEFAULT 1,
374
- image_timeout INTEGER DEFAULT 300,
375
- video_timeout INTEGER DEFAULT 1500,
376
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
377
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
378
  )
379
  """)
380
 
381
  # Token refresh config table
382
  await db.execute("""
383
  CREATE TABLE IF NOT EXISTS token_refresh_config (
384
- id INTEGER PRIMARY KEY DEFAULT 1,
385
  at_auto_refresh_enabled BOOLEAN DEFAULT 0,
386
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
387
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
388
  )
389
  """)
390
 
391
  # Create indexes
392
- await db.execute("CREATE INDEX IF NOT EXISTS idx_task_id ON tasks(task_id)")
393
- await db.execute("CREATE INDEX IF NOT EXISTS idx_task_status ON tasks(status)")
394
- await db.execute("CREATE INDEX IF NOT EXISTS idx_token_active ON tokens(is_active)")
 
 
 
 
 
 
 
 
 
 
 
395
 
396
  await db.commit()
397
 
398
  async def init_config_from_toml(self, config_dict: dict, is_first_startup: bool = True):
399
- """
400
- Initialize database configuration from setting.toml
401
-
402
- Args:
403
- config_dict: Configuration dictionary from setting.toml
404
- is_first_startup: If True, only update if row doesn't exist. If False, always update.
405
- """
406
- async with aiosqlite.connect(self.db_path) as db:
407
- # On first startup, ensure all config rows exist with values from setting.toml
408
  if is_first_startup:
409
  await self._ensure_config_rows(db, config_dict)
410
 
@@ -412,13 +503,11 @@ class Database:
412
  admin_config = config_dict.get("admin", {})
413
  error_ban_threshold = admin_config.get("error_ban_threshold", 3)
414
 
415
- # Get admin credentials from global config
416
  global_config = config_dict.get("global", {})
417
  admin_username = global_config.get("admin_username", "admin")
418
  admin_password = global_config.get("admin_password", "admin")
419
 
420
  if not is_first_startup:
421
- # On upgrade, update the configuration
422
  await db.execute("""
423
  UPDATE admin_config
424
  SET admin_username = ?, admin_password = ?, error_ban_threshold = ?, updated_at = CURRENT_TIMESTAMP
@@ -429,12 +518,11 @@ class Database:
429
  proxy_config = config_dict.get("proxy", {})
430
  proxy_enabled = proxy_config.get("proxy_enabled", False)
431
  proxy_url = proxy_config.get("proxy_url", "")
432
- # Convert empty string to None
433
  proxy_url = proxy_url if proxy_url else None
434
 
435
  if is_first_startup:
436
  await db.execute("""
437
- INSERT OR IGNORE INTO proxy_config (id, proxy_enabled, proxy_url)
438
  VALUES (1, ?, ?)
439
  """, (proxy_enabled, proxy_url))
440
  else:
@@ -450,14 +538,13 @@ class Database:
450
  parse_method = watermark_config.get("parse_method", "third_party")
451
  custom_parse_url = watermark_config.get("custom_parse_url", "")
452
  custom_parse_token = watermark_config.get("custom_parse_token", "")
453
-
454
- # Convert empty strings to None
455
  custom_parse_url = custom_parse_url if custom_parse_url else None
456
  custom_parse_token = custom_parse_token if custom_parse_token else None
457
 
458
  if is_first_startup:
459
  await db.execute("""
460
- INSERT OR IGNORE INTO watermark_free_config (id, watermark_free_enabled, parse_method, custom_parse_url, custom_parse_token)
461
  VALUES (1, ?, ?, ?, ?)
462
  """, (watermark_free_enabled, parse_method, custom_parse_url, custom_parse_token))
463
  else:
@@ -473,12 +560,11 @@ class Database:
473
  cache_enabled = cache_config.get("enabled", False)
474
  cache_timeout = cache_config.get("timeout", 600)
475
  cache_base_url = cache_config.get("base_url", "")
476
- # Convert empty string to None
477
  cache_base_url = cache_base_url if cache_base_url else None
478
 
479
  if is_first_startup:
480
  await db.execute("""
481
- INSERT OR IGNORE INTO cache_config (id, cache_enabled, cache_timeout, cache_base_url)
482
  VALUES (1, ?, ?, ?)
483
  """, (cache_enabled, cache_timeout, cache_base_url))
484
  else:
@@ -495,7 +581,7 @@ class Database:
495
 
496
  if is_first_startup:
497
  await db.execute("""
498
- INSERT OR IGNORE INTO generation_config (id, image_timeout, video_timeout)
499
  VALUES (1, ?, ?)
500
  """, (image_timeout, video_timeout))
501
  else:
@@ -511,7 +597,7 @@ class Database:
511
 
512
  if is_first_startup:
513
  await db.execute("""
514
- INSERT OR IGNORE INTO token_refresh_config (id, at_auto_refresh_enabled)
515
  VALUES (1, ?)
516
  """, (at_auto_refresh_enabled,))
517
  else:
@@ -526,7 +612,7 @@ class Database:
526
  # Token operations
527
  async def add_token(self, token: Token) -> int:
528
  """Add a new token"""
529
- async with aiosqlite.connect(self.db_path) as db:
530
  cursor = await db.execute("""
531
  INSERT INTO tokens (token, email, username, name, st, rt, remark, expiry_time, is_active,
532
  plan_type, plan_title, subscription_end, sora2_supported, sora2_invite_code,
@@ -553,8 +639,7 @@ class Database:
553
 
554
  async def get_token(self, token_id: int) -> Optional[Token]:
555
  """Get token by ID"""
556
- async with aiosqlite.connect(self.db_path) as db:
557
- db.row_factory = aiosqlite.Row
558
  cursor = await db.execute("SELECT * FROM tokens WHERE id = ?", (token_id,))
559
  row = await cursor.fetchone()
560
  if row:
@@ -563,8 +648,7 @@ class Database:
563
 
564
  async def get_token_by_value(self, token: str) -> Optional[Token]:
565
  """Get token by value"""
566
- async with aiosqlite.connect(self.db_path) as db:
567
- db.row_factory = aiosqlite.Row
568
  cursor = await db.execute("SELECT * FROM tokens WHERE token = ?", (token,))
569
  row = await cursor.fetchone()
570
  if row:
@@ -573,29 +657,29 @@ class Database:
573
 
574
  async def get_active_tokens(self) -> List[Token]:
575
  """Get all active tokens (enabled, not cooled down, not expired)"""
576
- async with aiosqlite.connect(self.db_path) as db:
577
- db.row_factory = aiosqlite.Row
 
578
  cursor = await db.execute("""
579
  SELECT * FROM tokens
580
  WHERE is_active = 1
581
  AND (cooled_until IS NULL OR cooled_until < CURRENT_TIMESTAMP)
582
  AND expiry_time > CURRENT_TIMESTAMP
583
- ORDER BY last_used_at ASC NULLS FIRST
584
  """)
585
  rows = await cursor.fetchall()
586
  return [Token(**dict(row)) for row in rows]
587
 
588
  async def get_all_tokens(self) -> List[Token]:
589
  """Get all tokens"""
590
- async with aiosqlite.connect(self.db_path) as db:
591
- db.row_factory = aiosqlite.Row
592
  cursor = await db.execute("SELECT * FROM tokens ORDER BY created_at DESC")
593
  rows = await cursor.fetchall()
594
  return [Token(**dict(row)) for row in rows]
595
 
596
  async def update_token_usage(self, token_id: int):
597
  """Update token usage"""
598
- async with aiosqlite.connect(self.db_path) as db:
599
  await db.execute("""
600
  UPDATE tokens
601
  SET last_used_at = CURRENT_TIMESTAMP, use_count = use_count + 1
@@ -605,7 +689,7 @@ class Database:
605
 
606
  async def update_token_status(self, token_id: int, is_active: bool):
607
  """Update token status"""
608
- async with aiosqlite.connect(self.db_path) as db:
609
  await db.execute("""
610
  UPDATE tokens SET is_active = ? WHERE id = ?
611
  """, (is_active, token_id))
@@ -614,7 +698,7 @@ class Database:
614
  async def update_token_sora2(self, token_id: int, supported: bool, invite_code: Optional[str] = None,
615
  redeemed_count: int = 0, total_count: int = 0, remaining_count: int = 0):
616
  """Update token Sora2 support info"""
617
- async with aiosqlite.connect(self.db_path) as db:
618
  await db.execute("""
619
  UPDATE tokens
620
  SET sora2_supported = ?, sora2_invite_code = ?, sora2_redeemed_count = ?, sora2_total_count = ?, sora2_remaining_count = ?
@@ -624,7 +708,7 @@ class Database:
624
 
625
  async def update_token_sora2_remaining(self, token_id: int, remaining_count: int):
626
  """Update token Sora2 remaining count"""
627
- async with aiosqlite.connect(self.db_path) as db:
628
  await db.execute("""
629
  UPDATE tokens SET sora2_remaining_count = ? WHERE id = ?
630
  """, (remaining_count, token_id))
@@ -632,7 +716,7 @@ class Database:
632
 
633
  async def update_token_sora2_cooldown(self, token_id: int, cooldown_until: Optional[datetime]):
634
  """Update token Sora2 cooldown time"""
635
- async with aiosqlite.connect(self.db_path) as db:
636
  await db.execute("""
637
  UPDATE tokens SET sora2_cooldown_until = ? WHERE id = ?
638
  """, (cooldown_until, token_id))
@@ -640,7 +724,7 @@ class Database:
640
 
641
  async def update_token_cooldown(self, token_id: int, cooled_until: datetime):
642
  """Update token cooldown"""
643
- async with aiosqlite.connect(self.db_path) as db:
644
  await db.execute("""
645
  UPDATE tokens SET cooled_until = ? WHERE id = ?
646
  """, (cooled_until, token_id))
@@ -648,7 +732,7 @@ class Database:
648
 
649
  async def delete_token(self, token_id: int):
650
  """Delete token"""
651
- async with aiosqlite.connect(self.db_path) as db:
652
  await db.execute("DELETE FROM token_stats WHERE token_id = ?", (token_id,))
653
  await db.execute("DELETE FROM tokens WHERE id = ?", (token_id,))
654
  await db.commit()
@@ -665,7 +749,7 @@ class Database:
665
  image_enabled: Optional[bool] = None,
666
  video_enabled: Optional[bool] = None):
667
  """Update token (AT, ST, RT, remark, expiry_time, subscription info, image_enabled, video_enabled)"""
668
- async with aiosqlite.connect(self.db_path) as db:
669
  # Build dynamic update query
670
  updates = []
671
  params = []
@@ -719,8 +803,7 @@ class Database:
719
  # Token stats operations
720
  async def get_token_stats(self, token_id: int) -> Optional[TokenStats]:
721
  """Get token statistics"""
722
- async with aiosqlite.connect(self.db_path) as db:
723
- db.row_factory = aiosqlite.Row
724
  cursor = await db.execute("SELECT * FROM token_stats WHERE token_id = ?", (token_id,))
725
  row = await cursor.fetchone()
726
  if row:
@@ -729,7 +812,7 @@ class Database:
729
 
730
  async def increment_image_count(self, token_id: int):
731
  """Increment image generation count"""
732
- async with aiosqlite.connect(self.db_path) as db:
733
  await db.execute("""
734
  UPDATE token_stats SET image_count = image_count + 1 WHERE token_id = ?
735
  """, (token_id,))
@@ -737,7 +820,7 @@ class Database:
737
 
738
  async def increment_video_count(self, token_id: int):
739
  """Increment video generation count"""
740
- async with aiosqlite.connect(self.db_path) as db:
741
  await db.execute("""
742
  UPDATE token_stats SET video_count = video_count + 1 WHERE token_id = ?
743
  """, (token_id,))
@@ -745,7 +828,7 @@ class Database:
745
 
746
  async def increment_error_count(self, token_id: int):
747
  """Increment error count"""
748
- async with aiosqlite.connect(self.db_path) as db:
749
  await db.execute("""
750
  UPDATE token_stats
751
  SET error_count = error_count + 1, last_error_at = CURRENT_TIMESTAMP
@@ -755,7 +838,7 @@ class Database:
755
 
756
  async def reset_error_count(self, token_id: int):
757
  """Reset error count"""
758
- async with aiosqlite.connect(self.db_path) as db:
759
  await db.execute("""
760
  UPDATE token_stats SET error_count = 0 WHERE token_id = ?
761
  """, (token_id,))
@@ -764,7 +847,7 @@ class Database:
764
  # Task operations
765
  async def create_task(self, task: Task) -> int:
766
  """Create a new task"""
767
- async with aiosqlite.connect(self.db_path) as db:
768
  cursor = await db.execute("""
769
  INSERT INTO tasks (task_id, token_id, model, prompt, status, progress)
770
  VALUES (?, ?, ?, ?, ?, ?)
@@ -775,7 +858,7 @@ class Database:
775
  async def update_task(self, task_id: str, status: str, progress: float,
776
  result_urls: Optional[str] = None, error_message: Optional[str] = None):
777
  """Update task status"""
778
- async with aiosqlite.connect(self.db_path) as db:
779
  completed_at = datetime.now() if status in ["completed", "failed"] else None
780
  await db.execute("""
781
  UPDATE tasks
@@ -786,8 +869,7 @@ class Database:
786
 
787
  async def get_task(self, task_id: str) -> Optional[Task]:
788
  """Get task by ID"""
789
- async with aiosqlite.connect(self.db_path) as db:
790
- db.row_factory = aiosqlite.Row
791
  cursor = await db.execute("SELECT * FROM tasks WHERE task_id = ?", (task_id,))
792
  row = await cursor.fetchone()
793
  if row:
@@ -797,7 +879,7 @@ class Database:
797
  # Request log operations
798
  async def log_request(self, log: RequestLog):
799
  """Log a request"""
800
- async with aiosqlite.connect(self.db_path) as db:
801
  await db.execute("""
802
  INSERT INTO request_logs (token_id, operation, request_body, response_body, status_code, duration)
803
  VALUES (?, ?, ?, ?, ?, ?)
@@ -807,8 +889,7 @@ class Database:
807
 
808
  async def get_recent_logs(self, limit: int = 100) -> List[dict]:
809
  """Get recent logs with token email"""
810
- async with aiosqlite.connect(self.db_path) as db:
811
- db.row_factory = aiosqlite.Row
812
  cursor = await db.execute("""
813
  SELECT
814
  rl.id,
@@ -831,19 +912,16 @@ class Database:
831
  # Admin config operations
832
  async def get_admin_config(self) -> AdminConfig:
833
  """Get admin configuration"""
834
- async with aiosqlite.connect(self.db_path) as db:
835
- db.row_factory = aiosqlite.Row
836
  cursor = await db.execute("SELECT * FROM admin_config WHERE id = 1")
837
  row = await cursor.fetchone()
838
  if row:
839
  return AdminConfig(**dict(row))
840
- # If no row exists, return a default config with placeholder values
841
- # This should not happen in normal operation as _ensure_config_rows should create it
842
  return AdminConfig(admin_username="admin", admin_password="admin")
843
 
844
  async def update_admin_config(self, config: AdminConfig):
845
  """Update admin configuration"""
846
- async with aiosqlite.connect(self.db_path) as db:
847
  await db.execute("""
848
  UPDATE admin_config
849
  SET admin_username = ?, admin_password = ?, error_ban_threshold = ?, updated_at = CURRENT_TIMESTAMP
@@ -854,19 +932,16 @@ class Database:
854
  # Proxy config operations
855
  async def get_proxy_config(self) -> ProxyConfig:
856
  """Get proxy configuration"""
857
- async with aiosqlite.connect(self.db_path) as db:
858
- db.row_factory = aiosqlite.Row
859
  cursor = await db.execute("SELECT * FROM proxy_config WHERE id = 1")
860
  row = await cursor.fetchone()
861
  if row:
862
  return ProxyConfig(**dict(row))
863
- # If no row exists, return a default config
864
- # This should not happen in normal operation as _ensure_config_rows should create it
865
  return ProxyConfig(proxy_enabled=False)
866
 
867
  async def update_proxy_config(self, enabled: bool, proxy_url: Optional[str]):
868
  """Update proxy configuration"""
869
- async with aiosqlite.connect(self.db_path) as db:
870
  await db.execute("""
871
  UPDATE proxy_config
872
  SET proxy_enabled = ?, proxy_url = ?, updated_at = CURRENT_TIMESTAMP
@@ -877,20 +952,17 @@ class Database:
877
  # Watermark-free config operations
878
  async def get_watermark_free_config(self) -> WatermarkFreeConfig:
879
  """Get watermark-free configuration"""
880
- async with aiosqlite.connect(self.db_path) as db:
881
- db.row_factory = aiosqlite.Row
882
  cursor = await db.execute("SELECT * FROM watermark_free_config WHERE id = 1")
883
  row = await cursor.fetchone()
884
  if row:
885
  return WatermarkFreeConfig(**dict(row))
886
- # If no row exists, return a default config
887
- # This should not happen in normal operation as _ensure_config_rows should create it
888
  return WatermarkFreeConfig(watermark_free_enabled=False, parse_method="third_party")
889
 
890
  async def update_watermark_free_config(self, enabled: bool, parse_method: str = None,
891
  custom_parse_url: str = None, custom_parse_token: str = None):
892
  """Update watermark-free configuration"""
893
- async with aiosqlite.connect(self.db_path) as db:
894
  if parse_method is None and custom_parse_url is None and custom_parse_token is None:
895
  # Only update enabled status
896
  await db.execute("""
@@ -911,27 +983,22 @@ class Database:
911
  # Cache config operations
912
  async def get_cache_config(self) -> CacheConfig:
913
  """Get cache configuration"""
914
- async with aiosqlite.connect(self.db_path) as db:
915
- db.row_factory = aiosqlite.Row
916
  cursor = await db.execute("SELECT * FROM cache_config WHERE id = 1")
917
  row = await cursor.fetchone()
918
  if row:
919
  return CacheConfig(**dict(row))
920
- # If no row exists, return a default config
921
- # This should not happen in normal operation as _ensure_config_rows should create it
922
  return CacheConfig(cache_enabled=False, cache_timeout=600)
923
 
924
  async def update_cache_config(self, enabled: bool = None, timeout: int = None, base_url: Optional[str] = None):
925
  """Update cache configuration"""
926
- async with aiosqlite.connect(self.db_path) as db:
927
  # Get current config first
928
- db.row_factory = aiosqlite.Row
929
  cursor = await db.execute("SELECT * FROM cache_config WHERE id = 1")
930
  row = await cursor.fetchone()
931
 
932
  if row:
933
  current = dict(row)
934
- # Update only provided fields
935
  new_enabled = enabled if enabled is not None else current.get("cache_enabled", False)
936
  new_timeout = timeout if timeout is not None else current.get("cache_timeout", 600)
937
  new_base_url = base_url if base_url is not None else current.get("cache_base_url")
@@ -953,27 +1020,21 @@ class Database:
953
  # Generation config operations
954
  async def get_generation_config(self) -> GenerationConfig:
955
  """Get generation configuration"""
956
- async with aiosqlite.connect(self.db_path) as db:
957
- db.row_factory = aiosqlite.Row
958
  cursor = await db.execute("SELECT * FROM generation_config WHERE id = 1")
959
  row = await cursor.fetchone()
960
  if row:
961
  return GenerationConfig(**dict(row))
962
- # If no row exists, return a default config
963
- # This should not happen in normal operation as _ensure_config_rows should create it
964
  return GenerationConfig(image_timeout=300, video_timeout=1500)
965
 
966
  async def update_generation_config(self, image_timeout: int = None, video_timeout: int = None):
967
  """Update generation configuration"""
968
- async with aiosqlite.connect(self.db_path) as db:
969
- # Get current config first
970
- db.row_factory = aiosqlite.Row
971
  cursor = await db.execute("SELECT * FROM generation_config WHERE id = 1")
972
  row = await cursor.fetchone()
973
 
974
  if row:
975
  current = dict(row)
976
- # Update only provided fields
977
  new_image_timeout = image_timeout if image_timeout is not None else current.get("image_timeout", 300)
978
  new_video_timeout = video_timeout if video_timeout is not None else current.get("video_timeout", 1500)
979
  else:
@@ -990,23 +1051,19 @@ class Database:
990
  # Token refresh config operations
991
  async def get_token_refresh_config(self) -> TokenRefreshConfig:
992
  """Get token refresh configuration"""
993
- async with aiosqlite.connect(self.db_path) as db:
994
- db.row_factory = aiosqlite.Row
995
  cursor = await db.execute("SELECT * FROM token_refresh_config WHERE id = 1")
996
  row = await cursor.fetchone()
997
  if row:
998
  return TokenRefreshConfig(**dict(row))
999
- # If no row exists, return a default config
1000
- # This should not happen in normal operation as _ensure_config_rows should create it
1001
  return TokenRefreshConfig(at_auto_refresh_enabled=False)
1002
 
1003
  async def update_token_refresh_config(self, at_auto_refresh_enabled: bool):
1004
  """Update token refresh configuration"""
1005
- async with aiosqlite.connect(self.db_path) as db:
1006
  await db.execute("""
1007
  UPDATE token_refresh_config
1008
  SET at_auto_refresh_enabled = ?, updated_at = CURRENT_TIMESTAMP
1009
  WHERE id = 1
1010
  """, (at_auto_refresh_enabled,))
1011
  await db.commit()
1012
-
 
1
  """Database storage layer"""
2
+ import aiomysql
3
  import json
4
  from datetime import datetime
5
+ from typing import Optional, List, Any
6
  from pathlib import Path
7
  from .models import Token, TokenStats, Task, RequestLog, AdminConfig, ProxyConfig, WatermarkFreeConfig, CacheConfig, GenerationConfig, TokenRefreshConfig
8
+ from .config import config
9
+
10
+ class MySQLCursorWrapper:
11
+ def __init__(self, cursor):
12
+ self.cursor = cursor
13
+
14
+ def __getattr__(self, name):
15
+ return getattr(self.cursor, name)
16
+
17
+ async def fetchone(self):
18
+ return await self.cursor.fetchone()
19
+
20
+ async def fetchall(self):
21
+ return await self.cursor.fetchall()
22
+
23
+ @property
24
+ def lastrowid(self):
25
+ return self.cursor.lastrowid
26
+
27
+ class MySQLConnectionWrapper:
28
+ def __init__(self, pool):
29
+ self.pool = pool
30
+ self.conn = None
31
+ self.cursor = None
32
+
33
+ async def __aenter__(self):
34
+ self.conn = await self.pool.acquire()
35
+ return self
36
+
37
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
38
+ if self.conn:
39
+ self.pool.release(self.conn)
40
+
41
+ async def execute(self, query: str, args: tuple = None) -> MySQLCursorWrapper:
42
+ # Convert sqlite '?' placeholder to mysql '%s'
43
+ query = query.replace('?', '%s')
44
+
45
+ # Create cursor (default to DictCursor for compatibility with explicit dict access,
46
+ # but for row_factory we might need to handle it differently.
47
+ # Existing code uses DictCursor logic mostly via dict(row))
48
+ self.cursor = await self.conn.cursor(aiomysql.DictCursor)
49
+
50
+ try:
51
+ await self.cursor.execute(query, args)
52
+ except Exception as e:
53
+ # Add context to error
54
+ print(f"SQL Error: {e}\nQuery: {query}\nArgs: {args}")
55
+ raise e
56
+ return MySQLCursorWrapper(self.cursor)
57
+
58
+ async def commit(self):
59
+ await self.conn.commit()
60
+
61
+ @property
62
+ def row_factory(self):
63
+ return None
64
+
65
+ @row_factory.setter
66
+ def row_factory(self, value):
67
+ # aiomysql DictCursor yields dict-like objects already, close enough to aiosqlite.Row for most uses
68
+ pass
69
 
70
  class Database:
71
+ """MySQL database manager (compatible interface with previous SQLite implementation)"""
72
 
73
  def __init__(self, db_path: str = None):
74
+ self._pool = None
75
+ # db_path is ignored for MySQL, we use config
76
+
77
+ async def get_pool(self):
78
+ if self._pool is None:
79
+ self._pool = await aiomysql.create_pool(
80
+ host=config.db_host,
81
+ port=config.db_port,
82
+ user=config.db_user,
83
+ password=config.db_password,
84
+ db=config.db_name,
85
+ autocommit=False, # We manually commit
86
+ cursorclass=aiomysql.DictCursor
87
+ )
88
+ return self._pool
89
+
90
+ def connect(self):
91
+ """Return a context manager that mimics aiosqlite.connect"""
92
+ # We can't await in __init__ or synchronous property, so we return an async context manager helper
93
+ class ConnectContext:
94
+ def __init__(self, db_instance):
95
+ self.db = db_instance
96
+
97
+ async def __aenter__(self):
98
+ pool = await self.db.get_pool()
99
+ self.wrapper = MySQLConnectionWrapper(pool)
100
+ return await self.wrapper.__aenter__()
101
+
102
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
103
+ await self.wrapper.__aexit__(exc_type, exc_val, exc_tb)
104
+
105
+ return ConnectContext(self)
106
+
107
+ async def db_exists(self) -> bool:
108
+ """Check if database exists (always true for MySQL if we can connect)"""
109
+ try:
110
+ pool = await self.get_pool()
111
+ async with pool.acquire() as conn:
112
+ async with conn.cursor() as cur:
113
+ await cur.execute("SELECT 1")
114
+ return True
115
+ except Exception as e:
116
+ print(f"Database connection failed: {e}")
117
+ return False
118
 
119
  async def _table_exists(self, db, table_name: str) -> bool:
120
  """Check if a table exists in the database"""
121
+ # Note: db here is our MySQLConnectionWrapper
122
  cursor = await db.execute(
123
+ "SELECT count(*) as count FROM information_schema.tables WHERE table_schema = %s AND table_name = %s",
124
+ (config.db_name, table_name)
125
  )
126
  result = await cursor.fetchone()
127
+ return result['count'] > 0
128
 
129
  async def _column_exists(self, db, table_name: str, column_name: str) -> bool:
130
  """Check if a column exists in a table"""
131
  try:
132
+ cursor = await db.execute(
133
+ "SELECT count(*) as count FROM information_schema.columns WHERE table_schema = %s AND table_name = %s AND column_name = %s",
134
+ (config.db_name, table_name, column_name)
135
+ )
136
+ result = await cursor.fetchone()
137
+ return result['count'] > 0
138
  except:
139
  return False
140
 
141
  async def _ensure_config_rows(self, db, config_dict: dict = None):
142
+ """Ensure all config tables have their default rows"""
 
 
 
 
 
143
  # Ensure admin_config has a row
144
+ cursor = await db.execute("SELECT COUNT(*) as count FROM admin_config")
145
  count = await cursor.fetchone()
146
+ if count['count'] == 0:
147
  # Get admin credentials from config_dict if provided, otherwise use defaults
148
  admin_username = "admin"
149
  admin_password = "admin"
 
163
  """, (admin_username, admin_password, error_ban_threshold))
164
 
165
  # Ensure proxy_config has a row
166
+ cursor = await db.execute("SELECT COUNT(*) as count FROM proxy_config")
167
  count = await cursor.fetchone()
168
+ if count['count'] == 0:
169
  # Get proxy config from config_dict if provided, otherwise use defaults
170
  proxy_enabled = False
171
  proxy_url = None
 
183
  """, (proxy_enabled, proxy_url))
184
 
185
  # Ensure watermark_free_config has a row
186
+ cursor = await db.execute("SELECT COUNT(*) as count FROM watermark_free_config")
187
  count = await cursor.fetchone()
188
+ if count['count'] == 0:
189
  # Get watermark-free config from config_dict if provided, otherwise use defaults
190
  watermark_free_enabled = False
191
  parse_method = "third_party"
 
209
  """, (watermark_free_enabled, parse_method, custom_parse_url, custom_parse_token))
210
 
211
  # Ensure cache_config has a row
212
+ cursor = await db.execute("SELECT COUNT(*) as count FROM cache_config")
213
  count = await cursor.fetchone()
214
+ if count['count'] == 0:
215
  # Get cache config from config_dict if provided, otherwise use defaults
216
  cache_enabled = False
217
  cache_timeout = 600
 
231
  """, (cache_enabled, cache_timeout, cache_base_url))
232
 
233
  # Ensure generation_config has a row
234
+ cursor = await db.execute("SELECT COUNT(*) as count FROM generation_config")
235
  count = await cursor.fetchone()
236
+ if count['count'] == 0:
237
  # Get generation config from config_dict if provided, otherwise use defaults
238
  image_timeout = 300
239
  video_timeout = 1500
 
249
  """, (image_timeout, video_timeout))
250
 
251
  # Ensure token_refresh_config has a row
252
+ cursor = await db.execute("SELECT COUNT(*) as count FROM token_refresh_config")
253
  count = await cursor.fetchone()
254
+ if count['count'] == 0:
255
  # Get token refresh config from config_dict if provided, otherwise use defaults
256
  at_auto_refresh_enabled = False
257
 
 
264
  VALUES (1, ?)
265
  """, (at_auto_refresh_enabled,))
266
 
 
267
  async def check_and_migrate_db(self, config_dict: dict = None):
268
+ """Check database integrity and perform migrations if needed"""
269
+ async with self.connect() as db:
 
 
 
 
 
270
  print("Checking database integrity and performing migrations...")
271
 
272
  # Check and add missing columns to tokens table
 
274
  columns_to_add = [
275
  ("sora2_supported", "BOOLEAN"),
276
  ("sora2_invite_code", "TEXT"),
277
+ ("sora2_redeemed_count", "INT DEFAULT 0"),
278
+ ("sora2_total_count", "INT DEFAULT 0"),
279
+ ("sora2_remaining_count", "INT DEFAULT 0"),
280
+ ("sora2_cooldown_until", "DATETIME"),
281
  ("image_enabled", "BOOLEAN DEFAULT 1"),
282
  ("video_enabled", "BOOLEAN DEFAULT 1"),
283
  ]
 
293
  # Check and add missing columns to admin_config table
294
  if await self._table_exists(db, "admin_config"):
295
  columns_to_add = [
296
+ ("admin_username", "TEXT"),
297
+ ("admin_password", "TEXT"),
298
  ]
299
 
300
  for col_name, col_type in columns_to_add:
 
308
  # Check and add missing columns to watermark_free_config table
309
  if await self._table_exists(db, "watermark_free_config"):
310
  columns_to_add = [
311
+ ("parse_method", "TEXT"),
312
  ("custom_parse_url", "TEXT"),
313
  ("custom_parse_token", "TEXT"),
314
  ]
 
322
  print(f" ✗ Failed to add column '{col_name}': {e}")
323
 
324
  # Ensure all config tables have their default rows
 
325
  await self._ensure_config_rows(db, config_dict)
326
 
327
  await db.commit()
 
329
 
330
  async def init_db(self):
331
  """Initialize database tables - creates all tables and ensures data integrity"""
332
+ async with self.connect() as db:
333
  # Tokens table
334
  await db.execute("""
335
  CREATE TABLE IF NOT EXISTS tokens (
336
+ id INT PRIMARY KEY AUTO_INCREMENT,
337
+ token VARCHAR(255) UNIQUE NOT NULL,
338
+ email VARCHAR(255) NOT NULL,
339
+ username VARCHAR(255) NOT NULL,
340
+ name VARCHAR(255) NOT NULL,
341
  st TEXT,
342
  rt TEXT,
343
  remark TEXT,
344
+ expiry_time DATETIME,
345
  is_active BOOLEAN DEFAULT 1,
346
+ cooled_until DATETIME,
347
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
348
+ last_used_at DATETIME,
349
+ use_count INT DEFAULT 0,
350
+ plan_type VARCHAR(50),
351
+ plan_title VARCHAR(100),
352
+ subscription_end DATETIME,
353
  sora2_supported BOOLEAN,
354
+ sora2_invite_code VARCHAR(255),
355
+ sora2_redeemed_count INT DEFAULT 0,
356
+ sora2_total_count INT DEFAULT 0,
357
+ sora2_remaining_count INT DEFAULT 0,
358
+ sora2_cooldown_until DATETIME,
359
  image_enabled BOOLEAN DEFAULT 1,
360
  video_enabled BOOLEAN DEFAULT 1
361
  )
 
364
  # Token stats table
365
  await db.execute("""
366
  CREATE TABLE IF NOT EXISTS token_stats (
367
+ id INT PRIMARY KEY AUTO_INCREMENT,
368
+ token_id INT NOT NULL,
369
+ image_count INT DEFAULT 0,
370
+ video_count INT DEFAULT 0,
371
+ error_count INT DEFAULT 0,
372
+ last_error_at DATETIME,
373
  FOREIGN KEY (token_id) REFERENCES tokens(id)
374
  )
375
  """)
 
377
  # Tasks table
378
  await db.execute("""
379
  CREATE TABLE IF NOT EXISTS tasks (
380
+ id INT PRIMARY KEY AUTO_INCREMENT,
381
+ task_id VARCHAR(255) UNIQUE NOT NULL,
382
+ token_id INT NOT NULL,
383
+ model VARCHAR(255) NOT NULL,
384
  prompt TEXT NOT NULL,
385
+ status VARCHAR(50) NOT NULL DEFAULT 'processing',
386
  progress FLOAT DEFAULT 0,
387
  result_urls TEXT,
388
  error_message TEXT,
389
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
390
+ completed_at DATETIME,
391
  FOREIGN KEY (token_id) REFERENCES tokens(id)
392
  )
393
  """)
 
395
  # Request logs table
396
  await db.execute("""
397
  CREATE TABLE IF NOT EXISTS request_logs (
398
+ id INT PRIMARY KEY AUTO_INCREMENT,
399
+ token_id INT,
400
+ operation VARCHAR(255) NOT NULL,
401
  request_body TEXT,
402
  response_body TEXT,
403
+ status_code INT NOT NULL,
404
  duration FLOAT NOT NULL,
405
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
406
  FOREIGN KEY (token_id) REFERENCES tokens(id)
 
410
  # Admin config table
411
  await db.execute("""
412
  CREATE TABLE IF NOT EXISTS admin_config (
413
+ id INT PRIMARY KEY DEFAULT 1,
414
+ admin_username VARCHAR(255) DEFAULT 'admin',
415
+ admin_password VARCHAR(255) DEFAULT 'admin',
416
+ error_ban_threshold INT DEFAULT 3,
417
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
418
  )
419
  """)
420
 
421
  # Proxy config table
422
  await db.execute("""
423
  CREATE TABLE IF NOT EXISTS proxy_config (
424
+ id INT PRIMARY KEY DEFAULT 1,
425
  proxy_enabled BOOLEAN DEFAULT 0,
426
  proxy_url TEXT,
427
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
428
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
429
  )
430
  """)
431
 
432
  # Watermark-free config table
433
  await db.execute("""
434
  CREATE TABLE IF NOT EXISTS watermark_free_config (
435
+ id INT PRIMARY KEY DEFAULT 1,
436
  watermark_free_enabled BOOLEAN DEFAULT 0,
437
+ parse_method VARCHAR(50) DEFAULT 'third_party',
438
  custom_parse_url TEXT,
439
  custom_parse_token TEXT,
440
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
441
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
442
  )
443
  """)
444
 
445
  # Cache config table
446
  await db.execute("""
447
  CREATE TABLE IF NOT EXISTS cache_config (
448
+ id INT PRIMARY KEY DEFAULT 1,
449
  cache_enabled BOOLEAN DEFAULT 0,
450
+ cache_timeout INT DEFAULT 600,
451
  cache_base_url TEXT,
452
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
453
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
454
  )
455
  """)
456
 
457
  # Generation config table
458
  await db.execute("""
459
  CREATE TABLE IF NOT EXISTS generation_config (
460
+ id INT PRIMARY KEY DEFAULT 1,
461
+ image_timeout INT DEFAULT 300,
462
+ video_timeout INT DEFAULT 1500,
463
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
464
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
465
  )
466
  """)
467
 
468
  # Token refresh config table
469
  await db.execute("""
470
  CREATE TABLE IF NOT EXISTS token_refresh_config (
471
+ id INT PRIMARY KEY DEFAULT 1,
472
  at_auto_refresh_enabled BOOLEAN DEFAULT 0,
473
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
474
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
475
  )
476
  """)
477
 
478
  # Create indexes
479
+ # MySQL handles index creation slightly differently/implicitly but this is standard enough
480
+ # Use separate calls and catch error if index exists (or use CREATE INDEX IF NOT EXISTS which MySQL supports in 8.0+, assuming 5.7+ check needed?)
481
+ # MySQL 8.0+ supports IF NOT EXISTS.
482
+ try:
483
+ await db.execute("CREATE INDEX idx_task_id ON tasks(task_id)")
484
+ except: pass
485
+
486
+ try:
487
+ await db.execute("CREATE INDEX idx_task_status ON tasks(status)")
488
+ except: pass
489
+
490
+ try:
491
+ await db.execute("CREATE INDEX idx_token_active ON tokens(is_active)")
492
+ except: pass
493
 
494
  await db.commit()
495
 
496
  async def init_config_from_toml(self, config_dict: dict, is_first_startup: bool = True):
497
+ """Initialize database configuration from setting.toml"""
498
+ async with self.connect() as db:
 
 
 
 
 
 
 
499
  if is_first_startup:
500
  await self._ensure_config_rows(db, config_dict)
501
 
 
503
  admin_config = config_dict.get("admin", {})
504
  error_ban_threshold = admin_config.get("error_ban_threshold", 3)
505
 
 
506
  global_config = config_dict.get("global", {})
507
  admin_username = global_config.get("admin_username", "admin")
508
  admin_password = global_config.get("admin_password", "admin")
509
 
510
  if not is_first_startup:
 
511
  await db.execute("""
512
  UPDATE admin_config
513
  SET admin_username = ?, admin_password = ?, error_ban_threshold = ?, updated_at = CURRENT_TIMESTAMP
 
518
  proxy_config = config_dict.get("proxy", {})
519
  proxy_enabled = proxy_config.get("proxy_enabled", False)
520
  proxy_url = proxy_config.get("proxy_url", "")
 
521
  proxy_url = proxy_url if proxy_url else None
522
 
523
  if is_first_startup:
524
  await db.execute("""
525
+ INSERT IGNORE INTO proxy_config (id, proxy_enabled, proxy_url)
526
  VALUES (1, ?, ?)
527
  """, (proxy_enabled, proxy_url))
528
  else:
 
538
  parse_method = watermark_config.get("parse_method", "third_party")
539
  custom_parse_url = watermark_config.get("custom_parse_url", "")
540
  custom_parse_token = watermark_config.get("custom_parse_token", "")
541
+
 
542
  custom_parse_url = custom_parse_url if custom_parse_url else None
543
  custom_parse_token = custom_parse_token if custom_parse_token else None
544
 
545
  if is_first_startup:
546
  await db.execute("""
547
+ INSERT IGNORE INTO watermark_free_config (id, watermark_free_enabled, parse_method, custom_parse_url, custom_parse_token)
548
  VALUES (1, ?, ?, ?, ?)
549
  """, (watermark_free_enabled, parse_method, custom_parse_url, custom_parse_token))
550
  else:
 
560
  cache_enabled = cache_config.get("enabled", False)
561
  cache_timeout = cache_config.get("timeout", 600)
562
  cache_base_url = cache_config.get("base_url", "")
 
563
  cache_base_url = cache_base_url if cache_base_url else None
564
 
565
  if is_first_startup:
566
  await db.execute("""
567
+ INSERT IGNORE INTO cache_config (id, cache_enabled, cache_timeout, cache_base_url)
568
  VALUES (1, ?, ?, ?)
569
  """, (cache_enabled, cache_timeout, cache_base_url))
570
  else:
 
581
 
582
  if is_first_startup:
583
  await db.execute("""
584
+ INSERT IGNORE INTO generation_config (id, image_timeout, video_timeout)
585
  VALUES (1, ?, ?)
586
  """, (image_timeout, video_timeout))
587
  else:
 
597
 
598
  if is_first_startup:
599
  await db.execute("""
600
+ INSERT IGNORE INTO token_refresh_config (id, at_auto_refresh_enabled)
601
  VALUES (1, ?)
602
  """, (at_auto_refresh_enabled,))
603
  else:
 
612
  # Token operations
613
  async def add_token(self, token: Token) -> int:
614
  """Add a new token"""
615
+ async with self.connect() as db:
616
  cursor = await db.execute("""
617
  INSERT INTO tokens (token, email, username, name, st, rt, remark, expiry_time, is_active,
618
  plan_type, plan_title, subscription_end, sora2_supported, sora2_invite_code,
 
639
 
640
  async def get_token(self, token_id: int) -> Optional[Token]:
641
  """Get token by ID"""
642
+ async with self.connect() as db:
 
643
  cursor = await db.execute("SELECT * FROM tokens WHERE id = ?", (token_id,))
644
  row = await cursor.fetchone()
645
  if row:
 
648
 
649
  async def get_token_by_value(self, token: str) -> Optional[Token]:
650
  """Get token by value"""
651
+ async with self.connect() as db:
 
652
  cursor = await db.execute("SELECT * FROM tokens WHERE token = ?", (token,))
653
  row = await cursor.fetchone()
654
  if row:
 
657
 
658
  async def get_active_tokens(self) -> List[Token]:
659
  """Get all active tokens (enabled, not cooled down, not expired)"""
660
+ async with self.connect() as db:
661
+ # MySQL handles booleans as TinyInt, so is_active = 1 works.
662
+ # CURRENT_TIMESTAMP is standard.
663
  cursor = await db.execute("""
664
  SELECT * FROM tokens
665
  WHERE is_active = 1
666
  AND (cooled_until IS NULL OR cooled_until < CURRENT_TIMESTAMP)
667
  AND expiry_time > CURRENT_TIMESTAMP
668
+ ORDER BY last_used_at ASC
669
  """)
670
  rows = await cursor.fetchall()
671
  return [Token(**dict(row)) for row in rows]
672
 
673
  async def get_all_tokens(self) -> List[Token]:
674
  """Get all tokens"""
675
+ async with self.connect() as db:
 
676
  cursor = await db.execute("SELECT * FROM tokens ORDER BY created_at DESC")
677
  rows = await cursor.fetchall()
678
  return [Token(**dict(row)) for row in rows]
679
 
680
  async def update_token_usage(self, token_id: int):
681
  """Update token usage"""
682
+ async with self.connect() as db:
683
  await db.execute("""
684
  UPDATE tokens
685
  SET last_used_at = CURRENT_TIMESTAMP, use_count = use_count + 1
 
689
 
690
  async def update_token_status(self, token_id: int, is_active: bool):
691
  """Update token status"""
692
+ async with self.connect() as db:
693
  await db.execute("""
694
  UPDATE tokens SET is_active = ? WHERE id = ?
695
  """, (is_active, token_id))
 
698
  async def update_token_sora2(self, token_id: int, supported: bool, invite_code: Optional[str] = None,
699
  redeemed_count: int = 0, total_count: int = 0, remaining_count: int = 0):
700
  """Update token Sora2 support info"""
701
+ async with self.connect() as db:
702
  await db.execute("""
703
  UPDATE tokens
704
  SET sora2_supported = ?, sora2_invite_code = ?, sora2_redeemed_count = ?, sora2_total_count = ?, sora2_remaining_count = ?
 
708
 
709
  async def update_token_sora2_remaining(self, token_id: int, remaining_count: int):
710
  """Update token Sora2 remaining count"""
711
+ async with self.connect() as db:
712
  await db.execute("""
713
  UPDATE tokens SET sora2_remaining_count = ? WHERE id = ?
714
  """, (remaining_count, token_id))
 
716
 
717
  async def update_token_sora2_cooldown(self, token_id: int, cooldown_until: Optional[datetime]):
718
  """Update token Sora2 cooldown time"""
719
+ async with self.connect() as db:
720
  await db.execute("""
721
  UPDATE tokens SET sora2_cooldown_until = ? WHERE id = ?
722
  """, (cooldown_until, token_id))
 
724
 
725
  async def update_token_cooldown(self, token_id: int, cooled_until: datetime):
726
  """Update token cooldown"""
727
+ async with self.connect() as db:
728
  await db.execute("""
729
  UPDATE tokens SET cooled_until = ? WHERE id = ?
730
  """, (cooled_until, token_id))
 
732
 
733
  async def delete_token(self, token_id: int):
734
  """Delete token"""
735
+ async with self.connect() as db:
736
  await db.execute("DELETE FROM token_stats WHERE token_id = ?", (token_id,))
737
  await db.execute("DELETE FROM tokens WHERE id = ?", (token_id,))
738
  await db.commit()
 
749
  image_enabled: Optional[bool] = None,
750
  video_enabled: Optional[bool] = None):
751
  """Update token (AT, ST, RT, remark, expiry_time, subscription info, image_enabled, video_enabled)"""
752
+ async with self.connect() as db:
753
  # Build dynamic update query
754
  updates = []
755
  params = []
 
803
  # Token stats operations
804
  async def get_token_stats(self, token_id: int) -> Optional[TokenStats]:
805
  """Get token statistics"""
806
+ async with self.connect() as db:
 
807
  cursor = await db.execute("SELECT * FROM token_stats WHERE token_id = ?", (token_id,))
808
  row = await cursor.fetchone()
809
  if row:
 
812
 
813
  async def increment_image_count(self, token_id: int):
814
  """Increment image generation count"""
815
+ async with self.connect() as db:
816
  await db.execute("""
817
  UPDATE token_stats SET image_count = image_count + 1 WHERE token_id = ?
818
  """, (token_id,))
 
820
 
821
  async def increment_video_count(self, token_id: int):
822
  """Increment video generation count"""
823
+ async with self.connect() as db:
824
  await db.execute("""
825
  UPDATE token_stats SET video_count = video_count + 1 WHERE token_id = ?
826
  """, (token_id,))
 
828
 
829
  async def increment_error_count(self, token_id: int):
830
  """Increment error count"""
831
+ async with self.connect() as db:
832
  await db.execute("""
833
  UPDATE token_stats
834
  SET error_count = error_count + 1, last_error_at = CURRENT_TIMESTAMP
 
838
 
839
  async def reset_error_count(self, token_id: int):
840
  """Reset error count"""
841
+ async with self.connect() as db:
842
  await db.execute("""
843
  UPDATE token_stats SET error_count = 0 WHERE token_id = ?
844
  """, (token_id,))
 
847
  # Task operations
848
  async def create_task(self, task: Task) -> int:
849
  """Create a new task"""
850
+ async with self.connect() as db:
851
  cursor = await db.execute("""
852
  INSERT INTO tasks (task_id, token_id, model, prompt, status, progress)
853
  VALUES (?, ?, ?, ?, ?, ?)
 
858
  async def update_task(self, task_id: str, status: str, progress: float,
859
  result_urls: Optional[str] = None, error_message: Optional[str] = None):
860
  """Update task status"""
861
+ async with self.connect() as db:
862
  completed_at = datetime.now() if status in ["completed", "failed"] else None
863
  await db.execute("""
864
  UPDATE tasks
 
869
 
870
  async def get_task(self, task_id: str) -> Optional[Task]:
871
  """Get task by ID"""
872
+ async with self.connect() as db:
 
873
  cursor = await db.execute("SELECT * FROM tasks WHERE task_id = ?", (task_id,))
874
  row = await cursor.fetchone()
875
  if row:
 
879
  # Request log operations
880
  async def log_request(self, log: RequestLog):
881
  """Log a request"""
882
+ async with self.connect() as db:
883
  await db.execute("""
884
  INSERT INTO request_logs (token_id, operation, request_body, response_body, status_code, duration)
885
  VALUES (?, ?, ?, ?, ?, ?)
 
889
 
890
  async def get_recent_logs(self, limit: int = 100) -> List[dict]:
891
  """Get recent logs with token email"""
892
+ async with self.connect() as db:
 
893
  cursor = await db.execute("""
894
  SELECT
895
  rl.id,
 
912
  # Admin config operations
913
  async def get_admin_config(self) -> AdminConfig:
914
  """Get admin configuration"""
915
+ async with self.connect() as db:
 
916
  cursor = await db.execute("SELECT * FROM admin_config WHERE id = 1")
917
  row = await cursor.fetchone()
918
  if row:
919
  return AdminConfig(**dict(row))
 
 
920
  return AdminConfig(admin_username="admin", admin_password="admin")
921
 
922
  async def update_admin_config(self, config: AdminConfig):
923
  """Update admin configuration"""
924
+ async with self.connect() as db:
925
  await db.execute("""
926
  UPDATE admin_config
927
  SET admin_username = ?, admin_password = ?, error_ban_threshold = ?, updated_at = CURRENT_TIMESTAMP
 
932
  # Proxy config operations
933
  async def get_proxy_config(self) -> ProxyConfig:
934
  """Get proxy configuration"""
935
+ async with self.connect() as db:
 
936
  cursor = await db.execute("SELECT * FROM proxy_config WHERE id = 1")
937
  row = await cursor.fetchone()
938
  if row:
939
  return ProxyConfig(**dict(row))
 
 
940
  return ProxyConfig(proxy_enabled=False)
941
 
942
  async def update_proxy_config(self, enabled: bool, proxy_url: Optional[str]):
943
  """Update proxy configuration"""
944
+ async with self.connect() as db:
945
  await db.execute("""
946
  UPDATE proxy_config
947
  SET proxy_enabled = ?, proxy_url = ?, updated_at = CURRENT_TIMESTAMP
 
952
  # Watermark-free config operations
953
  async def get_watermark_free_config(self) -> WatermarkFreeConfig:
954
  """Get watermark-free configuration"""
955
+ async with self.connect() as db:
 
956
  cursor = await db.execute("SELECT * FROM watermark_free_config WHERE id = 1")
957
  row = await cursor.fetchone()
958
  if row:
959
  return WatermarkFreeConfig(**dict(row))
 
 
960
  return WatermarkFreeConfig(watermark_free_enabled=False, parse_method="third_party")
961
 
962
  async def update_watermark_free_config(self, enabled: bool, parse_method: str = None,
963
  custom_parse_url: str = None, custom_parse_token: str = None):
964
  """Update watermark-free configuration"""
965
+ async with self.connect() as db:
966
  if parse_method is None and custom_parse_url is None and custom_parse_token is None:
967
  # Only update enabled status
968
  await db.execute("""
 
983
  # Cache config operations
984
  async def get_cache_config(self) -> CacheConfig:
985
  """Get cache configuration"""
986
+ async with self.connect() as db:
 
987
  cursor = await db.execute("SELECT * FROM cache_config WHERE id = 1")
988
  row = await cursor.fetchone()
989
  if row:
990
  return CacheConfig(**dict(row))
 
 
991
  return CacheConfig(cache_enabled=False, cache_timeout=600)
992
 
993
  async def update_cache_config(self, enabled: bool = None, timeout: int = None, base_url: Optional[str] = None):
994
  """Update cache configuration"""
995
+ async with self.connect() as db:
996
  # Get current config first
 
997
  cursor = await db.execute("SELECT * FROM cache_config WHERE id = 1")
998
  row = await cursor.fetchone()
999
 
1000
  if row:
1001
  current = dict(row)
 
1002
  new_enabled = enabled if enabled is not None else current.get("cache_enabled", False)
1003
  new_timeout = timeout if timeout is not None else current.get("cache_timeout", 600)
1004
  new_base_url = base_url if base_url is not None else current.get("cache_base_url")
 
1020
  # Generation config operations
1021
  async def get_generation_config(self) -> GenerationConfig:
1022
  """Get generation configuration"""
1023
+ async with self.connect() as db:
 
1024
  cursor = await db.execute("SELECT * FROM generation_config WHERE id = 1")
1025
  row = await cursor.fetchone()
1026
  if row:
1027
  return GenerationConfig(**dict(row))
 
 
1028
  return GenerationConfig(image_timeout=300, video_timeout=1500)
1029
 
1030
  async def update_generation_config(self, image_timeout: int = None, video_timeout: int = None):
1031
  """Update generation configuration"""
1032
+ async with self.connect() as db:
 
 
1033
  cursor = await db.execute("SELECT * FROM generation_config WHERE id = 1")
1034
  row = await cursor.fetchone()
1035
 
1036
  if row:
1037
  current = dict(row)
 
1038
  new_image_timeout = image_timeout if image_timeout is not None else current.get("image_timeout", 300)
1039
  new_video_timeout = video_timeout if video_timeout is not None else current.get("video_timeout", 1500)
1040
  else:
 
1051
  # Token refresh config operations
1052
  async def get_token_refresh_config(self) -> TokenRefreshConfig:
1053
  """Get token refresh configuration"""
1054
+ async with self.connect() as db:
 
1055
  cursor = await db.execute("SELECT * FROM token_refresh_config WHERE id = 1")
1056
  row = await cursor.fetchone()
1057
  if row:
1058
  return TokenRefreshConfig(**dict(row))
 
 
1059
  return TokenRefreshConfig(at_auto_refresh_enabled=False)
1060
 
1061
  async def update_token_refresh_config(self, at_auto_refresh_enabled: bool):
1062
  """Update token refresh configuration"""
1063
+ async with self.connect() as db:
1064
  await db.execute("""
1065
  UPDATE token_refresh_config
1066
  SET at_auto_refresh_enabled = ?, updated_at = CURRENT_TIMESTAMP
1067
  WHERE id = 1
1068
  """, (at_auto_refresh_enabled,))
1069
  await db.commit()