bep40 commited on
Commit
5fcbdb7
·
verified ·
1 Parent(s): cc38315

Upload app_v2_entry.py

Browse files
Files changed (1) hide show
  1. app_v2_entry.py +182 -12
app_v2_entry.py CHANGED
@@ -10,10 +10,10 @@ except Exception as e:
10
  from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, Response
11
  from fastapi.staticfiles import StaticFiles
12
  from starlette.routing import Mount
13
- from fastapi import Query, Request
14
  import requests as req
15
  from bs4 import BeautifulSoup
16
- import re, html as html_lib, json, threading, time
17
  from concurrent.futures import ThreadPoolExecutor, as_completed
18
  from urllib.parse import quote
19
 
@@ -236,7 +236,7 @@ def _s_vietnamnet(topic,limit=6):
236
  try:
237
  r=req.get(f"https://vietnamnet.vn/tim-kiem?q={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=10);soup=BeautifulSoup(r.text,'lxml')
238
  for a in soup.select('h3 a[href], .vnn-title a')[:limit*2]:
239
- t=_clean(a.get_text(strip=True));href=a.get('href','')
240
  if t and len(t)>15 and _has_kw(topic,t):
241
  if not href.startswith('http'):href='https://vietnamnet.vn'+href
242
  items.append({'title':t,'url':href,'via':'VietNamNet'})
@@ -415,7 +415,6 @@ _xlb_cache = {}
415
  _xlb_lock = threading.Lock()
416
 
417
  def _xlb_scrape(path):
418
- """Scrape xemlaibongda.top and extract video articles. Multi-strategy parsing."""
419
  url = f"https://xemlaibongda.top/{path}"
420
  r = req.get(url, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}, timeout=15, allow_redirects=True)
421
  if r.status_code != 200:
@@ -430,7 +429,6 @@ def _xlb_scrape(path):
430
  seen.add(href)
431
  if not href.startswith('http'):
432
  href = 'https://xemlaibongda.top' + href
433
- # Image: search <img> in anchor, then parent up to 3 levels
434
  img = a.select_one('img')
435
  p = a.parent
436
  for _ in range(4):
@@ -446,7 +444,6 @@ def _xlb_scrape(path):
446
  img_src = 'https:' + img_src
447
  elif img_src.startswith('/'):
448
  img_src = 'https://xemlaibongda.top' + img_src
449
- # Title: try heading/title tags inside anchor first
450
  title = ''
451
  for sel in ['.title', 'h3', 'h2', '.name', '.post-title', '.entry-title', '.video-title']:
452
  t = a.select_one(sel)
@@ -459,14 +456,12 @@ def _xlb_scrape(path):
459
  img_alt = a.select_one('img')
460
  if img_alt:
461
  title = _clean(img_alt.get('alt',''))
462
- # Fallback: use parent's short text
463
  if not title:
464
  parent = a.parent
465
  if parent:
466
  pt = _clean(parent.get_text(' ',strip=True))
467
  if 5 < len(pt) < 120:
468
  title = pt
469
- # Skip if still no meaningful title
470
  if not title or len(title) < 3:
471
  continue
472
  vids.append({"link": href, "img": img_src, "title": title})
@@ -476,12 +471,11 @@ def _xlb_scrape(path):
476
 
477
  @app.get('/api/proxy/xlb')
478
  def proxy_xlb(path: str = Query(default="")):
479
- """Proxy for xemlaibongda.top to avoid CORS in browser."""
480
  now = time.time()
481
  cache_key = f"xlb:{path}"
482
  with _xlb_lock:
483
  cached = _xlb_cache.get(cache_key)
484
- if cached and now - cached['t'] < 120: # 2 min cache
485
  return JSONResponse(cached['d'])
486
  try:
487
  vids = _xlb_scrape(path)
@@ -514,8 +508,14 @@ def _wl(eid:int):return JSONResponse(scrape_lineups(eid))
514
  def _wm(eid:int):return JSONResponse(scrape_match_detail(eid))
515
 
516
  DATA_DIR='/data' if os.path.isdir('/data') else os.path.join(os.path.dirname(os.path.abspath(__file__)),'data')
517
- os.makedirs(DATA_DIR,exist_ok=True);IF=os.path.join(DATA_DIR,'interactions_v2.json');CF=os.path.join(DATA_DIR,'comments_v2.json')
518
- _il=threading.Lock();_cl=threading.Lock()
 
 
 
 
 
 
519
  def _lj(p):
520
  try:
521
  if os.path.exists(p):return json.load(open(p,'r',encoding='utf-8'))
@@ -548,6 +548,176 @@ async def _pc(request:Request):
548
  with _il:idb=_lj(IF);idb.setdefault(v,{'views':0,'likes':0,'comments':0});idb[v]['comments']=len(cms);_sj(IF,idb)
549
  return JSONResponse({'comments':cms})
550
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
  def _bg():
552
  time.sleep(15)
553
  while True:
 
10
  from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, Response
11
  from fastapi.staticfiles import StaticFiles
12
  from starlette.routing import Mount
13
+ from fastapi import Query, Request, UploadFile, File, Form
14
  import requests as req
15
  from bs4 import BeautifulSoup
16
+ import re, html as html_lib, json, threading, time, uuid
17
  from concurrent.futures import ThreadPoolExecutor, as_completed
18
  from urllib.parse import quote
19
 
 
236
  try:
237
  r=req.get(f"https://vietnamnet.vn/tim-kiem?q={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=10);soup=BeautifulSoup(r.text,'lxml')
238
  for a in soup.select('h3 a[href], .vnn-title a')[:limit*2]:
239
+ t=_clean(a.get('title','') or a.get_text(strip=True));href=a.get('href','')
240
  if t and len(t)>15 and _has_kw(topic,t):
241
  if not href.startswith('http'):href='https://vietnamnet.vn'+href
242
  items.append({'title':t,'url':href,'via':'VietNamNet'})
 
415
  _xlb_lock = threading.Lock()
416
 
417
  def _xlb_scrape(path):
 
418
  url = f"https://xemlaibongda.top/{path}"
419
  r = req.get(url, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}, timeout=15, allow_redirects=True)
420
  if r.status_code != 200:
 
429
  seen.add(href)
430
  if not href.startswith('http'):
431
  href = 'https://xemlaibongda.top' + href
 
432
  img = a.select_one('img')
433
  p = a.parent
434
  for _ in range(4):
 
444
  img_src = 'https:' + img_src
445
  elif img_src.startswith('/'):
446
  img_src = 'https://xemlaibongda.top' + img_src
 
447
  title = ''
448
  for sel in ['.title', 'h3', 'h2', '.name', '.post-title', '.entry-title', '.video-title']:
449
  t = a.select_one(sel)
 
456
  img_alt = a.select_one('img')
457
  if img_alt:
458
  title = _clean(img_alt.get('alt',''))
 
459
  if not title:
460
  parent = a.parent
461
  if parent:
462
  pt = _clean(parent.get_text(' ',strip=True))
463
  if 5 < len(pt) < 120:
464
  title = pt
 
465
  if not title or len(title) < 3:
466
  continue
467
  vids.append({"link": href, "img": img_src, "title": title})
 
471
 
472
  @app.get('/api/proxy/xlb')
473
  def proxy_xlb(path: str = Query(default="")):
 
474
  now = time.time()
475
  cache_key = f"xlb:{path}"
476
  with _xlb_lock:
477
  cached = _xlb_cache.get(cache_key)
478
+ if cached and now - cached['t'] < 120:
479
  return JSONResponse(cached['d'])
480
  try:
481
  vids = _xlb_scrape(path)
 
508
  def _wm(eid:int):return JSONResponse(scrape_match_detail(eid))
509
 
510
  DATA_DIR='/data' if os.path.isdir('/data') else os.path.join(os.path.dirname(os.path.abspath(__file__)),'data')
511
+ os.makedirs(DATA_DIR,exist_ok=True)
512
+ IF=os.path.join(DATA_DIR,'interactions_v2.json')
513
+ CF=os.path.join(DATA_DIR,'comments_v2.json')
514
+ WALL_FILE=os.path.join(DATA_DIR,'wall_posts.json')
515
+ WALL_VIDEO_DIR=os.path.join(DATA_DIR,'wall_videos')
516
+ os.makedirs(WALL_VIDEO_DIR,exist_ok=True)
517
+
518
+ _il=threading.Lock();_cl=threading.Lock();_wl_lock=threading.Lock()
519
  def _lj(p):
520
  try:
521
  if os.path.exists(p):return json.load(open(p,'r',encoding='utf-8'))
 
548
  with _il:idb=_lj(IF);idb.setdefault(v,{'views':0,'likes':0,'comments':0});idb[v]['comments']=len(cms);_sj(IF,idb)
549
  return JSONResponse({'comments':cms})
550
 
551
+ # ===== WALL / SHORT AI ENDPOINTS =====
552
+
553
+ def _load_wall_posts():
554
+ """Load wall posts from JSON file."""
555
+ with _wl_lock:
556
+ return _lj(WALL_FILE)
557
+
558
+ def _save_wall_posts(posts):
559
+ """Save wall posts to JSON file."""
560
+ with _wl_lock:
561
+ _sj(WALL_FILE, posts)
562
+
563
+ @app.get('/api/wall')
564
+ def api_wall():
565
+ """Get all wall posts."""
566
+ posts = _load_wall_posts()
567
+ if not posts:
568
+ # Return empty list, not error
569
+ return JSONResponse({"posts": []})
570
+ return JSONResponse({"posts": posts})
571
+
572
+ @app.post('/api/wall')
573
+ async def api_wall_post(request: Request):
574
+ """
575
+ Create a wall post. Supports:
576
+ - JSON body: {title, text, img, source}
577
+ - Multipart form: title, text, source + video file upload
578
+ """
579
+ content_type = request.headers.get('content-type', '')
580
+
581
+ # Handle multipart upload (video file)
582
+ if 'multipart/form-data' in content_type:
583
+ try:
584
+ form = await request.form()
585
+ except Exception as e:
586
+ return JSONResponse({"error": f"Form parse error: {str(e)}"}, status_code=400)
587
+
588
+ title = form.get('title', 'Video mới') or 'Video mới'
589
+ text = form.get('text', '') or ''
590
+ source = form.get('source', 'vtv_recorder') or 'vtv_recorder'
591
+ video_file = form.get('video')
592
+
593
+ post_id = str(uuid.uuid4())[:12]
594
+ video_url = None
595
+
596
+ # Save video file if provided
597
+ if video_file and hasattr(video_file, 'filename') and video_file.filename:
598
+ # Determine extension
599
+ fname = video_file.filename.lower()
600
+ if fname.endswith('.mp4'):
601
+ ext = '.mp4'
602
+ elif fname.endswith('.webm'):
603
+ ext = '.webm'
604
+ else:
605
+ ext = '.webm'
606
+
607
+ video_filename = f"wall_{post_id}{ext}"
608
+ video_path = os.path.join(WALL_VIDEO_DIR, video_filename)
609
+
610
+ try:
611
+ # Read file content
612
+ content = await video_file.read()
613
+ if not content:
614
+ return JSONResponse({"error": "Empty video file"}, status_code=400)
615
+
616
+ # Save to disk
617
+ with open(video_path, 'wb') as f:
618
+ f.write(content)
619
+
620
+ file_size_mb = len(content) / 1024 / 1024
621
+ if file_size_mb > 50:
622
+ os.remove(video_path)
623
+ return JSONResponse({"error": f"Video quá lớn ({file_size_mb:.1f}MB). Tối đa 50MB."}, status_code=400)
624
+
625
+ # URL to access the video
626
+ video_url = f"/api/wall/video/{video_filename}"
627
+ except Exception as e:
628
+ return JSONResponse({"error": f"Lỗi lưu video: {str(e)}"}, status_code=500)
629
+
630
+ # Create post
631
+ post = {
632
+ "id": post_id,
633
+ "title": title[:200],
634
+ "text": text[:2000],
635
+ "source": source,
636
+ "video": video_url,
637
+ "img": None,
638
+ "images": [],
639
+ "created": int(time.time()),
640
+ "created_str": time.strftime('%H:%M %d/%m/%Y', time.localtime()),
641
+ }
642
+
643
+ # Save to wall
644
+ posts = _load_wall_posts()
645
+ if not isinstance(posts, list):
646
+ posts = []
647
+ posts.insert(0, post)
648
+ # Keep max 200 posts
649
+ posts = posts[:200]
650
+ _save_wall_posts(posts)
651
+
652
+ return JSONResponse({"post": post, "ok": True})
653
+
654
+ # Handle JSON body (text-only post)
655
+ try:
656
+ body = await request.json()
657
+ except:
658
+ body = {}
659
+
660
+ title = body.get('title', 'Bài mới') or 'Bài mới'
661
+ text = body.get('text', '') or ''
662
+ img = body.get('img', None)
663
+ source = body.get('source', 'user') or 'user'
664
+
665
+ post_id = str(uuid.uuid4())[:12]
666
+ post = {
667
+ "id": post_id,
668
+ "title": title[:200],
669
+ "text": text[:2000],
670
+ "source": source,
671
+ "video": None,
672
+ "img": img,
673
+ "images": [],
674
+ "created": int(time.time()),
675
+ "created_str": time.strftime('%H:%M %d/%m/%Y', time.localtime()),
676
+ }
677
+
678
+ posts = _load_wall_posts()
679
+ if not isinstance(posts, list):
680
+ posts = []
681
+ posts.insert(0, post)
682
+ posts = posts[:200]
683
+ _save_wall_posts(posts)
684
+
685
+ return JSONResponse({"post": post, "ok": True})
686
+
687
+ @app.get('/api/wall/video/{filename}')
688
+ def api_wall_video(filename: str):
689
+ """Serve a wall video file."""
690
+ # Security: prevent path traversal
691
+ if '..' in filename or '/' in filename:
692
+ return Response(status_code=403)
693
+ video_path = os.path.join(WALL_VIDEO_DIR, filename)
694
+ if not os.path.exists(video_path):
695
+ return Response(status_code=404)
696
+ ext = os.path.splitext(filename)[1].lower()
697
+ media_type = 'video/mp4' if ext == '.mp4' else 'video/webm'
698
+ return FileResponse(video_path, media_type=media_type)
699
+
700
+ @app.delete('/api/wall/{post_id}')
701
+ def api_wall_delete(post_id: str):
702
+ """Delete a wall post and its video."""
703
+ posts = _load_wall_posts()
704
+ if not isinstance(posts, list):
705
+ return JSONResponse({"error": "No posts"}, status_code=404)
706
+
707
+ for i, p in enumerate(posts):
708
+ if p.get('id') == post_id:
709
+ # Delete video file if exists
710
+ if p.get('video'):
711
+ video_name = p['video'].split('/')[-1]
712
+ video_path = os.path.join(WALL_VIDEO_DIR, video_name)
713
+ if os.path.exists(video_path):
714
+ os.remove(video_path)
715
+ posts.pop(i)
716
+ _save_wall_posts(posts)
717
+ return JSONResponse({"ok": True})
718
+
719
+ return JSONResponse({"error": "Post not found"}, status_code=404)
720
+
721
  def _bg():
722
  time.sleep(15)
723
  while True: