seawolf2357 commited on
Commit
bbfa293
·
verified ·
1 Parent(s): 0c29456

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +250 -79
app.py CHANGED
@@ -5,29 +5,94 @@ import os
5
  API_KEY = os.getenv("YOUTUBE_API_KEY")
6
  youtube = build("youtube", "v3", developerKey=API_KEY)
7
 
8
- # 국가 코드
9
  COUNTRIES = {
10
  "전세계": "",
11
- "한국": "KR",
 
12
  "미국": "US",
 
 
 
 
 
 
 
13
  "일본": "JP",
14
- "영국": "GB",
 
 
 
 
 
 
15
  "독일": "DE",
 
 
16
  "프랑스": "FR",
17
- "인도": "IN",
18
- "브라질": "BR",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  }
20
 
21
- # 언어 코드
22
  LANGUAGES = {
23
  "전체": "",
24
- "한국어": "ko",
25
  "영어": "en",
26
- "일본어": "ja",
27
  "중국어": "zh",
 
28
  "스페인어": "es",
 
 
 
 
 
 
29
  "독일어": "de",
 
 
30
  "프랑스어": "fr",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  }
32
 
33
  # 정렬 기준
@@ -47,6 +112,40 @@ DATE_OPTIONS = {
47
  "올해": "thisYear",
48
  }
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  def format_count(count):
51
  if count >= 1000000000:
52
  return f"{count/1000000000:.1f}B"
@@ -56,17 +155,29 @@ def format_count(count):
56
  return f"{count/1000:.1f}K"
57
  return str(count)
58
 
 
 
 
 
 
 
 
 
 
 
59
  def search_videos(keyword, country, language, sort_by, date_filter, max_results):
60
- if not keyword.strip():
61
  return "검색어를 입력하세요."
62
 
63
- # 검색 요청
 
 
 
64
  search_params = {
65
  "q": keyword,
66
  "part": "snippet",
67
  "type": "video",
68
  "order": SORT_OPTIONS[sort_by],
69
- "maxResults": int(max_results),
70
  }
71
 
72
  if COUNTRIES[country]:
@@ -74,7 +185,6 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
74
  if LANGUAGES[language]:
75
  search_params["relevanceLanguage"] = LANGUAGES[language]
76
 
77
- # 기간 필터
78
  if date_filter != "전체 기간":
79
  from datetime import datetime, timedelta
80
  now = datetime.utcnow()
@@ -88,110 +198,171 @@ def search_videos(keyword, country, language, sort_by, date_filter, max_results)
88
  after = now - timedelta(days=365)
89
  search_params["publishedAfter"] = after.strftime("%Y-%m-%dT%H:%M:%SZ")
90
 
91
- response = youtube.search().list(**search_params).execute()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
- if not response.get("items"):
94
  return "검색 결과가 없습니다."
95
 
96
- # 비디오 ID 수집
97
- video_ids = [item["id"]["videoId"] for item in response["items"]]
98
- channel_ids = [item["snippet"]["channelId"] for item in response["items"]]
99
 
100
- # 비디오 상세 정보 (조회수, 좋아요 등)
101
- video_details = youtube.videos().list(
102
- id=",".join(video_ids),
103
- part="statistics,contentDetails"
104
- ).execute()
105
-
106
- # 채널 상세 정보 (구독자 수)
107
- channel_details = youtube.channels().list(
108
- id=",".join(list(set(channel_ids))),
109
- part="statistics"
110
- ).execute()
 
 
 
 
 
111
 
112
- # 채널 구독자 매핑
113
  channel_subs = {}
114
- for ch in channel_details.get("items", []):
115
- sub_count = ch["statistics"].get("subscriberCount", "비공개")
116
- if sub_count != "비공개":
117
- sub_count = format_count(int(sub_count))
118
- channel_subs[ch["id"]] = sub_count
 
 
 
 
 
 
 
119
 
120
- # 비디오 통계 매핑
121
- video_stats = {}
122
- for v in video_details.get("items", []):
123
- stats = v["statistics"]
124
- video_stats[v["id"]] = {
125
- "views": format_count(int(stats.get("viewCount", 0))),
126
- "likes": format_count(int(stats.get("likeCount", 0))),
127
- "comments": format_count(int(stats.get("commentCount", 0))),
128
- "duration": v["contentDetails"]["duration"],
129
- }
130
-
131
- # HTML 결과 생성
132
- html = '<div style="display:flex; flex-direction:column; gap:15px;">'
133
-
134
- for i, item in enumerate(response["items"], 1):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  video_id = item["id"]["videoId"]
136
  snippet = item["snippet"]
137
  channel_id = snippet["channelId"]
138
 
139
  title = snippet["title"]
 
 
140
  channel = snippet["channelTitle"]
141
- thumbnail = snippet["thumbnails"]["medium"]["url"]
 
 
142
  published = snippet["publishedAt"][:10]
143
  url = f"https://youtube.com/watch?v={video_id}"
 
144
 
145
  stats = video_stats.get(video_id, {})
146
- views = stats.get("views", "N/A")
147
  likes = stats.get("likes", "N/A")
148
  comments = stats.get("comments", "N/A")
149
  subs = channel_subs.get(channel_id, "N/A")
150
 
 
 
 
 
 
 
 
 
 
151
  html += f'''
152
- <div style="display:flex; gap:15px; padding:10px; border:1px solid #ddd; border-radius:8px; background:#f9f9f9;">
153
- <a href="{url}" target="_blank">
154
- <img src="{thumbnail}" style="width:200px; border-radius:5px;">
155
- </a>
156
- <div style="flex:1;">
157
- <a href="{url}" target="_blank" style="font-weight:bold; font-size:16px; color:#1a0dab; text-decoration:none;">
158
- {i}. {title}
159
- </a>
160
- <p style="margin:5px 0; color:#555;">
161
- <b>채널:</b> {channel} &nbsp;|&nbsp; <b>구독자:</b> {subs}
162
- </p>
163
- <p style="margin:5px 0; color:#333;">
164
- <b>조회수:</b> {views} &nbsp;|&nbsp;
165
- <b>좋아요:</b> {likes} &nbsp;|&nbsp;
166
- <b>댓글:</b> {comments}
167
- </p>
168
- <p style="margin:5px 0; color:#888; font-size:12px;">
169
- <b>게시일:</b> {published}
170
- </p>
171
- </div>
172
- </div>
173
  '''
174
 
175
- html += '</div>'
176
  return html
177
 
 
 
178
  with gr.Blocks() as demo:
179
  gr.Markdown("## YouTube 인기 영상 검색기")
180
 
 
 
 
 
 
 
 
 
 
181
  with gr.Row():
182
- keyword = gr.Textbox(label="검색어", placeholder="검색어 입력...")
183
- btn = gr.Button("검색", variant="primary")
184
 
185
  with gr.Row():
186
- country = gr.Dropdown(list(COUNTRIES.keys()), value="전세계", label="국가/지역")
187
- language = gr.Dropdown(list(LANGUAGES.keys()), value="전체", label="언어")
188
  sort_by = gr.Dropdown(list(SORT_OPTIONS.keys()), value="조회수 순", label="정렬")
189
  date_filter = gr.Dropdown(list(DATE_OPTIONS.keys()), value="전체 기간", label="기간")
190
- max_results = gr.Slider(5, 50, value=10, step=5, label="결과 수")
191
 
192
  output = gr.HTML()
193
 
 
 
194
  btn.click(search_videos, [keyword, country, language, sort_by, date_filter, max_results], output)
195
  keyword.submit(search_videos, [keyword, country, language, sort_by, date_filter, max_results], output)
196
 
197
- demo.launch()
 
5
  API_KEY = os.getenv("YOUTUBE_API_KEY")
6
  youtube = build("youtube", "v3", developerKey=API_KEY)
7
 
8
+ # 국가 코드 - 인구 순 상위 40개
9
  COUNTRIES = {
10
  "전세계": "",
11
+ "중국": "CN",
12
+ "인도": "IN",
13
  "미국": "US",
14
+ "인도네시아": "ID",
15
+ "파키스탄": "PK",
16
+ "브라질": "BR",
17
+ "나이지리아": "NG",
18
+ "방글라데시": "BD",
19
+ "러시아": "RU",
20
+ "멕시코": "MX",
21
  "일본": "JP",
22
+ "에티오피아": "ET",
23
+ "필리핀": "PH",
24
+ "이집트": "EG",
25
+ "베트남": "VN",
26
+ "콩고민주공화국": "CD",
27
+ "터키": "TR",
28
+ "이란": "IR",
29
  "독일": "DE",
30
+ "태국": "TH",
31
+ "영국": "GB",
32
  "프랑스": "FR",
33
+ "이탈리아": "IT",
34
+ "남아공": "ZA",
35
+ "탄자니아": "TZ",
36
+ "미얀마": "MM",
37
+ "한국": "KR",
38
+ "콜롬비아": "CO",
39
+ "케냐": "KE",
40
+ "스페인": "ES",
41
+ "아르헨티나": "AR",
42
+ "알제리": "DZ",
43
+ "우크라이나": "UA",
44
+ "이라크": "IQ",
45
+ "캐나다": "CA",
46
+ "폴란드": "PL",
47
+ "모로코": "MA",
48
+ "사우디": "SA",
49
+ "페루": "PE",
50
+ "말레이시아": "MY",
51
  }
52
 
53
+ # 언어 코드 - 사용 인구 순 상위 40개
54
  LANGUAGES = {
55
  "전체": "",
 
56
  "영어": "en",
 
57
  "중국어": "zh",
58
+ "힌디어": "hi",
59
  "스페인어": "es",
60
+ "아랍어": "ar",
61
+ "벵골어": "bn",
62
+ "포르투갈어": "pt",
63
+ "러시아어": "ru",
64
+ "일본어": "ja",
65
+ "펀자브어": "pa",
66
  "독일어": "de",
67
+ "인도네시아어": "id",
68
+ "페르시아어": "fa",
69
  "프랑스어": "fr",
70
+ "베트남어": "vi",
71
+ "터키어": "tr",
72
+ "한국어": "ko",
73
+ "타밀어": "ta",
74
+ "이탈리아어": "it",
75
+ "텔루구어": "te",
76
+ "마라티어": "mr",
77
+ "우르두어": "ur",
78
+ "태국어": "th",
79
+ "구자라트어": "gu",
80
+ "폴란드어": "pl",
81
+ "말라얄람어": "ml",
82
+ "칸나다어": "kn",
83
+ "우크라이나어": "uk",
84
+ "필리핀어": "tl",
85
+ "미얀마어": "my",
86
+ "네덜란드어": "nl",
87
+ "루마니아어": "ro",
88
+ "스와힐리어": "sw",
89
+ "그리스어": "el",
90
+ "체코어": "cs",
91
+ "헝가리어": "hu",
92
+ "스웨덴어": "sv",
93
+ "히브리어": "he",
94
+ "핀란드어": "fi",
95
+ "노르웨이어": "no",
96
  }
97
 
98
  # 정렬 기준
 
112
  "올해": "thisYear",
113
  }
114
 
115
+ def get_trending_keywords(region_code="KR"):
116
+ """YouTube 인기 동영상에서 트렌드 키워드 추출"""
117
+ try:
118
+ response = youtube.videos().list(
119
+ part="snippet",
120
+ chart="mostPopular",
121
+ regionCode=region_code if region_code else "US",
122
+ maxResults=20
123
+ ).execute()
124
+
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)
134
+ if len(keywords) >= 15:
135
+ break
136
+
137
+ return keywords if keywords else get_default_trending()
138
+ except:
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"
 
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 = []
174
+ next_page_token = None
175
+
176
  search_params = {
177
  "q": keyword,
178
  "part": "snippet",
179
  "type": "video",
180
  "order": SORT_OPTIONS[sort_by],
 
181
  }
182
 
183
  if COUNTRIES[country]:
 
185
  if LANGUAGES[language]:
186
  search_params["relevanceLanguage"] = LANGUAGES[language]
187
 
 
188
  if date_filter != "전체 기간":
189
  from datetime import datetime, timedelta
190
  now = datetime.utcnow()
 
198
  after = now - timedelta(days=365)
199
  search_params["publishedAfter"] = after.strftime("%Y-%m-%dT%H:%M:%SZ")
200
 
201
+ while len(all_items) < max_results:
202
+ search_params["maxResults"] = min(50, max_results - len(all_items))
203
+ if next_page_token:
204
+ search_params["pageToken"] = next_page_token
205
+
206
+ response = youtube.search().list(**search_params).execute()
207
+ items = response.get("items", [])
208
+
209
+ if not items:
210
+ break
211
+
212
+ all_items.extend(items)
213
+ next_page_token = response.get("nextPageToken")
214
+
215
+ if not next_page_token:
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]))
 
223
 
224
+ video_stats = {}
225
+ for i in range(0, len(video_ids), 50):
226
+ batch_ids = video_ids[i:i+50]
227
+ video_details = youtube.videos().list(
228
+ id=",".join(batch_ids),
229
+ part="statistics"
230
+ ).execute()
231
+
232
+ for v in video_details.get("items", []):
233
+ stats = v["statistics"]
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(
245
+ id=",".join(batch_ids),
246
+ part="statistics"
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; }}
258
+ .yt-table th {{ background: #ff0000; color: white; padding: 12px 8px; text-align: left; position: sticky; top: 0; }}
259
+ .yt-table td {{ padding: 10px 8px; border-bottom: 1px solid #ddd; vertical-align: middle; }}
260
+ .yt-table tr:hover {{ background: #f5f5f5; }}
261
+ .yt-table img {{ border-radius: 4px; }}
262
+ .yt-table a {{ color: #1a0dab; text-decoration: none; }}
263
+ .yt-table a:hover {{ text-decoration: underline; }}
264
+ .yt-rank {{ font-weight: bold; color: #ff0000; text-align: center; }}
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>
273
+ <tr>
274
+ <th style="width:50px;">순위</th>
275
+ <th style="width:120px;">썸네일</th>
276
+ <th>제목</th>
277
+ <th style="width:150px;">채널</th>
278
+ <th style="width:80px;">구독자</th>
279
+ <th style="width:80px;">조회수</th>
280
+ <th style="width:60px;">좋아요</th>
281
+ <th style="width:60px;">댓글</th>
282
+ <th style="width:90px;">게시일</th>
283
+ </tr>
284
+ </thead>
285
+ <tbody>
286
+ '''
287
+
288
+ for i, item in enumerate(all_items, 1):
289
  video_id = item["id"]["videoId"]
290
  snippet = item["snippet"]
291
  channel_id = snippet["channelId"]
292
 
293
  title = snippet["title"]
294
+ if len(title) > 50:
295
+ title = title[:50] + "..."
296
  channel = snippet["channelTitle"]
297
+ if len(channel) > 15:
298
+ channel = channel[:15] + "..."
299
+ thumbnail = snippet["thumbnails"]["default"]["url"]
300
  published = snippet["publishedAt"][:10]
301
  url = f"https://youtube.com/watch?v={video_id}"
302
+ channel_url = f"https://youtube.com/channel/{channel_id}"
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:
311
+ rank_style = "color: #FFD700; font-size: 18px;"
312
+ elif i == 2:
313
+ rank_style = "color: #C0C0C0; font-size: 16px;"
314
+ elif i == 3:
315
+ rank_style = "color: #CD7F32; font-size: 16px;"
316
+ else:
317
+ rank_style = ""
318
+
319
  html += f'''
320
+ <tr>
321
+ <td class="yt-rank" style="{rank_style}">{i}</td>
322
+ <td><a href="{url}" target="_blank"><img src="{thumbnail}" width="100"></a></td>
323
+ <td><a href="{url}" target="_blank">{title}</a></td>
324
+ <td><a href="{channel_url}" target="_blank">{channel}</a></td>
325
+ <td class="yt-subs">{subs}</td>
326
+ <td class="yt-views">{views}</td>
327
+ <td>{likes}</td>
328
+ <td>{comments}</td>
329
+ <td class="yt-date">{published}</td>
330
+ </tr>
 
 
 
 
 
 
 
 
 
 
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()