seawolf2357 commited on
Commit
17e78e6
·
verified ·
1 Parent(s): bbfa293

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +499 -52
app.py CHANGED
@@ -1,10 +1,96 @@
1
  import gradio as gr
2
  from googleapiclient.discovery import build
3
  import os
 
 
 
4
 
5
  API_KEY = os.getenv("YOUTUBE_API_KEY")
6
  youtube = build("youtube", "v3", developerKey=API_KEY)
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  # 국가 코드 - 인구 순 상위 40개
9
  COUNTRIES = {
10
  "전세계": "",
@@ -95,7 +181,6 @@ LANGUAGES = {
95
  "노르웨이어": "no",
96
  }
97
 
98
- # 정렬 기준
99
  SORT_OPTIONS = {
100
  "조회수 순": "viewCount",
101
  "최신순": "date",
@@ -103,7 +188,6 @@ SORT_OPTIONS = {
103
  "평점 순": "rating",
104
  }
105
 
106
- # 기간 필터
107
  DATE_OPTIONS = {
108
  "전체 기간": "",
109
  "오늘": "today",
@@ -112,8 +196,180 @@ DATE_OPTIONS = {
112
  "올해": "thisYear",
113
  }
114
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  def get_trending_keywords(region_code="KR"):
116
- """YouTube 인기 동영상에서 트렌드 키워드 추출"""
117
  try:
118
  response = youtube.videos().list(
119
  part="snippet",
@@ -125,9 +381,8 @@ def get_trending_keywords(region_code="KR"):
125
  keywords = []
126
  seen = set()
127
  for item in response.get("items", []):
128
- title = item["snippet"]["title"]
129
  channel = item["snippet"]["channelTitle"]
130
- keyword = channel.split()[0] if len(channel.split()) > 0 else title.split()[0]
131
  if keyword not in seen and len(keyword) > 1:
132
  keywords.append(keyword)
133
  seen.add(keyword)
@@ -139,35 +394,23 @@ def get_trending_keywords(region_code="KR"):
139
  return get_default_trending()
140
 
141
  def get_default_trending():
142
- """기본 인기 검색어"""
143
  return [
144
  "AI", "ChatGPT", "뉴진스", "BTS", "Netflix",
145
  "게임", "먹방", "브이로그", "shorts", "음악",
146
  "축구", "NBA", "애니메이션", "영화 리뷰", "코딩"
147
  ]
148
 
149
- def format_count(count):
150
- if count >= 1000000000:
151
- return f"{count/1000000000:.1f}B"
152
- elif count >= 1000000:
153
- return f"{count/1000000:.1f}M"
154
- elif count >= 1000:
155
- return f"{count/1000:.1f}K"
156
- return str(count)
157
-
158
  def update_trending(country):
159
- """국가 변경시 트렌딩 키워드 업데이트"""
160
  region = COUNTRIES.get(country, "")
161
  keywords = get_trending_keywords(region if region else "US")
162
  return gr.update(choices=keywords, value=None)
163
 
164
  def use_trending_keyword(trending_keyword):
165
- """트렌딩 키워드 선택시 검색창에 입력"""
166
  return trending_keyword if trending_keyword else ""
167
 
168
- def search_videos(keyword, country, language, sort_by, date_filter, max_results):
169
  if not keyword or not keyword.strip():
170
- return "검색어를 입력하세요."
171
 
172
  max_results = int(max_results)
173
  all_items = []
@@ -186,7 +429,6 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
186
  search_params["relevanceLanguage"] = LANGUAGES[language]
187
 
188
  if date_filter != "전체 기간":
189
- from datetime import datetime, timedelta
190
  now = datetime.utcnow()
191
  if date_filter == "오늘":
192
  after = now - timedelta(days=1)
@@ -216,7 +458,7 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
216
  break
217
 
218
  if not all_items:
219
- return "검색 결과가 없습니다."
220
 
221
  video_ids = [item["id"]["videoId"] for item in all_items]
222
  channel_ids = list(set([item["snippet"]["channelId"] for item in all_items]))
@@ -234,11 +476,14 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
234
  video_stats[v["id"]] = {
235
  "views": int(stats.get("viewCount", 0)),
236
  "views_fmt": format_count(int(stats.get("viewCount", 0))),
237
- "likes": format_count(int(stats.get("likeCount", 0))),
238
- "comments": format_count(int(stats.get("commentCount", 0))),
 
 
239
  }
240
 
241
  channel_subs = {}
 
242
  for i in range(0, len(channel_ids), 50):
243
  batch_ids = channel_ids[i:i+50]
244
  channel_details = youtube.channels().list(
@@ -247,11 +492,37 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
247
  ).execute()
248
 
249
  for ch in channel_details.get("items", []):
250
- sub_count = ch["statistics"].get("subscriberCount", "비공개")
251
- if sub_count != "비공개":
252
- sub_count = format_count(int(sub_count))
253
- channel_subs[ch["id"]] = sub_count
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
 
255
  html = f'''
256
  <style>
257
  .yt-table {{ width: 100%; border-collapse: collapse; font-size: 14px; }}
@@ -265,8 +536,9 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
265
  .yt-views {{ font-weight: bold; color: #065fd4; }}
266
  .yt-subs {{ color: #606060; }}
267
  .yt-date {{ color: #888; font-size: 12px; }}
 
268
  </style>
269
- <p><b>총 {len(all_items)}개 결과</b> | 검색어: "{keyword}" | 정렬: {sort_by}</p>
270
  <div style="max-height: 800px; overflow-y: auto;">
271
  <table class="yt-table">
272
  <thead>
@@ -303,8 +575,8 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
303
 
304
  stats = video_stats.get(video_id, {})
305
  views = stats.get("views_fmt", "N/A")
306
- likes = stats.get("likes", "N/A")
307
- comments = stats.get("comments", "N/A")
308
  subs = channel_subs.get(channel_id, "N/A")
309
 
310
  if i == 1:
@@ -331,38 +603,213 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
331
  '''
332
 
333
  html += '</tbody></table></div>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  return html
335
 
336
- initial_trending = get_default_trending()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
 
338
- with gr.Blocks() as demo:
339
- gr.Markdown("## YouTube 인기 영상 검색기")
 
340
 
341
- gr.Markdown("### 🔥 실시간 인기 검색어")
342
- trending_radio = gr.Radio(
343
- choices=initial_trending,
344
- label="클릭하면 검색창에 자동 입력됩니다",
345
- interactive=True
346
- )
347
 
348
- gr.Markdown("---")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
 
350
- with gr.Row():
351
- keyword = gr.Textbox(label="검색어", placeholder="검색어 ��력...", scale=3)
352
- btn = gr.Button("검색", variant="primary", scale=1)
 
 
 
 
 
 
 
 
 
353
 
354
- with gr.Row():
355
- country = gr.Dropdown(list(COUNTRIES.keys()), value="한국", label="국가 (인구순)")
356
- language = gr.Dropdown(list(LANGUAGES.keys()), value="전체", label="언어 (사용인구순)")
357
- sort_by = gr.Dropdown(list(SORT_OPTIONS.keys()), value="조회수 순", label="정렬")
358
- date_filter = gr.Dropdown(list(DATE_OPTIONS.keys()), value="전체 기간", label="기간")
359
- max_results = gr.Slider(10, 300, value=50, step=10, label="결과 수")
 
 
 
 
360
 
361
- output = gr.HTML()
 
 
 
 
 
 
 
 
 
362
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  trending_radio.change(use_trending_keyword, trending_radio, keyword)
364
  country.change(update_trending, country, trending_radio)
365
- btn.click(search_videos, [keyword, country, language, sort_by, date_filter, max_results], output)
366
- keyword.submit(search_videos, [keyword, country, language, sort_by, date_filter, max_results], output)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
 
368
  demo.launch()
 
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 json
7
 
8
  API_KEY = os.getenv("YOUTUBE_API_KEY")
9
  youtube = build("youtube", "v3", developerKey=API_KEY)
10
 
11
+ # DB 초기화
12
+ def init_db():
13
+ conn = sqlite3.connect("youtube_data.db")
14
+ c = conn.cursor()
15
+
16
+ # 영상 데이터 테이블
17
+ c.execute('''
18
+ CREATE TABLE IF NOT EXISTS videos (
19
+ video_id TEXT PRIMARY KEY,
20
+ title TEXT,
21
+ channel_id TEXT,
22
+ channel_name TEXT,
23
+ thumbnail TEXT,
24
+ published_at TEXT,
25
+ first_seen TEXT
26
+ )
27
+ ''')
28
+
29
+ # 영상 통계 히스토리 (시간별 추적)
30
+ c.execute('''
31
+ CREATE TABLE IF NOT EXISTS video_stats (
32
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
33
+ video_id TEXT,
34
+ views INTEGER,
35
+ likes INTEGER,
36
+ comments INTEGER,
37
+ recorded_at TEXT,
38
+ FOREIGN KEY (video_id) REFERENCES videos(video_id)
39
+ )
40
+ ''')
41
+
42
+ # 채널 데이터 테이블
43
+ c.execute('''
44
+ CREATE TABLE IF NOT EXISTS channels (
45
+ channel_id TEXT PRIMARY KEY,
46
+ channel_name TEXT,
47
+ first_seen TEXT
48
+ )
49
+ ''')
50
+
51
+ # 채널 통계 히스토리
52
+ c.execute('''
53
+ CREATE TABLE IF NOT EXISTS channel_stats (
54
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
55
+ channel_id TEXT,
56
+ subscribers INTEGER,
57
+ recorded_at TEXT,
58
+ FOREIGN KEY (channel_id) REFERENCES channels(channel_id)
59
+ )
60
+ ''')
61
+
62
+ # 검색 기록 테이블
63
+ c.execute('''
64
+ CREATE TABLE IF NOT EXISTS search_history (
65
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
66
+ keyword TEXT,
67
+ country TEXT,
68
+ language TEXT,
69
+ sort_by TEXT,
70
+ results_count INTEGER,
71
+ searched_at TEXT
72
+ )
73
+ ''')
74
+
75
+ # 급상승 알림 테이블
76
+ c.execute('''
77
+ CREATE TABLE IF NOT EXISTS trending_alerts (
78
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
79
+ video_id TEXT,
80
+ alert_type TEXT,
81
+ old_value INTEGER,
82
+ new_value INTEGER,
83
+ change_percent REAL,
84
+ detected_at TEXT,
85
+ FOREIGN KEY (video_id) REFERENCES videos(video_id)
86
+ )
87
+ ''')
88
+
89
+ conn.commit()
90
+ conn.close()
91
+
92
+ init_db()
93
+
94
  # 국가 코드 - 인구 순 상위 40개
95
  COUNTRIES = {
96
  "전세계": "",
 
181
  "노르웨이어": "no",
182
  }
183
 
 
184
  SORT_OPTIONS = {
185
  "조회수 순": "viewCount",
186
  "최신순": "date",
 
188
  "평점 순": "rating",
189
  }
190
 
 
191
  DATE_OPTIONS = {
192
  "전체 기간": "",
193
  "오늘": "today",
 
196
  "올해": "thisYear",
197
  }
198
 
199
+ def format_count(count):
200
+ if count >= 1000000000:
201
+ return f"{count/1000000000:.1f}B"
202
+ elif count >= 1000000:
203
+ return f"{count/1000000:.1f}M"
204
+ elif count >= 1000:
205
+ return f"{count/1000:.1f}K"
206
+ return str(count)
207
+
208
+ def save_to_db(videos_data, channels_data, keyword, country, language, sort_by):
209
+ """검색 결과를 DB에 저장"""
210
+ conn = sqlite3.connect("youtube_data.db")
211
+ c = conn.cursor()
212
+ now = datetime.now().isoformat()
213
+
214
+ # 검색 기록 저장
215
+ c.execute('''
216
+ INSERT INTO search_history (keyword, country, language, sort_by, results_count, searched_at)
217
+ VALUES (?, ?, ?, ?, ?, ?)
218
+ ''', (keyword, country, language, sort_by, len(videos_data), now))
219
+
220
+ for video in videos_data:
221
+ # 영상 기본 정보 저장/업데이트
222
+ c.execute('''
223
+ INSERT OR IGNORE INTO videos (video_id, title, channel_id, channel_name, thumbnail, published_at, first_seen)
224
+ VALUES (?, ?, ?, ?, ?, ?, ?)
225
+ ''', (video['video_id'], video['title'], video['channel_id'], video['channel_name'],
226
+ video['thumbnail'], video['published_at'], now))
227
+
228
+ # 이전 통계 가져오기 (급상승 감지용)
229
+ c.execute('''
230
+ SELECT views FROM video_stats
231
+ WHERE video_id = ?
232
+ ORDER BY recorded_at DESC LIMIT 1
233
+ ''', (video['video_id'],))
234
+ prev_stats = c.fetchone()
235
+
236
+ # 영상 통계 저장
237
+ c.execute('''
238
+ INSERT INTO video_stats (video_id, views, likes, comments, recorded_at)
239
+ VALUES (?, ?, ?, ?, ?)
240
+ ''', (video['video_id'], video['views'], video['likes'], video['comments'], now))
241
+
242
+ # 급상승 감지 (조회수 20% 이상 증가)
243
+ if prev_stats and prev_stats[0] > 0:
244
+ change_percent = ((video['views'] - prev_stats[0]) / prev_stats[0]) * 100
245
+ if change_percent >= 20:
246
+ c.execute('''
247
+ INSERT INTO trending_alerts (video_id, alert_type, old_value, new_value, change_percent, detected_at)
248
+ VALUES (?, ?, ?, ?, ?, ?)
249
+ ''', (video['video_id'], 'views_surge', prev_stats[0], video['views'], change_percent, now))
250
+
251
+ for channel_id, subs in channels_data.items():
252
+ # 채널 정보 저장
253
+ c.execute('''
254
+ INSERT OR IGNORE INTO channels (channel_id, channel_name, first_seen)
255
+ VALUES (?, ?, ?)
256
+ ''', (channel_id, '', now))
257
+
258
+ # 채널 통계 저장
259
+ if isinstance(subs, int):
260
+ c.execute('''
261
+ INSERT INTO channel_stats (channel_id, subscribers, recorded_at)
262
+ VALUES (?, ?, ?)
263
+ ''', (channel_id, subs, now))
264
+
265
+ conn.commit()
266
+ conn.close()
267
+
268
+ def get_trending_alerts():
269
+ """최근 급상승 알림 가져오기"""
270
+ conn = sqlite3.connect("youtube_data.db")
271
+ c = conn.cursor()
272
+
273
+ c.execute('''
274
+ SELECT ta.video_id, v.title, v.channel_name, ta.alert_type,
275
+ ta.old_value, ta.new_value, ta.change_percent, ta.detected_at
276
+ FROM trending_alerts ta
277
+ JOIN videos v ON ta.video_id = v.video_id
278
+ ORDER BY ta.detected_at DESC
279
+ LIMIT 20
280
+ ''')
281
+
282
+ alerts = c.fetchall()
283
+ conn.close()
284
+ return alerts
285
+
286
+ def get_search_history():
287
+ """검색 기록 가져오기"""
288
+ conn = sqlite3.connect("youtube_data.db")
289
+ c = conn.cursor()
290
+
291
+ c.execute('''
292
+ SELECT keyword, country, language, sort_by, results_count, searched_at
293
+ FROM search_history
294
+ ORDER BY searched_at DESC
295
+ LIMIT 50
296
+ ''')
297
+
298
+ history = c.fetchall()
299
+ conn.close()
300
+ return history
301
+
302
+ def get_video_history(video_id):
303
+ """특정 영상의 통계 히스토리"""
304
+ conn = sqlite3.connect("youtube_data.db")
305
+ c = conn.cursor()
306
+
307
+ c.execute('''
308
+ SELECT views, likes, comments, recorded_at
309
+ FROM video_stats
310
+ WHERE video_id = ?
311
+ ORDER BY recorded_at ASC
312
+ ''', (video_id,))
313
+
314
+ history = c.fetchall()
315
+ conn.close()
316
+ return history
317
+
318
+ def get_top_growing_videos(hours=24):
319
+ """급성장 영상 TOP 10"""
320
+ conn = sqlite3.connect("youtube_data.db")
321
+ c = conn.cursor()
322
+
323
+ cutoff = (datetime.now() - timedelta(hours=hours)).isoformat()
324
+
325
+ c.execute('''
326
+ SELECT v.video_id, v.title, v.channel_name, v.thumbnail,
327
+ MIN(vs.views) as min_views, MAX(vs.views) as max_views,
328
+ ((MAX(vs.views) - MIN(vs.views)) * 100.0 / MIN(vs.views)) as growth_rate
329
+ FROM videos v
330
+ JOIN video_stats vs ON v.video_id = vs.video_id
331
+ WHERE vs.recorded_at > ?
332
+ GROUP BY v.video_id
333
+ HAVING min_views > 0 AND max_views > min_views
334
+ ORDER BY growth_rate DESC
335
+ LIMIT 10
336
+ ''', (cutoff,))
337
+
338
+ results = c.fetchall()
339
+ conn.close()
340
+ return results
341
+
342
+ def get_db_stats():
343
+ """DB 통계"""
344
+ conn = sqlite3.connect("youtube_data.db")
345
+ c = conn.cursor()
346
+
347
+ c.execute("SELECT COUNT(*) FROM videos")
348
+ video_count = c.fetchone()[0]
349
+
350
+ c.execute("SELECT COUNT(*) FROM video_stats")
351
+ stats_count = c.fetchone()[0]
352
+
353
+ c.execute("SELECT COUNT(*) FROM channels")
354
+ channel_count = c.fetchone()[0]
355
+
356
+ c.execute("SELECT COUNT(*) FROM search_history")
357
+ search_count = c.fetchone()[0]
358
+
359
+ c.execute("SELECT COUNT(*) FROM trending_alerts")
360
+ alert_count = c.fetchone()[0]
361
+
362
+ conn.close()
363
+
364
+ return {
365
+ "videos": video_count,
366
+ "stats_records": stats_count,
367
+ "channels": channel_count,
368
+ "searches": search_count,
369
+ "alerts": alert_count
370
+ }
371
+
372
  def get_trending_keywords(region_code="KR"):
 
373
  try:
374
  response = youtube.videos().list(
375
  part="snippet",
 
381
  keywords = []
382
  seen = set()
383
  for item in response.get("items", []):
 
384
  channel = item["snippet"]["channelTitle"]
385
+ keyword = channel.split()[0] if len(channel.split()) > 0 else item["snippet"]["title"].split()[0]
386
  if keyword not in seen and len(keyword) > 1:
387
  keywords.append(keyword)
388
  seen.add(keyword)
 
394
  return get_default_trending()
395
 
396
  def get_default_trending():
 
397
  return [
398
  "AI", "ChatGPT", "뉴진스", "BTS", "Netflix",
399
  "게임", "먹방", "브이로그", "shorts", "음악",
400
  "축구", "NBA", "애니메이션", "영화 리뷰", "코딩"
401
  ]
402
 
 
 
 
 
 
 
 
 
 
403
  def update_trending(country):
 
404
  region = COUNTRIES.get(country, "")
405
  keywords = get_trending_keywords(region if region else "US")
406
  return gr.update(choices=keywords, value=None)
407
 
408
  def use_trending_keyword(trending_keyword):
 
409
  return trending_keyword if trending_keyword else ""
410
 
411
+ def search_videos(keyword, country, language, sort_by, date_filter, max_results, save_db=True):
412
  if not keyword or not keyword.strip():
413
+ return "검색어를 입력하세요.", ""
414
 
415
  max_results = int(max_results)
416
  all_items = []
 
429
  search_params["relevanceLanguage"] = LANGUAGES[language]
430
 
431
  if date_filter != "전체 기간":
 
432
  now = datetime.utcnow()
433
  if date_filter == "오늘":
434
  after = now - timedelta(days=1)
 
458
  break
459
 
460
  if not all_items:
461
+ return "검색 결과가 없습니다.", ""
462
 
463
  video_ids = [item["id"]["videoId"] for item in all_items]
464
  channel_ids = list(set([item["snippet"]["channelId"] for item in all_items]))
 
476
  video_stats[v["id"]] = {
477
  "views": int(stats.get("viewCount", 0)),
478
  "views_fmt": format_count(int(stats.get("viewCount", 0))),
479
+ "likes": int(stats.get("likeCount", 0)),
480
+ "likes_fmt": format_count(int(stats.get("likeCount", 0))),
481
+ "comments": int(stats.get("commentCount", 0)),
482
+ "comments_fmt": format_count(int(stats.get("commentCount", 0))),
483
  }
484
 
485
  channel_subs = {}
486
+ channel_subs_raw = {}
487
  for i in range(0, len(channel_ids), 50):
488
  batch_ids = channel_ids[i:i+50]
489
  channel_details = youtube.channels().list(
 
492
  ).execute()
493
 
494
  for ch in channel_details.get("items", []):
495
+ sub_count = ch["statistics"].get("subscriberCount", "0")
496
+ if sub_count:
497
+ channel_subs_raw[ch["id"]] = int(sub_count)
498
+ channel_subs[ch["id"]] = format_count(int(sub_count))
499
+ else:
500
+ channel_subs[ch["id"]] = "비공개"
501
+
502
+ # DB 저장용 데이터 준비
503
+ videos_data = []
504
+ for item in all_items:
505
+ video_id = item["id"]["videoId"]
506
+ snippet = item["snippet"]
507
+ stats = video_stats.get(video_id, {})
508
+
509
+ videos_data.append({
510
+ "video_id": video_id,
511
+ "title": snippet["title"],
512
+ "channel_id": snippet["channelId"],
513
+ "channel_name": snippet["channelTitle"],
514
+ "thumbnail": snippet["thumbnails"]["default"]["url"],
515
+ "published_at": snippet["publishedAt"],
516
+ "views": stats.get("views", 0),
517
+ "likes": stats.get("likes", 0),
518
+ "comments": stats.get("comments", 0),
519
+ })
520
+
521
+ # DB에 저장
522
+ if save_db:
523
+ save_to_db(videos_data, channel_subs_raw, keyword, country, language, sort_by)
524
 
525
+ # HTML 테이블 생성
526
  html = f'''
527
  <style>
528
  .yt-table {{ width: 100%; border-collapse: collapse; font-size: 14px; }}
 
536
  .yt-views {{ font-weight: bold; color: #065fd4; }}
537
  .yt-subs {{ color: #606060; }}
538
  .yt-date {{ color: #888; font-size: 12px; }}
539
+ .surge {{ background: #fff3cd !important; }}
540
  </style>
541
+ <p><b>총 {len(all_items)}개 결과</b> | 검색어: "{keyword}" | 정렬: {sort_by} | 💾 DB 저장 완료</p>
542
  <div style="max-height: 800px; overflow-y: auto;">
543
  <table class="yt-table">
544
  <thead>
 
575
 
576
  stats = video_stats.get(video_id, {})
577
  views = stats.get("views_fmt", "N/A")
578
+ likes = stats.get("likes_fmt", "N/A")
579
+ comments = stats.get("comments_fmt", "N/A")
580
  subs = channel_subs.get(channel_id, "N/A")
581
 
582
  if i == 1:
 
603
  '''
604
 
605
  html += '</tbody></table></div>'
606
+
607
+ # DB 통계 업데이트
608
+ db_stats = get_db_stats()
609
+ stats_html = f"📊 DB: 영상 {db_stats['videos']}개 | 기록 {db_stats['stats_records']}건 | 채널 {db_stats['channels']}개 | 검색 {db_stats['searches']}회 | 알림 {db_stats['alerts']}건"
610
+
611
+ return html, stats_html
612
+
613
+ def show_trending_alerts():
614
+ """급상승 알림 표시"""
615
+ alerts = get_trending_alerts()
616
+
617
+ if not alerts:
618
+ return "<p>아직 급상승 알림이 없습니다. 데이터를 더 수집해주세요.</p>"
619
+
620
+ html = '''
621
+ <style>
622
+ .alert-table { width: 100%; border-collapse: collapse; }
623
+ .alert-table th { background: #ff6b6b; color: white; padding: 10px; }
624
+ .alert-table td { padding: 10px; border-bottom: 1px solid #ddd; }
625
+ .surge-badge { background: #ff0000; color: white; padding: 2px 8px; border-radius: 10px; font-size: 12px; }
626
+ </style>
627
+ <h3>🔥 급상승 알림 (조회수 20% 이상 증가)</h3>
628
+ <table class="alert-table">
629
+ <thead>
630
+ <tr>
631
+ <th>영상</th>
632
+ <th>채널</th>
633
+ <th>이전</th>
634
+ <th>현재</th>
635
+ <th>증가율</th>
636
+ <th>감지 시간</th>
637
+ </tr>
638
+ </thead>
639
+ <tbody>
640
+ '''
641
+
642
+ for alert in alerts:
643
+ video_id, title, channel, alert_type, old_val, new_val, change_pct, detected = alert
644
+ title = title[:30] + "..." if len(title) > 30 else title
645
+ url = f"https://youtube.com/watch?v={video_id}"
646
+ detected_fmt = detected[:16].replace("T", " ")
647
+
648
+ html += f'''
649
+ <tr>
650
+ <td><a href="{url}" target="_blank">{title}</a></td>
651
+ <td>{channel}</td>
652
+ <td>{format_count(old_val)}</td>
653
+ <td>{format_count(new_val)}</td>
654
+ <td><span class="surge-badge">+{change_pct:.1f}%</span></td>
655
+ <td>{detected_fmt}</td>
656
+ </tr>
657
+ '''
658
+
659
+ html += '</tbody></table>'
660
  return html
661
 
662
+ def show_top_growing():
663
+ """급성장 영상 TOP 10"""
664
+ results = get_top_growing_videos(24)
665
+
666
+ if not results:
667
+ return "<p>최근 24시간 내 급성장 영상 데이터가 없습니다.</p>"
668
+
669
+ html = '''
670
+ <h3>📈 24시간 급성장 TOP 10</h3>
671
+ <div style="display: flex; flex-wrap: wrap; gap: 15px;">
672
+ '''
673
+
674
+ for i, (video_id, title, channel, thumbnail, min_views, max_views, growth) in enumerate(results, 1):
675
+ title = title[:25] + "..." if len(title) > 25 else title
676
+ url = f"https://youtube.com/watch?v={video_id}"
677
+
678
+ html += f'''
679
+ <div style="width: 180px; border: 1px solid #ddd; border-radius: 8px; padding: 10px;">
680
+ <a href="{url}" target="_blank">
681
+ <img src="{thumbnail}" style="width: 100%; border-radius: 5px;">
682
+ </a>
683
+ <p style="margin: 5px 0; font-size: 12px;"><b>{i}.</b> {title}</p>
684
+ <p style="margin: 0; font-size: 11px; color: #666;">{channel}</p>
685
+ <p style="margin: 5px 0; font-size: 14px; color: #ff0000; font-weight: bold;">📈 +{growth:.1f}%</p>
686
+ <p style="margin: 0; font-size: 10px; color: #888;">{format_count(min_views)} → {format_count(max_views)}</p>
687
+ </div>
688
+ '''
689
+
690
+ html += '</div>'
691
+ return html
692
 
693
+ def show_search_history():
694
+ """검색 기록 표시"""
695
+ history = get_search_history()
696
 
697
+ if not history:
698
+ return "<p>검색 기록이 없습니다.</p>"
 
 
 
 
699
 
700
+ html = '''
701
+ <h3>🕐 최근 검색 기록</h3>
702
+ <table style="width:100%; border-collapse: collapse;">
703
+ <thead>
704
+ <tr style="background: #333; color: white;">
705
+ <th style="padding: 10px;">검색어</th>
706
+ <th style="padding: 10px;">국가</th>
707
+ <th style="padding: 10px;">언어</th>
708
+ <th style="padding: 10px;">정렬</th>
709
+ <th style="padding: 10px;">결과 수</th>
710
+ <th style="padding: 10px;">검색 시간</th>
711
+ </tr>
712
+ </thead>
713
+ <tbody>
714
+ '''
715
 
716
+ for keyword, country, language, sort_by, count, searched_at in history:
717
+ searched_fmt = searched_at[:16].replace("T", " ")
718
+ html += f'''
719
+ <tr style="border-bottom: 1px solid #ddd;">
720
+ <td style="padding: 8px;"><b>{keyword}</b></td>
721
+ <td style="padding: 8px;">{country}</td>
722
+ <td style="padding: 8px;">{language}</td>
723
+ <td style="padding: 8px;">{sort_by}</td>
724
+ <td style="padding: 8px;">{count}</td>
725
+ <td style="padding: 8px;">{searched_fmt}</td>
726
+ </tr>
727
+ '''
728
 
729
+ html += '</tbody></table>'
730
+ return html
731
+
732
+ def refresh_all():
733
+ """전체 데이터 갱신"""
734
+ db_stats = get_db_stats()
735
+ alerts = show_trending_alerts()
736
+ growing = show_top_growing()
737
+ history = show_search_history()
738
+ stats_html = f"📊 DB: 영상 {db_stats['videos']}개 | 기록 {db_stats['stats_records']}건 | 채널 {db_stats['channels']}개 | 검색 {db_stats['searches']}회 | 알림 {db_stats['alerts']}건"
739
 
740
+ return alerts, growing, history, stats_html
741
+
742
+ # 초기 트렌딩 키워드
743
+ initial_trending = get_default_trending()
744
+
745
+ with gr.Blocks() as demo:
746
+ gr.Markdown("## YouTube 인기 영상 검색기 + 트렌드 분석")
747
+
748
+ # DB 통계
749
+ db_stats_display = gr.Markdown("📊 DB 통계 로딩중...")
750
 
751
+ with gr.Tabs():
752
+ # 탭 1: 검색
753
+ with gr.Tab("🔍 검색"):
754
+ gr.Markdown("### 🔥 실시간 인기 검색어")
755
+ trending_radio = gr.Radio(
756
+ choices=initial_trending,
757
+ label="클릭하면 검색창에 자동 입력",
758
+ interactive=True
759
+ )
760
+
761
+ with gr.Row():
762
+ keyword = gr.Textbox(label="검색어", placeholder="검색어 입력...", scale=3)
763
+ btn = gr.Button("🔍 검색", variant="primary", scale=1)
764
+ refresh_btn = gr.Button("🔄 갱신", scale=1)
765
+
766
+ with gr.Row():
767
+ country = gr.Dropdown(list(COUNTRIES.keys()), value="한국", label="국가 (인구순)")
768
+ language = gr.Dropdown(list(LANGUAGES.keys()), value="전체", label="언어 (사용인구순)")
769
+ sort_by = gr.Dropdown(list(SORT_OPTIONS.keys()), value="조회수 순", label="정렬")
770
+ date_filter = gr.Dropdown(list(DATE_OPTIONS.keys()), value="전체 기간", label="기간")
771
+ max_results = gr.Slider(10, 300, value=50, step=10, label="결과 수")
772
+
773
+ search_output = gr.HTML()
774
+
775
+ # 탭 2: 급상승
776
+ with gr.Tab("🔥 급상승"):
777
+ refresh_alerts_btn = gr.Button("🔄 새로고침", variant="secondary")
778
+ alerts_output = gr.HTML()
779
+
780
+ # 탭 3: 급성장
781
+ with gr.Tab("📈 급성장 TOP"):
782
+ refresh_growing_btn = gr.Button("🔄 새로고침", variant="secondary")
783
+ growing_output = gr.HTML()
784
+
785
+ # 탭 4: 검색 기록
786
+ with gr.Tab("🕐 검색 기록"):
787
+ refresh_history_btn = gr.Button("🔄 새로고침", variant="secondary")
788
+ history_output = gr.HTML()
789
+
790
+ # 이벤트 연결
791
  trending_radio.change(use_trending_keyword, trending_radio, keyword)
792
  country.change(update_trending, country, trending_radio)
793
+
794
+ btn.click(
795
+ search_videos,
796
+ [keyword, country, language, sort_by, date_filter, max_results],
797
+ [search_output, db_stats_display]
798
+ )
799
+ keyword.submit(
800
+ search_videos,
801
+ [keyword, country, language, sort_by, date_filter, max_results],
802
+ [search_output, db_stats_display]
803
+ )
804
+
805
+ refresh_btn.click(
806
+ search_videos,
807
+ [keyword, country, language, sort_by, date_filter, max_results],
808
+ [search_output, db_stats_display]
809
+ )
810
+
811
+ refresh_alerts_btn.click(show_trending_alerts, outputs=alerts_output)
812
+ refresh_growing_btn.click(show_top_growing, outputs=growing_output)
813
+ refresh_history_btn.click(show_search_history, outputs=history_output)
814
 
815
  demo.launch()