bep40 commited on
Commit
52d42e6
·
verified ·
1 Parent(s): 1c80a77

Fix: restore working app_v2_entry.py + add /api/match/{event_id}/detail route

Browse files
Files changed (1) hide show
  1. app_v2_entry.py +22 -187
app_v2_entry.py CHANGED
@@ -71,25 +71,15 @@ def _s_vietnamnet(topic,limit=6):
71
  if len(items)>=limit:break
72
  except:pass
73
  return items
74
- def _s_bongda(topic,limit=6):
75
- """bongda.com.vn via Jina homepage (site is JS-protected); keyword-filtered."""
76
  items=[]
77
  try:
78
- from main import scrape_bongda_jina
79
- for a in scrape_bongda_jina(40):
80
- if _has_kw(topic,a.get('title','')):
81
- items.append({'title':a['title'],'url':a['link'],'via':'Bóng Đá'})
82
- if len(items)>=limit:break
83
- except:pass
84
- return items
85
- def _s_bongdaplus(topic,limit=6):
86
- """bongdaplus.vn via Jina homepage (JS SPA); keyword-filtered."""
87
- items=[]
88
- try:
89
- from main import scrape_bongdaplus_jina
90
- for a in scrape_bongdaplus_jina(40):
91
- if _has_kw(topic,a.get('title','')):
92
- items.append({'title':a['title'],'url':a['link'],'via':'Bóng Đá+'})
93
  if len(items)>=limit:break
94
  except:pass
95
  return items
@@ -102,7 +92,7 @@ def _s_genk(topic,limit=5):
102
  if t and len(t)>15 and _has_kw(topic,t):
103
  if href.startswith('/'):href='https://genk.vn'+href
104
  items.append({'title':t,'url':href,'via':'GenK'})
105
- if len(items)>=limit)break
106
  except:pass
107
  return items
108
  def _s_thanhnien(topic,limit=6):
@@ -114,7 +104,7 @@ def _s_thanhnien(topic,limit=6):
114
  if t and len(t)>15 and _has_kw(topic,t):
115
  if not href.startswith('http'):href='https://thanhnien.vn'+href
116
  items.append({'title':t,'url':href,'via':'Thanh Niên'})
117
- if len(items)>=limit)break
118
  except:pass
119
  return items
120
  def _s_tuoitre(topic,limit=6):
@@ -126,7 +116,7 @@ def _s_tuoitre(topic,limit=6):
126
  if t and len(t)>15 and _has_kw(topic,t):
127
  if not href.startswith('http'):href='https://tuoitre.vn'+href
128
  items.append({'title':t,'url':href,'via':'Tuổi Trẻ'})
129
- if len(items)>=limit)break
130
  except:pass
131
  return items
132
  def _s_thethaovanhoa(topic,limit=5):
@@ -138,14 +128,14 @@ def _s_thethaovanhoa(topic,limit=5):
138
  if t and len(t)>15 and _has_kw(topic,t):
139
  if not href.startswith('http'):href='https://thethaovanhoa.vn'+href
140
  items.append({'title':t,'url':href,'via':'TT&VH'})
141
- if len(items)>=limit)break
142
  except:pass
143
  return items
144
  def _search_all(topic,limit=36):
145
  results={}
146
- with ThreadPoolExecutor(10) as ex:
147
- futs={ex.submit(_s_vnexpress,topic,8):'vne',ex.submit(_s_dantri,topic,8):'dt',ex.submit(_s_vietnamnet,topic,6):'vnn',ex.submit(_s_bongda,topic,6):'bd',ex.submit(_s_bongdaplus,topic,6):'bdp',ex.submit(_s_genk,topic,5):'gk',ex.submit(_s_thanhnien,topic,6):'tn',ex.submit(_s_tuoitre,topic,6):'tt',ex.submit(_s_thethaovanhoa,topic,5):'tvh'}
148
- for f in as_completed(futs,timeout=16):
149
  try:results[futs[f]]=f.result()
150
  except:results[futs[f]]=[]
151
  srcs=list(results.values());out=[];seen=set();mx=max((len(s) for s in srcs),default=0)
@@ -153,132 +143,11 @@ def _search_all(topic,limit=36):
153
  for s in srcs:
154
  if i<len(s) and s[i].get('url') and s[i]['url'] not in seen:seen.add(s[i]['url']);out.append(s[i])
155
  return out[:limit]
156
-
157
- from main import (_yt_jina_page, _yt_rss_shorts, SHORTS_FALLBACK,
158
- _YT_CHANNEL_IDS, _cached, _cache_ttl_yt)
159
-
160
- def _yt_jina_shorts_classified(channel, count=24):
161
- items=[];seen=set()
162
- for path, tab_short in (("videos", False), ("shorts", True)):
163
- md=_yt_jina_page(channel, path)
164
- if not md:continue
165
- for m in re.finditer(r'\[([^\]\[]{8,160})\]\(https?://(?:www\.)?youtube\.com/(shorts/|watch\?v=)([A-Za-z0-9_-]{11})[^)]*\)', md):
166
- title=html_lib.unescape(m.group(1)).strip().strip('"')
167
- form=m.group(2); vid=m.group(3)
168
- if vid in seen:continue
169
- tl=title.lower()
170
- if tl in ("home home","shorts shorts","image") or title.startswith("!") or title.startswith("Image"):continue
171
- is_short = True if form.startswith("shorts/") else tab_short
172
- seen.add(vid)
173
- items.append({"title":title,"link":f"https://www.youtube.com/watch?v={vid}",
174
- "img":f"https://i.ytimg.com/vi/{vid}/hqdefault.jpg","source":"yt",
175
- "id":vid,"channel":channel,"is_short":is_short})
176
- if len(items)>=count:break
177
- return items
178
-
179
- def _scrape_shorts_v2():
180
- vids=[]
181
- with ThreadPoolExecutor(2) as ex:
182
- futs=[ex.submit(_yt_jina_shorts_classified,ch,24) for ch in ["baodantri7941","baosuckhoedoisongboyte"]]
183
- for f in as_completed(futs):
184
- try:
185
- r=f.result()
186
- if r:vids.extend(r)
187
- except:pass
188
- have=set(v.get("channel") for v in vids)
189
- for ch in ["baodantri7941","baosuckhoedoisongboyte"]:
190
- if ch not in have:
191
- for v in _yt_rss_shorts(ch,24):
192
- v.setdefault("is_short",True);vids.append(v)
193
- merged=[];seen=set()
194
- for v in vids+[dict(x) for x in SHORTS_FALLBACK]:
195
- vid=v.get("id")
196
- if not vid or vid in seen:continue
197
- if "is_short" not in v:
198
- t=(v.get("title","") or "").lower()
199
- v["is_short"]= ("#shorts" in t) or ("short" in t)
200
- seen.add(vid);merged.append(v)
201
- return merged[:40]
202
-
203
- app.router.routes=[r for r in app.router.routes if not(getattr(r,'path',None)=='/api/shorts' and 'GET' in getattr(r,'methods',set()))]
204
- @app.get('/api/shorts')
205
- def api_shorts_v2():
206
- return JSONResponse(_cached("yt_shorts_v4",_scrape_shorts_v2,ttl=_cache_ttl_yt))
207
-
208
  app.router.routes=[r for r in app.router.routes if not(getattr(r,'path',None)=='/api/article' and 'GET' in getattr(r,'methods',set()))]
209
- _JINA_BOILER_STOP=['facebooktelegram','copy link','follow us','bài viết liên quan','tin liên quan',
210
- 'cùng chuyên mục','bình luận','trả lời','bản quyền','địa chỉ:','lưu ý:','đăng ký nhận tin',
211
- 'chia sẻ bài viết','tags:','© ','bongdaplus.vn.','vật vờ studio để cập nhật','mời các bạn theo dõi',
212
- 'cập nhật tin tức, thủ thuật']
213
- _JINA_NAV=['đăng nhập','tạo tài khoản','reading:','sponsored by']
214
- _JINA_IMG_DENY=['logo','avatar','sponsor','vvaba','ductrinh','longnguyen','hoangtrinh','vvs_logo',
215
- 'cropped-','login.png','/icon','placeholder','/ads/','banner','giaminhmedia','-330x220','-420x280',
216
- '-150x','-100x','thumb']
217
- _JINA_AD_DENY=['giaminhmedia','delivery/cl.php','[hot]','[video','săn ngay','giảm giá','khuyến mãi',
218
- 'mua ngay','đặt mua','shopee','lazada','tiki.vn']
219
- def _jina_strip(s):
220
- s=re.sub(r'!\[[^\]]*\]\([^)]*\)','',s)
221
- s=re.sub(r'\[([^\]]*)\]\([^)]*\)',r'\1',s)
222
- s=re.sub(r'</?[a-zA-Z][^>]*>','',s)
223
- s=re.sub(r'[#>*`_]+','',s)
224
- return re.sub(r'\s+',' ',s).strip()
225
- def _jina_good_img(u,alt=''):
226
- ul=u.lower();al=(alt or '').lower()
227
- if any(k in ul for k in _JINA_IMG_DENY):return False
228
- if 'vật vờ studio' in al or 'sponsored' in al:return False
229
- if not re.search(r'\.(jpg|jpeg|png|webp|gif)',ul) and 'imgthumbnail' not in ul and '/media/' not in ul.lower():return False
230
- return True
231
- def _jina_article(url):
232
- try:
233
- r=req.get("https://r.jina.ai/"+url,headers={'Accept':'text/markdown','X-Return-Format':'markdown','User-Agent':'Mozilla/5.0'},timeout=18)
234
- if r.status_code!=200 or not r.text:return None
235
- lines=r.text.splitlines()
236
- title=''
237
- for ln in lines[:30]:
238
- if ln.startswith('Title:'):title=ln.replace('Title:','',1).strip();break
239
- tw=set(w for w in re.findall(r'[A-Za-zÀ-ỹ0-9]+',title.lower()) if len(w)>3)
240
- start=0
241
- for i,ln in enumerate(lines):
242
- if ln.startswith('# '):
243
- hw=set(re.findall(r'[A-Za-zÀ-ỹ0-9]+',_jina_strip(ln).lower()))
244
- if tw and len(tw&hw)>=max(2,len(tw)//2):start=i+1
245
- body=[];og_img=''
246
- for ln in lines[start:]:
247
- low=_jina_strip(ln).lower()
248
- if low and any(b in low for b in _JINA_BOILER_STOP):break
249
- if any(a in ln.lower() for a in _JINA_AD_DENY):continue
250
- mi=re.search(r'!\[([^\]]*)\]\((https?://[^)\s]+)\)',ln)
251
- if mi and _jina_good_img(mi.group(2),mi.group(1)):
252
- src=mi.group(2)
253
- if not og_img:og_img=src
254
- body.append({'type':'img','src':src});continue
255
- s=ln.strip();is_h=bool(re.match(r'^#{2,4}\s',s));txt=_jina_strip(ln)
256
- if not txt or len(txt)<40:continue
257
- if any(p in txt.lower() for p in _JINA_NAV):continue
258
- raw=re.sub(r'!\[[^\]]*\]\([^)]*\)','',ln);lt=''.join(re.findall(r'\[([^\]]*)\]\([^)]*\)',raw))
259
- if len(lt)>len(txt)*0.5:continue
260
- body.append({'type':'heading' if is_h else 'p','text':txt})
261
- if len([b for b in body if b['type']!='img'])>=40:break
262
- if not og_img:
263
- for ln in lines:
264
- mi=re.search(r'!\[([^\]]*)\]\((https?://[^)\s]+)\)',ln)
265
- if mi and _jina_good_img(mi.group(2),mi.group(1)):og_img=mi.group(2);break
266
- out=[]
267
- for b in body:
268
- if out and b==out[-1]:continue
269
- out.append(b)
270
- if not title and out:title=next((b['text'] for b in out if b['type']=='p'),'')[:90]
271
- if not out:return None
272
- first_p=next((b['text'] for b in out if b['type']=='p'),'')
273
- return {'title':_clean(title),'summary':_clean(first_p[:200]),'og_image':og_img,'image':og_img,'body':out,'source':'jina','url':url}
274
- except:return None
275
  def _scrape_generic(url):
276
  try:
277
- r=req.get(url,headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36','Accept-Language':'vi-VN,vi;q=0.9'},timeout=15,allow_redirects=True);r.encoding='utf-8'
278
- if r.status_code>=400 or 'bongdaplus.vn' in url or 'vatvostudio.vn' in url or 'Just a moment' in r.text[:600]:
279
- jr=_jina_article(url)
280
- if jr and jr.get('body'):return jr
281
- soup=BeautifulSoup(r.text,'lxml')
282
  for tag in soup.find_all(['script','style','nav','footer','aside','form','noscript']):tag.decompose()
283
  h1=soup.find('h1');ogt=soup.find('meta',property='og:title');title=(h1.get_text(strip=True) if h1 else '')or(ogt.get('content','') if ogt else '')
284
  ogd=soup.find('meta',property='og:description');summary=ogd.get('content','') if ogd else ''
@@ -297,39 +166,19 @@ def _scrape_generic(url):
297
  im=el if el.name=='img' else el.find('img')
298
  if im:src=im.get('data-src') or im.get('src') or'';(body.append({'type':'img','src':'https:'+src if src.startswith('//') else src}) if src and'base64' not in src else None)
299
  if not body and summary:body=[{'type':'p','text':summary}]
300
- if not body:
301
- jr=_jina_article(url)
302
- if jr and jr.get('body'):return jr
303
  return{'title':_clean(title),'summary':_clean(summary),'og_image':og_img,'body':body[:50],'source':'generic','url':url}
304
- except:
305
- return _jina_article(url)
306
- _ARTICLE_CACHE={};_ARTICLE_TTL=900
307
- def _article_cache_get(url):
308
- e=_ARTICLE_CACHE.get(url)
309
- if e and time.time()-e['t']<_ARTICLE_TTL:return e['d']
310
- return None
311
- def _article_cache_put(url,data):
312
- if data and data.get('body'):
313
- _ARTICLE_CACHE[url]={'d':data,'t':time.time()}
314
- if len(_ARTICLE_CACHE)>300:
315
- for k in sorted(_ARTICLE_CACHE,key=lambda k:_ARTICLE_CACHE[k]['t'])[:100]:_ARTICLE_CACHE.pop(k,None)
316
  @app.get('/api/article')
317
  def api_article_v2(url:str=Query(...)):
318
- cached=_article_cache_get(url)
319
- if cached:return JSONResponse(cached)
320
  from main import scrape_vne_article,scrape_bbc_article,scrape_dantri_article,scrape_genk_article,scrape_ttvh_article
321
- used_generic=False
322
  if 'vnexpress.net' in url:data=scrape_vne_article(url)
323
  elif 'bbc.com' in url:data=scrape_bbc_article(url)
324
  elif 'dantri.com.vn' in url:data=scrape_dantri_article(url)
325
  elif 'genk.vn' in url:data=scrape_genk_article(url)
326
  elif 'thethaovanhoa.vn' in url:data=scrape_ttvh_article(url)
327
- else:data=_scrape_generic(url);used_generic=True
328
- if not(data and data.get('body')) and not used_generic:
329
- data=_scrape_generic(url)
330
- if data and data.get('body'):
331
- _article_cache_put(url,data);return JSONResponse(data)
332
- return JSONResponse(data if data else{'error':'Không đọc được','url':url})
333
  _hot_cache={'t':0,'d':[]}
334
  def _get_hot_topics():
335
  now=time.time()
@@ -349,22 +198,6 @@ def _get_hot_topics():
349
  phrase=' '.join(words[i:i+n])
350
  if 8<=len(phrase)<=45:key=phrase.lower();freq[key]=freq.get(key,0)+1;display[key]=phrase
351
  except:continue
352
- def _ingest_titles(titles):
353
- for title in titles:
354
- title=_clean(title)
355
- if not title:continue
356
- title=re.sub(r'\s*[-|].*$','',title)
357
- words=[w for w in re.findall(r'[A-Za-zÀ-ỹ0-9]+',title) if len(w)>2 and w.lower() not in _STOP]
358
- if len(words)<2:continue
359
- for n in(3,4,2):
360
- for i in range(max(0,len(words)-n+1)):
361
- phrase=' '.join(words[i:i+n])
362
- if 8<=len(phrase)<=45:key=phrase.lower();freq[key]=freq.get(key,0)+1;display[key]=phrase
363
- try:
364
- from main import scrape_bongda_jina,scrape_bongdaplus_jina
365
- _ingest_titles([a.get('title','') for a in scrape_bongda_jina(20)])
366
- _ingest_titles([a.get('title','') for a in scrape_bongdaplus_jina(20)])
367
- except:pass
368
  ranked=sorted(freq.items(),key=lambda x:x[1],reverse=True);topics=[];seen=set()
369
  for key,count in ranked:
370
  is_dup=any(len(set(e.split())&set(key.split()))/max(len(set(e.split())),len(set(key.split())),1)>0.6 for e in seen)
@@ -390,6 +223,8 @@ def _ht(topic:str=Query(...),page:int=Query(default=0)):
390
  def _cat():return JSONResponse([])
391
  @app.get('/api/storage_status')
392
  def _st():return JSONResponse({'persistent':os.path.isdir('/data') and os.access('/data',os.W_OK)})
 
 
393
  DATA_DIR='/data' if os.path.isdir('/data') else os.path.join(os.path.dirname(os.path.abspath(__file__)),'data')
394
  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')
395
  _il=threading.Lock();_cl=threading.Lock()
 
71
  if len(items)>=limit:break
72
  except:pass
73
  return items
74
+ def _s_bongda(topic,limit=5):
 
75
  items=[]
76
  try:
77
+ r=req.get(f"https://bongda.com.vn/tim-kiem.html?q={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=8);soup=BeautifulSoup(r.text,'lxml')
78
+ for a in soup.select('h3 a[href], .title a[href]')[:limit*2]:
79
+ t=_clean(a.get_text(strip=True));href=a.get('href','')
80
+ if t and len(t)>15 and _has_kw(topic,t):
81
+ if not href.startswith('http'):href='https://bongda.com.vn'+href
82
+ items.append({'title':t,'url':href,'via':'Bóng Đá'})
 
 
 
 
 
 
 
 
 
83
  if len(items)>=limit:break
84
  except:pass
85
  return items
 
92
  if t and len(t)>15 and _has_kw(topic,t):
93
  if href.startswith('/'):href='https://genk.vn'+href
94
  items.append({'title':t,'url':href,'via':'GenK'})
95
+ if len(items)>=limit:break
96
  except:pass
97
  return items
98
  def _s_thanhnien(topic,limit=6):
 
104
  if t and len(t)>15 and _has_kw(topic,t):
105
  if not href.startswith('http'):href='https://thanhnien.vn'+href
106
  items.append({'title':t,'url':href,'via':'Thanh Niên'})
107
+ if len(items)>=limit:break
108
  except:pass
109
  return items
110
  def _s_tuoitre(topic,limit=6):
 
116
  if t and len(t)>15 and _has_kw(topic,t):
117
  if not href.startswith('http'):href='https://tuoitre.vn'+href
118
  items.append({'title':t,'url':href,'via':'Tuổi Trẻ'})
119
+ if len(items)>=limit:break
120
  except:pass
121
  return items
122
  def _s_thethaovanhoa(topic,limit=5):
 
128
  if t and len(t)>15 and _has_kw(topic,t):
129
  if not href.startswith('http'):href='https://thethaovanhoa.vn'+href
130
  items.append({'title':t,'url':href,'via':'TT&VH'})
131
+ if len(items)>=limit:break
132
  except:pass
133
  return items
134
  def _search_all(topic,limit=36):
135
  results={}
136
+ with ThreadPoolExecutor(8) as ex:
137
+ futs={ex.submit(_s_vnexpress,topic,8):'vne',ex.submit(_s_dantri,topic,8):'dt',ex.submit(_s_vietnamnet,topic,6):'vnn',ex.submit(_s_bongda,topic,5):'bd',ex.submit(_s_genk,topic,5):'gk',ex.submit(_s_thanhnien,topic,6):'tn',ex.submit(_s_tuoitre,topic,6):'tt',ex.submit(_s_thethaovanhoa,topic,5):'tvh'}
138
+ for f in as_completed(futs,timeout=14):
139
  try:results[futs[f]]=f.result()
140
  except:results[futs[f]]=[]
141
  srcs=list(results.values());out=[];seen=set();mx=max((len(s) for s in srcs),default=0)
 
143
  for s in srcs:
144
  if i<len(s) and s[i].get('url') and s[i]['url'] not in seen:seen.add(s[i]['url']);out.append(s[i])
145
  return out[:limit]
146
+ # Override article endpoint
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  app.router.routes=[r for r in app.router.routes if not(getattr(r,'path',None)=='/api/article' and 'GET' in getattr(r,'methods',set()))]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  def _scrape_generic(url):
149
  try:
150
+ r=req.get(url,headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36','Accept-Language':'vi-VN,vi;q=0.9'},timeout=15,allow_redirects=True);r.encoding='utf-8';soup=BeautifulSoup(r.text,'lxml')
 
 
 
 
151
  for tag in soup.find_all(['script','style','nav','footer','aside','form','noscript']):tag.decompose()
152
  h1=soup.find('h1');ogt=soup.find('meta',property='og:title');title=(h1.get_text(strip=True) if h1 else '')or(ogt.get('content','') if ogt else '')
153
  ogd=soup.find('meta',property='og:description');summary=ogd.get('content','') if ogd else ''
 
166
  im=el if el.name=='img' else el.find('img')
167
  if im:src=im.get('data-src') or im.get('src') or'';(body.append({'type':'img','src':'https:'+src if src.startswith('//') else src}) if src and'base64' not in src else None)
168
  if not body and summary:body=[{'type':'p','text':summary}]
 
 
 
169
  return{'title':_clean(title),'summary':_clean(summary),'og_image':og_img,'body':body[:50],'source':'generic','url':url}
170
+ except:return None
 
 
 
 
 
 
 
 
 
 
 
171
  @app.get('/api/article')
172
  def api_article_v2(url:str=Query(...)):
 
 
173
  from main import scrape_vne_article,scrape_bbc_article,scrape_dantri_article,scrape_genk_article,scrape_ttvh_article
 
174
  if 'vnexpress.net' in url:data=scrape_vne_article(url)
175
  elif 'bbc.com' in url:data=scrape_bbc_article(url)
176
  elif 'dantri.com.vn' in url:data=scrape_dantri_article(url)
177
  elif 'genk.vn' in url:data=scrape_genk_article(url)
178
  elif 'thethaovanhoa.vn' in url:data=scrape_ttvh_article(url)
179
+ else:data=_scrape_generic(url)
180
+ if data and data.get('body'):return JSONResponse(data)
181
+ data=_scrape_generic(url);return JSONResponse(data if data else{'error':'Không đọc được','url':url})
 
 
 
182
  _hot_cache={'t':0,'d':[]}
183
  def _get_hot_topics():
184
  now=time.time()
 
198
  phrase=' '.join(words[i:i+n])
199
  if 8<=len(phrase)<=45:key=phrase.lower();freq[key]=freq.get(key,0)+1;display[key]=phrase
200
  except:continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  ranked=sorted(freq.items(),key=lambda x:x[1],reverse=True);topics=[];seen=set()
202
  for key,count in ranked:
203
  is_dup=any(len(set(e.split())&set(key.split()))/max(len(set(e.split())),len(set(key.split())),1)>0.6 for e in seen)
 
223
  def _cat():return JSONResponse([])
224
  @app.get('/api/storage_status')
225
  def _st():return JSONResponse({'persistent':os.path.isdir('/data') and os.access('/data',os.W_OK)})
226
+ @app.get('/s')
227
+ async def _sh(url:str='',title:str='',img:str=''):return HTMLResponse(f'<!DOCTYPE html><html><head><meta property="og:title" content="{_clean(title)}"><meta property="og:image" content="{_clean(img)}"><meta http-equiv="refresh" content="0;url={_clean(url) or "/"}"></head><body></body></html>')
228
  DATA_DIR='/data' if os.path.isdir('/data') else os.path.join(os.path.dirname(os.path.abspath(__file__)),'data')
229
  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')
230
  _il=threading.Lock();_cl=threading.Lock()