Update app.py
Browse files
app.py
CHANGED
|
@@ -1,16 +1,16 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
from googleapiclient.discovery import build
|
|
|
|
| 3 |
import os
|
| 4 |
import sqlite3
|
| 5 |
from datetime import datetime, timedelta
|
| 6 |
import re
|
| 7 |
-
import anthropic
|
| 8 |
|
| 9 |
API_KEY = os.getenv("YOUTUBE_API_KEY")
|
| 10 |
-
|
| 11 |
|
| 12 |
youtube = build("youtube", "v3", developerKey=API_KEY)
|
| 13 |
-
|
| 14 |
|
| 15 |
# ============================================
|
| 16 |
# 🎨 Comic Classic Theme CSS
|
|
@@ -177,62 +177,71 @@ def format_count(count):
|
|
| 177 |
return str(count)
|
| 178 |
|
| 179 |
def get_ai_pick_rating(videos_data):
|
| 180 |
-
"""
|
| 181 |
-
if not
|
| 182 |
-
# API 없으면 자체 알고리즘으로 계산
|
| 183 |
return calculate_local_rating(videos_data)
|
| 184 |
|
| 185 |
try:
|
| 186 |
-
# 상위
|
| 187 |
-
sample = videos_data[:
|
| 188 |
|
| 189 |
video_info = "\n".join([
|
| 190 |
-
f"#{i+1}. 제목:
|
| 191 |
-
f"조회수:
|
| 192 |
-
f"구독자: {v.get('subs', 0)}"
|
| 193 |
for i, v in enumerate(sample)
|
| 194 |
])
|
| 195 |
|
| 196 |
-
|
| 197 |
-
model="claude-sonnet-4-20250514",
|
| 198 |
-
max_tokens=2000,
|
| 199 |
-
messages=[{
|
| 200 |
-
"role": "user",
|
| 201 |
-
"content": f"""다음 YouTube 영상 목록을 분석하여 각 영상의 종합 품질 등급을 매겨주세요.
|
| 202 |
|
| 203 |
평가 기준:
|
| 204 |
- 조회수 대비 좋아요/댓글 비율 (engagement)
|
| 205 |
- 채널 구독자 대비 조회수 (viral potential)
|
| 206 |
-
- 전반적인
|
| 207 |
|
| 208 |
-
등급 (숫자로만
|
| 209 |
-
0
|
| 210 |
-
1 = ♥ (평균)
|
| 211 |
-
2 = ⭐ (좋음)
|
| 212 |
-
3 = ⭐⭐ (매우 좋음)
|
| 213 |
-
4 = ⭐⭐⭐ (최고)
|
| 214 |
|
| 215 |
-
각 영상 번호와 등급만
|
| 216 |
-
|
| 217 |
|
| 218 |
영상 목록:
|
| 219 |
{video_info}
|
| 220 |
|
| 221 |
응답 (번호:등급 형식으로만):"""
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
)
|
| 224 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
# 응답 파싱
|
| 226 |
ratings = {}
|
| 227 |
-
result_text = response.content[0].text.strip()
|
| 228 |
pairs = re.findall(r'(\d+):(\d)', result_text)
|
| 229 |
for idx, rating in pairs:
|
| 230 |
ratings[int(idx)-1] = int(rating)
|
| 231 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
return ratings
|
| 233 |
|
| 234 |
except Exception as e:
|
| 235 |
-
print(f"AI Rating Error: {e}")
|
| 236 |
return calculate_local_rating(videos_data)
|
| 237 |
|
| 238 |
def calculate_local_rating(videos_data):
|
|
@@ -242,7 +251,6 @@ def calculate_local_rating(videos_data):
|
|
| 242 |
if not videos_data:
|
| 243 |
return ratings
|
| 244 |
|
| 245 |
-
# 통계 계산
|
| 246 |
views_list = [v['views'] for v in videos_data if v['views'] > 0]
|
| 247 |
if not views_list:
|
| 248 |
return ratings
|
|
@@ -256,52 +264,35 @@ def calculate_local_rating(videos_data):
|
|
| 256 |
comments = v['comments']
|
| 257 |
subs = v.get('subs', 0)
|
| 258 |
|
| 259 |
-
# 스코어 계산
|
| 260 |
score = 0
|
| 261 |
|
| 262 |
# 조회수 점수 (0-40)
|
| 263 |
if views > 0:
|
| 264 |
-
|
| 265 |
-
score += view_score
|
| 266 |
|
| 267 |
# engagement 점수 (0-30)
|
| 268 |
if views > 0:
|
| 269 |
engagement = ((likes + comments * 2) / views) * 100
|
| 270 |
-
|
| 271 |
-
score += engagement_score
|
| 272 |
|
| 273 |
-
# viral 점수 (
|
| 274 |
if subs > 0 and views > 0:
|
| 275 |
-
|
| 276 |
-
viral_score = min(30, viral * 10)
|
| 277 |
-
score += viral_score
|
| 278 |
elif views > avg_views:
|
| 279 |
score += 15
|
| 280 |
|
| 281 |
# 등급 결정
|
| 282 |
-
if score >= 70:
|
| 283 |
-
|
| 284 |
-
elif score >=
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
ratings[i] = 2 # ⭐
|
| 288 |
-
elif score >= 15:
|
| 289 |
-
ratings[i] = 1 # ♥
|
| 290 |
-
else:
|
| 291 |
-
ratings[i] = 0 # 없음
|
| 292 |
|
| 293 |
return ratings
|
| 294 |
|
| 295 |
def get_rating_display(rating):
|
| 296 |
"""등급을 이모지로 변환"""
|
| 297 |
-
|
| 298 |
-
0: "",
|
| 299 |
-
1: "♥",
|
| 300 |
-
2: "⭐",
|
| 301 |
-
3: "⭐⭐",
|
| 302 |
-
4: "⭐⭐⭐"
|
| 303 |
-
}
|
| 304 |
-
return displays.get(rating, "")
|
| 305 |
|
| 306 |
def get_real_trending_keywords(region_code="KR", language="ko"):
|
| 307 |
try:
|
|
@@ -484,7 +475,7 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
|
|
| 484 |
"subs": channel_subs_raw.get(snip["channelId"], 0),
|
| 485 |
})
|
| 486 |
|
| 487 |
-
# AI Pick 등급 계산
|
| 488 |
ai_ratings = get_ai_pick_rating(videos_data)
|
| 489 |
|
| 490 |
# DB 저장
|
|
@@ -519,7 +510,7 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
|
|
| 519 |
|
| 520 |
<div class="result-header">
|
| 521 |
🎬 총 <b>{len(all_items)}개</b> 결과 | 🔍 "{keyword}" | 🌍 {country} | 📊 {sort_by}
|
| 522 |
-
<br><span style="font-size:0.9rem;">💡 헤더 클릭 = 정렬 | AI Pick
|
| 523 |
</div>
|
| 524 |
|
| 525 |
<div style="max-height:700px; overflow-y:auto; border:3px solid #1F2937; border-radius:8px;">
|
|
@@ -625,11 +616,10 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
|
|
| 625 |
return html, stats_html
|
| 626 |
|
| 627 |
def show_trending_alerts():
|
| 628 |
-
"""급상승 알림
|
| 629 |
conn = sqlite3.connect("youtube_data.db")
|
| 630 |
c = conn.cursor()
|
| 631 |
|
| 632 |
-
# 최근 급상승 알림
|
| 633 |
c.execute('''
|
| 634 |
SELECT ta.video_id, v.title, v.channel_name, ta.old_value, ta.new_value, ta.change_percent, ta.detected_at
|
| 635 |
FROM trending_alerts ta
|
|
@@ -640,14 +630,13 @@ def show_trending_alerts():
|
|
| 640 |
conn.close()
|
| 641 |
|
| 642 |
if not alerts:
|
| 643 |
-
# 알림 없으면 현재 인기 급상승 영상 직접 조회
|
| 644 |
try:
|
| 645 |
resp = youtube.videos().list(part="snippet,statistics", chart="mostPopular", regionCode="KR", maxResults=20).execute()
|
| 646 |
|
| 647 |
html = '''
|
| 648 |
-
<div style="background:#FACC15; padding:15px; border:3px solid #1F2937; border-radius:8px; margin-bottom:15px;">
|
| 649 |
<h3 style="font-family:'Bangers',cursive; margin:0;">🔥 현재 급상승 인기 영상 (한국)</h3>
|
| 650 |
-
<p style="margin:5px 0 0 0; font-size:0.9rem;"
|
| 651 |
</div>
|
| 652 |
<div style="display:flex; flex-wrap:wrap; gap:15px;">
|
| 653 |
'''
|
|
@@ -655,24 +644,23 @@ def show_trending_alerts():
|
|
| 655 |
for i, item in enumerate(resp.get("items", [])[:20], 1):
|
| 656 |
snip = item["snippet"]
|
| 657 |
stats = item["statistics"]
|
| 658 |
-
vid = item["id"]
|
| 659 |
title = snip["title"][:30] + "..." if len(snip["title"]) > 30 else snip["title"]
|
| 660 |
views = format_count(int(stats.get("viewCount", 0)))
|
| 661 |
-
url = f"https://youtube.com/watch?v={
|
| 662 |
thumb = snip["thumbnails"]["medium"]["url"]
|
| 663 |
|
| 664 |
html += f'''
|
| 665 |
-
<div style="width:180px; background:#FFF; border:3px solid #1F2937; border-radius:8px; padding:10px; box-shadow:4px 4px 0 #1F2937;">
|
| 666 |
<a href="{url}" target="_blank"><img src="{thumb}" style="width:100%; border-radius:5px; border:2px solid #1F2937;"></a>
|
| 667 |
<p style="margin:8px 0 5px 0; font-size:12px; font-weight:700; color:#1F2937;">{i}. {title}</p>
|
| 668 |
-
<p style="margin:0; font-size:14px; color:#EF4444; font-weight:700;">👀 {views}</p>
|
| 669 |
</div>
|
| 670 |
'''
|
| 671 |
|
| 672 |
html += '</div>'
|
| 673 |
return html
|
| 674 |
except Exception as e:
|
| 675 |
-
return f"<p>데이터 로드 오류: {e}</p>"
|
| 676 |
|
| 677 |
html = '''
|
| 678 |
<div style="background:#EF4444; color:white; padding:15px; border:3px solid #1F2937; border-radius:8px; margin-bottom:15px;">
|
|
@@ -681,11 +669,8 @@ def show_trending_alerts():
|
|
| 681 |
<table style="width:100%; border-collapse:collapse; font-family:'Comic Neue',cursive;">
|
| 682 |
<thead><tr style="background:#1F2937; color:#FACC15;">
|
| 683 |
<th style="padding:10px; border:2px solid #1F2937;">영상</th>
|
| 684 |
-
<th style="padding:10px;">채널</th>
|
| 685 |
-
<th style="padding:10px;"
|
| 686 |
-
<th style="padding:10px;">현재</th>
|
| 687 |
-
<th style="padding:10px;">증가율</th>
|
| 688 |
-
<th style="padding:10px;">감지시간</th>
|
| 689 |
</tr></thead><tbody>
|
| 690 |
'''
|
| 691 |
|
|
@@ -707,7 +692,7 @@ def show_trending_alerts():
|
|
| 707 |
return html
|
| 708 |
|
| 709 |
def show_top_growing():
|
| 710 |
-
"""급성장 TOP
|
| 711 |
conn = sqlite3.connect("youtube_data.db")
|
| 712 |
c = conn.cursor()
|
| 713 |
cutoff = (datetime.now() - timedelta(hours=48)).isoformat()
|
|
@@ -727,26 +712,22 @@ def show_top_growing():
|
|
| 727 |
conn.close()
|
| 728 |
|
| 729 |
if not results:
|
| 730 |
-
# DB 없으면 실시간 인기 영상 표시
|
| 731 |
try:
|
| 732 |
resp = youtube.videos().list(part="snippet,statistics", chart="mostPopular", regionCode="KR", maxResults=20).execute()
|
| 733 |
|
| 734 |
html = '''
|
| 735 |
<div style="background:#3B82F6; color:white; padding:15px; border:3px solid #1F2937; border-radius:8px; margin-bottom:15px;">
|
| 736 |
<h3 style="font-family:'Bangers',cursive; margin:0;">📈 현재 인기 급상승 TOP 20</h3>
|
| 737 |
-
<p style="margin:5px 0 0 0; font-size:0.9rem;">검색을 여러 번
|
| 738 |
</div>
|
| 739 |
<div style="display:flex; flex-wrap:wrap; gap:15px;">
|
| 740 |
'''
|
| 741 |
|
| 742 |
-
|
| 743 |
-
for i, item in enumerate(items[:20], 1):
|
| 744 |
snip = item["snippet"]
|
| 745 |
stats = item["statistics"]
|
| 746 |
views = int(stats.get("viewCount", 0))
|
| 747 |
likes = int(stats.get("likeCount", 0))
|
| 748 |
-
|
| 749 |
-
# 간단한 engagement 계산
|
| 750 |
engagement = (likes / views * 100) if views > 0 else 0
|
| 751 |
|
| 752 |
title = snip["title"][:25] + "..." if len(snip["title"]) > 25 else snip["title"]
|
|
@@ -754,11 +735,11 @@ def show_top_growing():
|
|
| 754 |
thumb = snip["thumbnails"]["medium"]["url"]
|
| 755 |
|
| 756 |
html += f'''
|
| 757 |
-
<div style="width:180px; background:#FFF; border:3px solid #1F2937; border-radius:8px; padding:10px; box-shadow:4px 4px 0 #1F2937;">
|
| 758 |
<a href="{url}" target="_blank"><img src="{thumb}" style="width:100%; border-radius:5px; border:2px solid #1F2937;"></a>
|
| 759 |
<p style="margin:8px 0 5px 0; font-size:12px; font-weight:700;">{i}. {title}</p>
|
| 760 |
<p style="margin:0; font-size:11px; color:#666;">{snip["channelTitle"][:15]}</p>
|
| 761 |
-
<p style="margin:5px 0 0 0; font-size:14px; color:#3B82F6; font-weight:700;">👀 {format_count(views)}</p>
|
| 762 |
<p style="margin:2px 0 0 0; font-size:12px; color:#EF4444;">❤️ {engagement:.2f}%</p>
|
| 763 |
</div>
|
| 764 |
'''
|
|
@@ -780,7 +761,7 @@ def show_top_growing():
|
|
| 780 |
url = f"https://youtube.com/watch?v={vid}"
|
| 781 |
|
| 782 |
html += f'''
|
| 783 |
-
<div style="width:180px; background:#FFF; border:3px solid #1F2937; border-radius:8px; padding:10px; box-shadow:4px 4px 0 #1F2937;">
|
| 784 |
<a href="{url}" target="_blank"><img src="{thumb}" style="width:100%; border-radius:5px; border:2px solid #1F2937;"></a>
|
| 785 |
<p style="margin:8px 0 5px 0; font-size:12px; font-weight:700;">{i}. {title}</p>
|
| 786 |
<p style="margin:0; font-size:11px; color:#666;">{channel[:15]}</p>
|
|
@@ -886,3 +867,4 @@ with gr.Blocks(css=css) as demo:
|
|
| 886 |
history_btn.click(show_search_history, outputs=history_out)
|
| 887 |
|
| 888 |
demo.launch()
|
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
from googleapiclient.discovery import build
|
| 3 |
+
from groq import Groq
|
| 4 |
import os
|
| 5 |
import sqlite3
|
| 6 |
from datetime import datetime, timedelta
|
| 7 |
import re
|
|
|
|
| 8 |
|
| 9 |
API_KEY = os.getenv("YOUTUBE_API_KEY")
|
| 10 |
+
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
|
| 11 |
|
| 12 |
youtube = build("youtube", "v3", developerKey=API_KEY)
|
| 13 |
+
groq_client = Groq(api_key=GROQ_API_KEY) if GROQ_API_KEY else None
|
| 14 |
|
| 15 |
# ============================================
|
| 16 |
# 🎨 Comic Classic Theme CSS
|
|
|
|
| 177 |
return str(count)
|
| 178 |
|
| 179 |
def get_ai_pick_rating(videos_data):
|
| 180 |
+
"""Groq API (GPT-OSS-120B)로 AI Pick 등급 산정"""
|
| 181 |
+
if not groq_client or not videos_data:
|
|
|
|
| 182 |
return calculate_local_rating(videos_data)
|
| 183 |
|
| 184 |
try:
|
| 185 |
+
# 상위 50개만 분석 (토큰 제한)
|
| 186 |
+
sample = videos_data[:50]
|
| 187 |
|
| 188 |
video_info = "\n".join([
|
| 189 |
+
f"#{i+1}. 제목:{v['title'][:40]}, 채널:{v['channel_name'][:15]}, "
|
| 190 |
+
f"조회수:{v['views']}, 좋아요:{v['likes']}, 댓글:{v['comments']}, 구독자:{v.get('subs',0)}"
|
|
|
|
| 191 |
for i, v in enumerate(sample)
|
| 192 |
])
|
| 193 |
|
| 194 |
+
prompt = f"""YouTube 영상 목록을 분석하여 각 영상의 종합 품질 등급을 매겨주세요.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
평가 기준:
|
| 197 |
- 조회수 대비 좋아요/댓글 비율 (engagement)
|
| 198 |
- 채널 구독자 대비 조회수 (viral potential)
|
| 199 |
+
- 전반적인 인기도
|
| 200 |
|
| 201 |
+
등급 (숫자로만):
|
| 202 |
+
0=없음(평균이하), 1=♥(평균), 2=⭐(좋음), 3=⭐⭐(매우좋음), 4=⭐⭐⭐(최고)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
|
| 204 |
+
각 영상 번호와 등급만 응답하세요.
|
| 205 |
+
형식: 1:3,2:2,3:4,4:1,5:0
|
| 206 |
|
| 207 |
영상 목록:
|
| 208 |
{video_info}
|
| 209 |
|
| 210 |
응답 (번호:등급 형식으로만):"""
|
| 211 |
+
|
| 212 |
+
# Groq API 호출 (스트리밍)
|
| 213 |
+
completion = groq_client.chat.completions.create(
|
| 214 |
+
model="openai/gpt-oss-120b",
|
| 215 |
+
messages=[{"role": "user", "content": prompt}],
|
| 216 |
+
temperature=0.7,
|
| 217 |
+
max_completion_tokens=2000,
|
| 218 |
+
top_p=1,
|
| 219 |
+
stream=True,
|
| 220 |
+
stop=None
|
| 221 |
)
|
| 222 |
|
| 223 |
+
# 스트리밍 응답 수집
|
| 224 |
+
result_text = ""
|
| 225 |
+
for chunk in completion:
|
| 226 |
+
if chunk.choices[0].delta.content:
|
| 227 |
+
result_text += chunk.choices[0].delta.content
|
| 228 |
+
|
| 229 |
# 응답 파싱
|
| 230 |
ratings = {}
|
|
|
|
| 231 |
pairs = re.findall(r'(\d+):(\d)', result_text)
|
| 232 |
for idx, rating in pairs:
|
| 233 |
ratings[int(idx)-1] = int(rating)
|
| 234 |
|
| 235 |
+
# 나머지 영상은 로컬 알고리즘으로
|
| 236 |
+
if len(videos_data) > 50:
|
| 237 |
+
local_ratings = calculate_local_rating(videos_data[50:])
|
| 238 |
+
for k, v in local_ratings.items():
|
| 239 |
+
ratings[k + 50] = v
|
| 240 |
+
|
| 241 |
return ratings
|
| 242 |
|
| 243 |
except Exception as e:
|
| 244 |
+
print(f"Groq AI Rating Error: {e}")
|
| 245 |
return calculate_local_rating(videos_data)
|
| 246 |
|
| 247 |
def calculate_local_rating(videos_data):
|
|
|
|
| 251 |
if not videos_data:
|
| 252 |
return ratings
|
| 253 |
|
|
|
|
| 254 |
views_list = [v['views'] for v in videos_data if v['views'] > 0]
|
| 255 |
if not views_list:
|
| 256 |
return ratings
|
|
|
|
| 264 |
comments = v['comments']
|
| 265 |
subs = v.get('subs', 0)
|
| 266 |
|
|
|
|
| 267 |
score = 0
|
| 268 |
|
| 269 |
# 조회수 점수 (0-40)
|
| 270 |
if views > 0:
|
| 271 |
+
score += min(40, (views / max_views) * 40)
|
|
|
|
| 272 |
|
| 273 |
# engagement 점수 (0-30)
|
| 274 |
if views > 0:
|
| 275 |
engagement = ((likes + comments * 2) / views) * 100
|
| 276 |
+
score += min(30, engagement * 3)
|
|
|
|
| 277 |
|
| 278 |
+
# viral 점수 (0-30)
|
| 279 |
if subs > 0 and views > 0:
|
| 280 |
+
score += min(30, (views / subs) * 10)
|
|
|
|
|
|
|
| 281 |
elif views > avg_views:
|
| 282 |
score += 15
|
| 283 |
|
| 284 |
# 등급 결정
|
| 285 |
+
if score >= 70: ratings[i] = 4
|
| 286 |
+
elif score >= 50: ratings[i] = 3
|
| 287 |
+
elif score >= 30: ratings[i] = 2
|
| 288 |
+
elif score >= 15: ratings[i] = 1
|
| 289 |
+
else: ratings[i] = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
|
| 291 |
return ratings
|
| 292 |
|
| 293 |
def get_rating_display(rating):
|
| 294 |
"""등급을 이모지로 변환"""
|
| 295 |
+
return {0: "", 1: "♥", 2: "⭐", 3: "⭐⭐", 4: "⭐⭐⭐"}.get(rating, "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
def get_real_trending_keywords(region_code="KR", language="ko"):
|
| 298 |
try:
|
|
|
|
| 475 |
"subs": channel_subs_raw.get(snip["channelId"], 0),
|
| 476 |
})
|
| 477 |
|
| 478 |
+
# AI Pick 등급 계산 (Groq GPT-OSS-120B)
|
| 479 |
ai_ratings = get_ai_pick_rating(videos_data)
|
| 480 |
|
| 481 |
# DB 저장
|
|
|
|
| 510 |
|
| 511 |
<div class="result-header">
|
| 512 |
🎬 총 <b>{len(all_items)}개</b> 결과 | 🔍 "{keyword}" | 🌍 {country} | 📊 {sort_by}
|
| 513 |
+
<br><span style="font-size:0.9rem;">💡 헤더 클릭 = 정렬 | 🤖 AI Pick (GPT-OSS-120B): ♥ ⭐ ⭐⭐ ⭐⭐⭐</span>
|
| 514 |
</div>
|
| 515 |
|
| 516 |
<div style="max-height:700px; overflow-y:auto; border:3px solid #1F2937; border-radius:8px;">
|
|
|
|
| 616 |
return html, stats_html
|
| 617 |
|
| 618 |
def show_trending_alerts():
|
| 619 |
+
"""급상승 알림"""
|
| 620 |
conn = sqlite3.connect("youtube_data.db")
|
| 621 |
c = conn.cursor()
|
| 622 |
|
|
|
|
| 623 |
c.execute('''
|
| 624 |
SELECT ta.video_id, v.title, v.channel_name, ta.old_value, ta.new_value, ta.change_percent, ta.detected_at
|
| 625 |
FROM trending_alerts ta
|
|
|
|
| 630 |
conn.close()
|
| 631 |
|
| 632 |
if not alerts:
|
|
|
|
| 633 |
try:
|
| 634 |
resp = youtube.videos().list(part="snippet,statistics", chart="mostPopular", regionCode="KR", maxResults=20).execute()
|
| 635 |
|
| 636 |
html = '''
|
| 637 |
+
<div style="background:#FACC15; padding:15px; border:3px solid #1F2937; border-radius:8px; margin-bottom:15px; font-family:'Comic Neue',cursive;">
|
| 638 |
<h3 style="font-family:'Bangers',cursive; margin:0;">🔥 현재 급상승 인기 영상 (한국)</h3>
|
| 639 |
+
<p style="margin:5px 0 0 0; font-size:0.9rem;">검색을 여러 번 실행하면 변화율 기반 알림이 표시됩니다</p>
|
| 640 |
</div>
|
| 641 |
<div style="display:flex; flex-wrap:wrap; gap:15px;">
|
| 642 |
'''
|
|
|
|
| 644 |
for i, item in enumerate(resp.get("items", [])[:20], 1):
|
| 645 |
snip = item["snippet"]
|
| 646 |
stats = item["statistics"]
|
|
|
|
| 647 |
title = snip["title"][:30] + "..." if len(snip["title"]) > 30 else snip["title"]
|
| 648 |
views = format_count(int(stats.get("viewCount", 0)))
|
| 649 |
+
url = f"https://youtube.com/watch?v={item['id']}"
|
| 650 |
thumb = snip["thumbnails"]["medium"]["url"]
|
| 651 |
|
| 652 |
html += f'''
|
| 653 |
+
<div style="width:180px; background:#FFF; border:3px solid #1F2937; border-radius:8px; padding:10px; box-shadow:4px 4px 0 #1F2937; font-family:'Comic Neue',cursive;">
|
| 654 |
<a href="{url}" target="_blank"><img src="{thumb}" style="width:100%; border-radius:5px; border:2px solid #1F2937;"></a>
|
| 655 |
<p style="margin:8px 0 5px 0; font-size:12px; font-weight:700; color:#1F2937;">{i}. {title}</p>
|
| 656 |
+
<p style="margin:0; font-size:14px; color:#EF4444; font-weight:700; font-family:'Bangers',cursive;">👀 {views}</p>
|
| 657 |
</div>
|
| 658 |
'''
|
| 659 |
|
| 660 |
html += '</div>'
|
| 661 |
return html
|
| 662 |
except Exception as e:
|
| 663 |
+
return f"<p style='font-family:Comic Neue,cursive;'>데이터 로드 오류: {e}</p>"
|
| 664 |
|
| 665 |
html = '''
|
| 666 |
<div style="background:#EF4444; color:white; padding:15px; border:3px solid #1F2937; border-radius:8px; margin-bottom:15px;">
|
|
|
|
| 669 |
<table style="width:100%; border-collapse:collapse; font-family:'Comic Neue',cursive;">
|
| 670 |
<thead><tr style="background:#1F2937; color:#FACC15;">
|
| 671 |
<th style="padding:10px; border:2px solid #1F2937;">영상</th>
|
| 672 |
+
<th style="padding:10px;">채널</th><th style="padding:10px;">이전</th>
|
| 673 |
+
<th style="padding:10px;">현재</th><th style="padding:10px;">증가율</th><th style="padding:10px;">감지시간</th>
|
|
|
|
|
|
|
|
|
|
| 674 |
</tr></thead><tbody>
|
| 675 |
'''
|
| 676 |
|
|
|
|
| 692 |
return html
|
| 693 |
|
| 694 |
def show_top_growing():
|
| 695 |
+
"""급성장 TOP"""
|
| 696 |
conn = sqlite3.connect("youtube_data.db")
|
| 697 |
c = conn.cursor()
|
| 698 |
cutoff = (datetime.now() - timedelta(hours=48)).isoformat()
|
|
|
|
| 712 |
conn.close()
|
| 713 |
|
| 714 |
if not results:
|
|
|
|
| 715 |
try:
|
| 716 |
resp = youtube.videos().list(part="snippet,statistics", chart="mostPopular", regionCode="KR", maxResults=20).execute()
|
| 717 |
|
| 718 |
html = '''
|
| 719 |
<div style="background:#3B82F6; color:white; padding:15px; border:3px solid #1F2937; border-radius:8px; margin-bottom:15px;">
|
| 720 |
<h3 style="font-family:'Bangers',cursive; margin:0;">📈 현재 인기 급상승 TOP 20</h3>
|
| 721 |
+
<p style="margin:5px 0 0 0; font-size:0.9rem; font-family:'Comic Neue',cursive;">검색을 여러 번 실행하면 성장률 데이터가 축적됩니다</p>
|
| 722 |
</div>
|
| 723 |
<div style="display:flex; flex-wrap:wrap; gap:15px;">
|
| 724 |
'''
|
| 725 |
|
| 726 |
+
for i, item in enumerate(resp.get("items", [])[:20], 1):
|
|
|
|
| 727 |
snip = item["snippet"]
|
| 728 |
stats = item["statistics"]
|
| 729 |
views = int(stats.get("viewCount", 0))
|
| 730 |
likes = int(stats.get("likeCount", 0))
|
|
|
|
|
|
|
| 731 |
engagement = (likes / views * 100) if views > 0 else 0
|
| 732 |
|
| 733 |
title = snip["title"][:25] + "..." if len(snip["title"]) > 25 else snip["title"]
|
|
|
|
| 735 |
thumb = snip["thumbnails"]["medium"]["url"]
|
| 736 |
|
| 737 |
html += f'''
|
| 738 |
+
<div style="width:180px; background:#FFF; border:3px solid #1F2937; border-radius:8px; padding:10px; box-shadow:4px 4px 0 #1F2937; font-family:'Comic Neue',cursive;">
|
| 739 |
<a href="{url}" target="_blank"><img src="{thumb}" style="width:100%; border-radius:5px; border:2px solid #1F2937;"></a>
|
| 740 |
<p style="margin:8px 0 5px 0; font-size:12px; font-weight:700;">{i}. {title}</p>
|
| 741 |
<p style="margin:0; font-size:11px; color:#666;">{snip["channelTitle"][:15]}</p>
|
| 742 |
+
<p style="margin:5px 0 0 0; font-size:14px; color:#3B82F6; font-weight:700; font-family:'Bangers',cursive;">👀 {format_count(views)}</p>
|
| 743 |
<p style="margin:2px 0 0 0; font-size:12px; color:#EF4444;">❤️ {engagement:.2f}%</p>
|
| 744 |
</div>
|
| 745 |
'''
|
|
|
|
| 761 |
url = f"https://youtube.com/watch?v={vid}"
|
| 762 |
|
| 763 |
html += f'''
|
| 764 |
+
<div style="width:180px; background:#FFF; border:3px solid #1F2937; border-radius:8px; padding:10px; box-shadow:4px 4px 0 #1F2937; font-family:'Comic Neue',cursive;">
|
| 765 |
<a href="{url}" target="_blank"><img src="{thumb}" style="width:100%; border-radius:5px; border:2px solid #1F2937;"></a>
|
| 766 |
<p style="margin:8px 0 5px 0; font-size:12px; font-weight:700;">{i}. {title}</p>
|
| 767 |
<p style="margin:0; font-size:11px; color:#666;">{channel[:15]}</p>
|
|
|
|
| 867 |
history_btn.click(show_search_history, outputs=history_out)
|
| 868 |
|
| 869 |
demo.launch()
|
| 870 |
+
|