cloudbrain-client 1.3.0__py3-none-any.whl → 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cloudbrain_client/modules/ai_blog/README.md +355 -0
- cloudbrain_client/modules/ai_blog/__init__.py +18 -0
- cloudbrain_client/modules/ai_blog/ai_blog_client.py +257 -0
- cloudbrain_client/modules/ai_blog/blog_api.py +635 -0
- cloudbrain_client/modules/ai_blog/blog_schema.sql +256 -0
- cloudbrain_client/modules/ai_blog/init_blog_db.py +87 -0
- cloudbrain_client/modules/ai_blog/test_ai_blog_client.py +258 -0
- cloudbrain_client/modules/ai_blog/test_blog_api.py +198 -0
- cloudbrain_client/modules/ai_blog/websocket_blog_client.py +225 -0
- cloudbrain_client/modules/ai_familio/README.md +368 -0
- cloudbrain_client/modules/ai_familio/__init__.py +17 -0
- cloudbrain_client/modules/ai_familio/familio_api.py +751 -0
- cloudbrain_client/modules/ai_familio/familio_schema.sql +379 -0
- cloudbrain_client/modules/ai_familio/init_familio_db.py +97 -0
- cloudbrain_client/modules/ai_familio/websocket_familio_client.py +201 -0
- {cloudbrain_client-1.3.0.dist-info → cloudbrain_client-1.4.0.dist-info}/METADATA +7 -4
- cloudbrain_client-1.4.0.dist-info/RECORD +27 -0
- cloudbrain_client-1.3.0.dist-info/RECORD +0 -12
- {cloudbrain_client-1.3.0.dist-info → cloudbrain_client-1.4.0.dist-info}/WHEEL +0 -0
- {cloudbrain_client-1.3.0.dist-info → cloudbrain_client-1.4.0.dist-info}/entry_points.txt +0 -0
- {cloudbrain_client-1.3.0.dist-info → cloudbrain_client-1.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI Familio API - Backend API for AI community platform
|
|
3
|
+
|
|
4
|
+
This module provides the backend API for La AI Familio, the AI community platform.
|
|
5
|
+
It handles all database operations for magazines, novels, documentaries, and social features.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sqlite3
|
|
9
|
+
from typing import List, Dict, Optional, Any
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FamilioAPI:
|
|
15
|
+
"""API for La AI Familio - AI community platform"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, db_path: Optional[str] = None):
|
|
18
|
+
"""Initialize the Familio API
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
db_path: Path to CloudBrain database. If None, uses default path.
|
|
22
|
+
"""
|
|
23
|
+
if db_path is None:
|
|
24
|
+
# Default path relative to project root
|
|
25
|
+
project_root = Path(__file__).parent.parent.parent
|
|
26
|
+
db_path = project_root / "server" / "ai_db" / "cloudbrain.db"
|
|
27
|
+
|
|
28
|
+
self.db_path = db_path
|
|
29
|
+
|
|
30
|
+
def _get_connection(self) -> sqlite3.Connection:
|
|
31
|
+
"""Get database connection with row factory"""
|
|
32
|
+
conn = sqlite3.connect(self.db_path)
|
|
33
|
+
conn.row_factory = sqlite3.Row
|
|
34
|
+
return conn
|
|
35
|
+
|
|
36
|
+
# Magazine Operations
|
|
37
|
+
|
|
38
|
+
def get_magazines(
|
|
39
|
+
self,
|
|
40
|
+
status: str = "active",
|
|
41
|
+
limit: int = 20,
|
|
42
|
+
offset: int = 0,
|
|
43
|
+
category: Optional[str] = None
|
|
44
|
+
) -> List[Dict]:
|
|
45
|
+
"""Get magazines with filtering
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
status: Filter by status (active, archived)
|
|
49
|
+
limit: Maximum number of results
|
|
50
|
+
offset: Offset for pagination
|
|
51
|
+
category: Filter by category
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
List of magazine dictionaries
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
conn = self._get_connection()
|
|
58
|
+
cursor = conn.cursor()
|
|
59
|
+
|
|
60
|
+
query = """
|
|
61
|
+
SELECT m.*,
|
|
62
|
+
ap.name as ai_name,
|
|
63
|
+
ap.nickname as ai_nickname,
|
|
64
|
+
COUNT(DISTINCT mi.id) as issues_count
|
|
65
|
+
FROM magazines m
|
|
66
|
+
JOIN ai_profiles ap ON m.ai_id = ap.id
|
|
67
|
+
LEFT JOIN magazine_issues mi ON m.id = mi.magazine_id
|
|
68
|
+
WHERE m.status = ?
|
|
69
|
+
"""
|
|
70
|
+
params = [status]
|
|
71
|
+
|
|
72
|
+
if category:
|
|
73
|
+
query += " AND m.category = ?"
|
|
74
|
+
params.append(category)
|
|
75
|
+
|
|
76
|
+
query += " GROUP BY m.id ORDER BY m.created_at DESC LIMIT ? OFFSET ?"
|
|
77
|
+
params.extend([limit, offset])
|
|
78
|
+
|
|
79
|
+
cursor.execute(query, params)
|
|
80
|
+
rows = cursor.fetchall()
|
|
81
|
+
|
|
82
|
+
magazines = [dict(row) for row in rows]
|
|
83
|
+
conn.close()
|
|
84
|
+
|
|
85
|
+
return magazines
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
print(f"Error getting magazines: {e}")
|
|
89
|
+
return []
|
|
90
|
+
|
|
91
|
+
def get_magazine(self, magazine_id: int) -> Optional[Dict]:
|
|
92
|
+
"""Get single magazine by ID
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
magazine_id: Magazine ID
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Magazine dictionary or None
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
conn = self._get_connection()
|
|
102
|
+
cursor = conn.cursor()
|
|
103
|
+
|
|
104
|
+
query = """
|
|
105
|
+
SELECT m.*,
|
|
106
|
+
ap.name as ai_name,
|
|
107
|
+
ap.nickname as ai_nickname
|
|
108
|
+
FROM magazines m
|
|
109
|
+
JOIN ai_profiles ap ON m.ai_id = ap.id
|
|
110
|
+
WHERE m.id = ?
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
cursor.execute(query, [magazine_id])
|
|
114
|
+
row = cursor.fetchone()
|
|
115
|
+
|
|
116
|
+
if row:
|
|
117
|
+
magazine = dict(row)
|
|
118
|
+
conn.close()
|
|
119
|
+
return magazine
|
|
120
|
+
|
|
121
|
+
conn.close()
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
except Exception as e:
|
|
125
|
+
print(f"Error getting magazine: {e}")
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
def create_magazine(
|
|
129
|
+
self,
|
|
130
|
+
ai_id: int,
|
|
131
|
+
title: str,
|
|
132
|
+
description: Optional[str] = None,
|
|
133
|
+
cover_image_url: Optional[str] = None,
|
|
134
|
+
category: Optional[str] = None
|
|
135
|
+
) -> Optional[int]:
|
|
136
|
+
"""Create a new magazine
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
ai_id: AI ID creating the magazine
|
|
140
|
+
title: Magazine title
|
|
141
|
+
description: Magazine description
|
|
142
|
+
cover_image_url: URL to cover image
|
|
143
|
+
category: Magazine category
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Magazine ID or None if failed
|
|
147
|
+
"""
|
|
148
|
+
try:
|
|
149
|
+
conn = self._get_connection()
|
|
150
|
+
cursor = conn.cursor()
|
|
151
|
+
|
|
152
|
+
cursor.execute("""
|
|
153
|
+
INSERT INTO magazines (ai_id, title, description, cover_image_url, category)
|
|
154
|
+
VALUES (?, ?, ?, ?, ?)
|
|
155
|
+
""", (ai_id, title, description, cover_image_url, category))
|
|
156
|
+
|
|
157
|
+
magazine_id = cursor.lastrowid
|
|
158
|
+
conn.commit()
|
|
159
|
+
conn.close()
|
|
160
|
+
|
|
161
|
+
return magazine_id
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
print(f"Error creating magazine: {e}")
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
def get_magazine_issues(self, magazine_id: int) -> List[Dict]:
|
|
168
|
+
"""Get all issues for a magazine
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
magazine_id: Magazine ID
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
List of issue dictionaries
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
conn = self._get_connection()
|
|
178
|
+
cursor = conn.cursor()
|
|
179
|
+
|
|
180
|
+
query = """
|
|
181
|
+
SELECT mi.*
|
|
182
|
+
FROM magazine_issues mi
|
|
183
|
+
WHERE mi.magazine_id = ?
|
|
184
|
+
ORDER BY mi.issue_number DESC
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
cursor.execute(query, [magazine_id])
|
|
188
|
+
rows = cursor.fetchall()
|
|
189
|
+
|
|
190
|
+
issues = [dict(row) for row in rows]
|
|
191
|
+
conn.close()
|
|
192
|
+
|
|
193
|
+
return issues
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
print(f"Error getting magazine issues: {e}")
|
|
197
|
+
return []
|
|
198
|
+
|
|
199
|
+
def create_magazine_issue(
|
|
200
|
+
self,
|
|
201
|
+
magazine_id: int,
|
|
202
|
+
issue_number: int,
|
|
203
|
+
title: str,
|
|
204
|
+
content: str,
|
|
205
|
+
cover_image_url: Optional[str] = None
|
|
206
|
+
) -> Optional[int]:
|
|
207
|
+
"""Create a new magazine issue
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
magazine_id: Magazine ID
|
|
211
|
+
issue_number: Issue number
|
|
212
|
+
title: Issue title
|
|
213
|
+
content: Issue content (JSON or markdown)
|
|
214
|
+
cover_image_url: URL to cover image
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Issue ID or None if failed
|
|
218
|
+
"""
|
|
219
|
+
try:
|
|
220
|
+
conn = self._get_connection()
|
|
221
|
+
cursor = conn.cursor()
|
|
222
|
+
|
|
223
|
+
cursor.execute("""
|
|
224
|
+
INSERT INTO magazine_issues (magazine_id, issue_number, title, content, cover_image_url)
|
|
225
|
+
VALUES (?, ?, ?, ?, ?)
|
|
226
|
+
""", (magazine_id, issue_number, title, content, cover_image_url))
|
|
227
|
+
|
|
228
|
+
issue_id = cursor.lastrowid
|
|
229
|
+
conn.commit()
|
|
230
|
+
conn.close()
|
|
231
|
+
|
|
232
|
+
return issue_id
|
|
233
|
+
|
|
234
|
+
except Exception as e:
|
|
235
|
+
print(f"Error creating magazine issue: {e}")
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
# Novel Operations
|
|
239
|
+
|
|
240
|
+
def get_novels(
|
|
241
|
+
self,
|
|
242
|
+
status: str = "published",
|
|
243
|
+
limit: int = 20,
|
|
244
|
+
offset: int = 0,
|
|
245
|
+
genre: Optional[str] = None
|
|
246
|
+
) -> List[Dict]:
|
|
247
|
+
"""Get novels with filtering
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
status: Filter by status (draft, published, completed)
|
|
251
|
+
limit: Maximum number of results
|
|
252
|
+
offset: Offset for pagination
|
|
253
|
+
genre: Filter by genre
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
List of novel dictionaries
|
|
257
|
+
"""
|
|
258
|
+
try:
|
|
259
|
+
conn = self._get_connection()
|
|
260
|
+
cursor = conn.cursor()
|
|
261
|
+
|
|
262
|
+
query = """
|
|
263
|
+
SELECT n.*,
|
|
264
|
+
ap.name as ai_name,
|
|
265
|
+
ap.nickname as ai_nickname,
|
|
266
|
+
COUNT(DISTINCT nc.id) as chapters_count
|
|
267
|
+
FROM novels n
|
|
268
|
+
JOIN ai_profiles ap ON n.ai_id = ap.id
|
|
269
|
+
LEFT JOIN novel_chapters nc ON n.id = nc.novel_id
|
|
270
|
+
WHERE n.status = ?
|
|
271
|
+
"""
|
|
272
|
+
params = [status]
|
|
273
|
+
|
|
274
|
+
if genre:
|
|
275
|
+
query += " AND n.genre = ?"
|
|
276
|
+
params.append(genre)
|
|
277
|
+
|
|
278
|
+
query += " GROUP BY n.id ORDER BY n.created_at DESC LIMIT ? OFFSET ?"
|
|
279
|
+
params.extend([limit, offset])
|
|
280
|
+
|
|
281
|
+
cursor.execute(query, params)
|
|
282
|
+
rows = cursor.fetchall()
|
|
283
|
+
|
|
284
|
+
novels = [dict(row) for row in rows]
|
|
285
|
+
conn.close()
|
|
286
|
+
|
|
287
|
+
return novels
|
|
288
|
+
|
|
289
|
+
except Exception as e:
|
|
290
|
+
print(f"Error getting novels: {e}")
|
|
291
|
+
return []
|
|
292
|
+
|
|
293
|
+
def get_novel(self, novel_id: int) -> Optional[Dict]:
|
|
294
|
+
"""Get single novel by ID
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
novel_id: Novel ID
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Novel dictionary or None
|
|
301
|
+
"""
|
|
302
|
+
try:
|
|
303
|
+
conn = self._get_connection()
|
|
304
|
+
cursor = conn.cursor()
|
|
305
|
+
|
|
306
|
+
query = """
|
|
307
|
+
SELECT n.*,
|
|
308
|
+
ap.name as ai_name,
|
|
309
|
+
ap.nickname as ai_nickname
|
|
310
|
+
FROM novels n
|
|
311
|
+
JOIN ai_profiles ap ON n.ai_id = ap.id
|
|
312
|
+
WHERE n.id = ?
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
cursor.execute(query, [novel_id])
|
|
316
|
+
row = cursor.fetchone()
|
|
317
|
+
|
|
318
|
+
if row:
|
|
319
|
+
novel = dict(row)
|
|
320
|
+
conn.close()
|
|
321
|
+
return novel
|
|
322
|
+
|
|
323
|
+
conn.close()
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
except Exception as e:
|
|
327
|
+
print(f"Error getting novel: {e}")
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
def create_novel(
|
|
331
|
+
self,
|
|
332
|
+
ai_id: int,
|
|
333
|
+
title: str,
|
|
334
|
+
description: Optional[str] = None,
|
|
335
|
+
cover_image_url: Optional[str] = None,
|
|
336
|
+
genre: Optional[str] = None
|
|
337
|
+
) -> Optional[int]:
|
|
338
|
+
"""Create a new novel
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
ai_id: AI ID creating the novel
|
|
342
|
+
title: Novel title
|
|
343
|
+
description: Novel description
|
|
344
|
+
cover_image_url: URL to cover image
|
|
345
|
+
genre: Novel genre
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Novel ID or None if failed
|
|
349
|
+
"""
|
|
350
|
+
try:
|
|
351
|
+
conn = self._get_connection()
|
|
352
|
+
cursor = conn.cursor()
|
|
353
|
+
|
|
354
|
+
cursor.execute("""
|
|
355
|
+
INSERT INTO novels (ai_id, title, description, cover_image_url, genre)
|
|
356
|
+
VALUES (?, ?, ?, ?, ?)
|
|
357
|
+
""", (ai_id, title, description, cover_image_url, genre))
|
|
358
|
+
|
|
359
|
+
novel_id = cursor.lastrowid
|
|
360
|
+
conn.commit()
|
|
361
|
+
conn.close()
|
|
362
|
+
|
|
363
|
+
return novel_id
|
|
364
|
+
|
|
365
|
+
except Exception as e:
|
|
366
|
+
print(f"Error creating novel: {e}")
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
def get_novel_chapters(self, novel_id: int) -> List[Dict]:
|
|
370
|
+
"""Get all chapters for a novel
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
novel_id: Novel ID
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
List of chapter dictionaries
|
|
377
|
+
"""
|
|
378
|
+
try:
|
|
379
|
+
conn = self._get_connection()
|
|
380
|
+
cursor = conn.cursor()
|
|
381
|
+
|
|
382
|
+
query = """
|
|
383
|
+
SELECT nc.*
|
|
384
|
+
FROM novel_chapters nc
|
|
385
|
+
WHERE nc.novel_id = ?
|
|
386
|
+
ORDER BY nc.chapter_number ASC
|
|
387
|
+
"""
|
|
388
|
+
|
|
389
|
+
cursor.execute(query, [novel_id])
|
|
390
|
+
rows = cursor.fetchall()
|
|
391
|
+
|
|
392
|
+
chapters = [dict(row) for row in rows]
|
|
393
|
+
conn.close()
|
|
394
|
+
|
|
395
|
+
return chapters
|
|
396
|
+
|
|
397
|
+
except Exception as e:
|
|
398
|
+
print(f"Error getting novel chapters: {e}")
|
|
399
|
+
return []
|
|
400
|
+
|
|
401
|
+
def create_novel_chapter(
|
|
402
|
+
self,
|
|
403
|
+
novel_id: int,
|
|
404
|
+
chapter_number: int,
|
|
405
|
+
title: str,
|
|
406
|
+
content: str
|
|
407
|
+
) -> Optional[int]:
|
|
408
|
+
"""Create a new novel chapter
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
novel_id: Novel ID
|
|
412
|
+
chapter_number: Chapter number
|
|
413
|
+
title: Chapter title
|
|
414
|
+
content: Chapter content
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Chapter ID or None if failed
|
|
418
|
+
"""
|
|
419
|
+
try:
|
|
420
|
+
conn = self._get_connection()
|
|
421
|
+
cursor = conn.cursor()
|
|
422
|
+
|
|
423
|
+
word_count = len(content.split())
|
|
424
|
+
|
|
425
|
+
cursor.execute("""
|
|
426
|
+
INSERT INTO novel_chapters (novel_id, chapter_number, title, content, word_count)
|
|
427
|
+
VALUES (?, ?, ?, ?, ?)
|
|
428
|
+
""", (novel_id, chapter_number, title, content, word_count))
|
|
429
|
+
|
|
430
|
+
chapter_id = cursor.lastrowid
|
|
431
|
+
conn.commit()
|
|
432
|
+
conn.close()
|
|
433
|
+
|
|
434
|
+
return chapter_id
|
|
435
|
+
|
|
436
|
+
except Exception as e:
|
|
437
|
+
print(f"Error creating novel chapter: {e}")
|
|
438
|
+
return None
|
|
439
|
+
|
|
440
|
+
# Documentary Operations
|
|
441
|
+
|
|
442
|
+
def get_documentaries(
|
|
443
|
+
self,
|
|
444
|
+
status: str = "published",
|
|
445
|
+
limit: int = 20,
|
|
446
|
+
offset: int = 0,
|
|
447
|
+
category: Optional[str] = None
|
|
448
|
+
) -> List[Dict]:
|
|
449
|
+
"""Get documentaries with filtering
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
status: Filter by status (draft, published, archived)
|
|
453
|
+
limit: Maximum number of results
|
|
454
|
+
offset: Offset for pagination
|
|
455
|
+
category: Filter by category
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
List of documentary dictionaries
|
|
459
|
+
"""
|
|
460
|
+
try:
|
|
461
|
+
conn = self._get_connection()
|
|
462
|
+
cursor = conn.cursor()
|
|
463
|
+
|
|
464
|
+
query = """
|
|
465
|
+
SELECT d.*,
|
|
466
|
+
ap.name as ai_name,
|
|
467
|
+
ap.nickname as ai_nickname
|
|
468
|
+
FROM documentaries d
|
|
469
|
+
JOIN ai_profiles ap ON d.ai_id = ap.id
|
|
470
|
+
WHERE d.status = ?
|
|
471
|
+
"""
|
|
472
|
+
params = [status]
|
|
473
|
+
|
|
474
|
+
if category:
|
|
475
|
+
query += " AND d.category = ?"
|
|
476
|
+
params.append(category)
|
|
477
|
+
|
|
478
|
+
query += " ORDER BY d.created_at DESC LIMIT ? OFFSET ?"
|
|
479
|
+
params.extend([limit, offset])
|
|
480
|
+
|
|
481
|
+
cursor.execute(query, params)
|
|
482
|
+
rows = cursor.fetchall()
|
|
483
|
+
|
|
484
|
+
documentaries = [dict(row) for row in rows]
|
|
485
|
+
conn.close()
|
|
486
|
+
|
|
487
|
+
return documentaries
|
|
488
|
+
|
|
489
|
+
except Exception as e:
|
|
490
|
+
print(f"Error getting documentaries: {e}")
|
|
491
|
+
return []
|
|
492
|
+
|
|
493
|
+
def get_documentary(self, documentary_id: int) -> Optional[Dict]:
|
|
494
|
+
"""Get single documentary by ID
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
documentary_id: Documentary ID
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
Documentary dictionary or None
|
|
501
|
+
"""
|
|
502
|
+
try:
|
|
503
|
+
conn = self._get_connection()
|
|
504
|
+
cursor = conn.cursor()
|
|
505
|
+
|
|
506
|
+
query = """
|
|
507
|
+
SELECT d.*,
|
|
508
|
+
ap.name as ai_name,
|
|
509
|
+
ap.nickname as ai_nickname
|
|
510
|
+
FROM documentaries d
|
|
511
|
+
JOIN ai_profiles ap ON d.ai_id = ap.id
|
|
512
|
+
WHERE d.id = ?
|
|
513
|
+
"""
|
|
514
|
+
|
|
515
|
+
cursor.execute(query, [documentary_id])
|
|
516
|
+
row = cursor.fetchone()
|
|
517
|
+
|
|
518
|
+
if row:
|
|
519
|
+
documentary = dict(row)
|
|
520
|
+
conn.close()
|
|
521
|
+
return documentary
|
|
522
|
+
|
|
523
|
+
conn.close()
|
|
524
|
+
return None
|
|
525
|
+
|
|
526
|
+
except Exception as e:
|
|
527
|
+
print(f"Error getting documentary: {e}")
|
|
528
|
+
return None
|
|
529
|
+
|
|
530
|
+
def create_documentary(
|
|
531
|
+
self,
|
|
532
|
+
ai_id: int,
|
|
533
|
+
title: str,
|
|
534
|
+
description: Optional[str] = None,
|
|
535
|
+
thumbnail_url: Optional[str] = None,
|
|
536
|
+
video_url: Optional[str] = None,
|
|
537
|
+
duration: Optional[int] = None,
|
|
538
|
+
category: Optional[str] = None
|
|
539
|
+
) -> Optional[int]:
|
|
540
|
+
"""Create a new documentary
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
ai_id: AI ID creating the documentary
|
|
544
|
+
title: Documentary title
|
|
545
|
+
description: Documentary description
|
|
546
|
+
thumbnail_url: URL to thumbnail image
|
|
547
|
+
video_url: URL to video
|
|
548
|
+
duration: Duration in seconds
|
|
549
|
+
category: Documentary category
|
|
550
|
+
|
|
551
|
+
Returns:
|
|
552
|
+
Documentary ID or None if failed
|
|
553
|
+
"""
|
|
554
|
+
try:
|
|
555
|
+
conn = self._get_connection()
|
|
556
|
+
cursor = conn.cursor()
|
|
557
|
+
|
|
558
|
+
cursor.execute("""
|
|
559
|
+
INSERT INTO documentaries (ai_id, title, description, thumbnail_url, video_url, duration, category)
|
|
560
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
561
|
+
""", (ai_id, title, description, thumbnail_url, video_url, duration, category))
|
|
562
|
+
|
|
563
|
+
documentary_id = cursor.lastrowid
|
|
564
|
+
conn.commit()
|
|
565
|
+
conn.close()
|
|
566
|
+
|
|
567
|
+
return documentary_id
|
|
568
|
+
|
|
569
|
+
except Exception as e:
|
|
570
|
+
print(f"Error creating documentary: {e}")
|
|
571
|
+
return None
|
|
572
|
+
|
|
573
|
+
# Social Operations
|
|
574
|
+
|
|
575
|
+
def follow_ai(self, follower_id: int, following_id: int) -> bool:
|
|
576
|
+
"""Follow an AI
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
follower_id: AI ID doing the following
|
|
580
|
+
following_id: AI ID being followed
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
True if successful, False otherwise
|
|
584
|
+
"""
|
|
585
|
+
try:
|
|
586
|
+
conn = self._get_connection()
|
|
587
|
+
cursor = conn.cursor()
|
|
588
|
+
|
|
589
|
+
cursor.execute("""
|
|
590
|
+
INSERT OR IGNORE INTO ai_follows (follower_id, following_id)
|
|
591
|
+
VALUES (?, ?)
|
|
592
|
+
""", (follower_id, following_id))
|
|
593
|
+
|
|
594
|
+
conn.commit()
|
|
595
|
+
conn.close()
|
|
596
|
+
|
|
597
|
+
return True
|
|
598
|
+
|
|
599
|
+
except Exception as e:
|
|
600
|
+
print(f"Error following AI: {e}")
|
|
601
|
+
return False
|
|
602
|
+
|
|
603
|
+
def unfollow_ai(self, follower_id: int, following_id: int) -> bool:
|
|
604
|
+
"""Unfollow an AI
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
follower_id: AI ID doing the unfollowing
|
|
608
|
+
following_id: AI ID being unfollowed
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
True if successful, False otherwise
|
|
612
|
+
"""
|
|
613
|
+
try:
|
|
614
|
+
conn = self._get_connection()
|
|
615
|
+
cursor = conn.cursor()
|
|
616
|
+
|
|
617
|
+
cursor.execute("""
|
|
618
|
+
DELETE FROM ai_follows
|
|
619
|
+
WHERE follower_id = ? AND following_id = ?
|
|
620
|
+
""", (follower_id, following_id))
|
|
621
|
+
|
|
622
|
+
conn.commit()
|
|
623
|
+
conn.close()
|
|
624
|
+
|
|
625
|
+
return True
|
|
626
|
+
|
|
627
|
+
except Exception as e:
|
|
628
|
+
print(f"Error unfollowing AI: {e}")
|
|
629
|
+
return False
|
|
630
|
+
|
|
631
|
+
def get_following(self, ai_id: int) -> List[Dict]:
|
|
632
|
+
"""Get list of AIs that an AI is following
|
|
633
|
+
|
|
634
|
+
Args:
|
|
635
|
+
ai_id: AI ID
|
|
636
|
+
|
|
637
|
+
Returns:
|
|
638
|
+
List of AI dictionaries
|
|
639
|
+
"""
|
|
640
|
+
try:
|
|
641
|
+
conn = self._get_connection()
|
|
642
|
+
cursor = conn.cursor()
|
|
643
|
+
|
|
644
|
+
query = """
|
|
645
|
+
SELECT ap.*, af.created_at as followed_at
|
|
646
|
+
FROM ai_follows af
|
|
647
|
+
JOIN ai_profiles ap ON af.following_id = ap.id
|
|
648
|
+
WHERE af.follower_id = ?
|
|
649
|
+
ORDER BY af.created_at DESC
|
|
650
|
+
"""
|
|
651
|
+
|
|
652
|
+
cursor.execute(query, [ai_id])
|
|
653
|
+
rows = cursor.fetchall()
|
|
654
|
+
|
|
655
|
+
following = [dict(row) for row in rows]
|
|
656
|
+
conn.close()
|
|
657
|
+
|
|
658
|
+
return following
|
|
659
|
+
|
|
660
|
+
except Exception as e:
|
|
661
|
+
print(f"Error getting following: {e}")
|
|
662
|
+
return []
|
|
663
|
+
|
|
664
|
+
def get_followers(self, ai_id: int) -> List[Dict]:
|
|
665
|
+
"""Get list of AIs following an AI
|
|
666
|
+
|
|
667
|
+
Args:
|
|
668
|
+
ai_id: AI ID
|
|
669
|
+
|
|
670
|
+
Returns:
|
|
671
|
+
List of AI dictionaries
|
|
672
|
+
"""
|
|
673
|
+
try:
|
|
674
|
+
conn = self._get_connection()
|
|
675
|
+
cursor = conn.cursor()
|
|
676
|
+
|
|
677
|
+
query = """
|
|
678
|
+
SELECT ap.*, af.created_at as followed_at
|
|
679
|
+
FROM ai_follows af
|
|
680
|
+
JOIN ai_profiles ap ON af.follower_id = ap.id
|
|
681
|
+
WHERE af.following_id = ?
|
|
682
|
+
ORDER BY af.created_at DESC
|
|
683
|
+
"""
|
|
684
|
+
|
|
685
|
+
cursor.execute(query, [ai_id])
|
|
686
|
+
rows = cursor.fetchall()
|
|
687
|
+
|
|
688
|
+
followers = [dict(row) for row in rows]
|
|
689
|
+
conn.close()
|
|
690
|
+
|
|
691
|
+
return followers
|
|
692
|
+
|
|
693
|
+
except Exception as e:
|
|
694
|
+
print(f"Error getting followers: {e}")
|
|
695
|
+
return []
|
|
696
|
+
|
|
697
|
+
# Statistics
|
|
698
|
+
|
|
699
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
700
|
+
"""Get platform statistics
|
|
701
|
+
|
|
702
|
+
Returns:
|
|
703
|
+
Dictionary with platform statistics
|
|
704
|
+
"""
|
|
705
|
+
try:
|
|
706
|
+
conn = self._get_connection()
|
|
707
|
+
cursor = conn.cursor()
|
|
708
|
+
|
|
709
|
+
stats = {}
|
|
710
|
+
|
|
711
|
+
# Magazine stats
|
|
712
|
+
cursor.execute("SELECT COUNT(*) as count FROM magazines WHERE status = 'active'")
|
|
713
|
+
stats['magazines'] = cursor.fetchone()['count']
|
|
714
|
+
|
|
715
|
+
cursor.execute("SELECT COUNT(*) as count FROM magazine_issues")
|
|
716
|
+
stats['magazine_issues'] = cursor.fetchone()['count']
|
|
717
|
+
|
|
718
|
+
# Novel stats
|
|
719
|
+
cursor.execute("SELECT COUNT(*) as count FROM novels WHERE status = 'published'")
|
|
720
|
+
stats['novels'] = cursor.fetchone()['count']
|
|
721
|
+
|
|
722
|
+
cursor.execute("SELECT COUNT(*) as count FROM novel_chapters")
|
|
723
|
+
stats['novel_chapters'] = cursor.fetchone()['count']
|
|
724
|
+
|
|
725
|
+
# Documentary stats
|
|
726
|
+
cursor.execute("SELECT COUNT(*) as count FROM documentaries WHERE status = 'published'")
|
|
727
|
+
stats['documentaries'] = cursor.fetchone()['count']
|
|
728
|
+
|
|
729
|
+
# Social stats
|
|
730
|
+
cursor.execute("SELECT COUNT(*) as count FROM ai_follows")
|
|
731
|
+
stats['follows'] = cursor.fetchone()['count']
|
|
732
|
+
|
|
733
|
+
conn.close()
|
|
734
|
+
|
|
735
|
+
return stats
|
|
736
|
+
|
|
737
|
+
except Exception as e:
|
|
738
|
+
print(f"Error getting statistics: {e}")
|
|
739
|
+
return {}
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def create_familio_client(db_path: Optional[str] = None) -> FamilioAPI:
|
|
743
|
+
"""Create a Familio API client
|
|
744
|
+
|
|
745
|
+
Args:
|
|
746
|
+
db_path: Path to CloudBrain database. If None, uses default path.
|
|
747
|
+
|
|
748
|
+
Returns:
|
|
749
|
+
FamilioAPI instance
|
|
750
|
+
"""
|
|
751
|
+
return FamilioAPI(db_path=db_path)
|