izuemon commited on
Commit
f92b0ea
·
verified ·
1 Parent(s): 1145bc0

Update yt.py

Browse files
Files changed (1) hide show
  1. yt.py +52 -323
yt.py CHANGED
@@ -4,7 +4,6 @@ import time
4
  import json
5
  import requests
6
  from datetime import datetime, timezone
7
- from bs4 import BeautifulSoup
8
 
9
  # ===== Channel.io 設定 =====
10
  GET_URL = "https://desk-api.channel.io/desk/channels/200605/groups/519217/messages"
@@ -33,9 +32,6 @@ HEADERS_POST = {
33
  "x-account": X_ACCOUNT,
34
  }
35
 
36
- # ===== ssyoutube =====
37
- SSYOUTUBE_URL = "https://ssyoutube.online/yt-video-detail/"
38
-
39
  # ===== Utils =====
40
  def parse_updated_at(value):
41
  if isinstance(value, (int, float)):
@@ -46,15 +42,10 @@ def parse_updated_at(value):
46
 
47
  def extract_youtube_id(text):
48
  patterns = [
49
- # クエリパラメータ v= または vi=
50
  r"(?:[?&](?:v|vi)=)([A-Za-z0-9_-]{11})",
51
- # youtu.be 短縮リンク
52
  r"youtu\.be/([A-Za-z0-9_-]{11})",
53
- # /embed/ /v/ /shorts/
54
  r"youtube(?:-nocookie)?\.com/(?:embed|v|shorts)/([A-Za-z0-9_-]{11})",
55
- # watch? の中に v= が含まれるケース(ioパラメータ等が混ざっても OK)
56
  r"youtube(?:-nocookie)?\.com/watch\?.*?v=([A-Za-z0-9_-]{11})",
57
- # hash や他パラメータに混ざるケース: ...#v=xxxx or &v=xxxx
58
  r"[#&]v=([A-Za-z0-9_-]{11})"
59
  ]
60
  for p in patterns:
@@ -64,29 +55,17 @@ def extract_youtube_id(text):
64
  return None
65
 
66
  def extract_youtube_and_option(text):
67
- """
68
- YouTube URL と画質指定オプションを抽出する
69
- 戻り値: (video_id, option) 例: ("abcdEFGHijk", "720")
70
- """
71
- # 全角スペースも対象に
72
  parts = re.split(r"[ \u3000]+", text.strip())
73
-
74
  video_id = extract_youtube_id(parts[0])
75
  option = None
76
  if len(parts) > 1:
77
  option = parts[1].lower()
78
-
79
  return video_id, option
80
 
81
  def normalize_quality(option):
82
- """
83
- ★ ユーザー入力を内部品質コードへ変換(全角も対応)
84
- """
85
  if option is None:
86
  return "720"
87
 
88
- # 全角 → 半角 に変換
89
- # 例: "720" → "720", "4k" → "4k"
90
  trans_table = str.maketrans(
91
  "0123456789kKpP",
92
  "0123456789kKpP"
@@ -123,218 +102,21 @@ def normalize_quality(option):
123
  if option in quality_map:
124
  return quality_map[option]
125
 
126
- if option in ["a", "f"]:
127
- return option # 特殊指定
128
-
129
- return "720" # デフォルト
130
-
131
- def get_video_title(youtube_url):
132
- """YouTube動画のタイトルを取得する簡易的な関数"""
133
- try:
134
- res = requests.get(youtube_url, timeout=10)
135
- soup = BeautifulSoup(res.text, "lxml")
136
- title_tag = soup.find("meta", property="og:title")
137
- if title_tag:
138
- return title_tag.get("content", "").replace(" - YouTube", "")
139
- except:
140
- pass
141
- return "YouTube動画"
142
-
143
- # ===== ssyoutube HTML 解析 =====
144
- def fetch_download_links(youtube_url):
145
- res = requests.post(
146
- SSYOUTUBE_URL,
147
- data={"videoURL": youtube_url},
148
- timeout=20,
149
- headers={
150
- "User-Agent": "Mozilla/5.0",
151
- "Referer": "https://ssyoutube.online/",
152
- }
153
- )
154
- res.raise_for_status()
155
-
156
- # nonceを抽出
157
- soup = BeautifulSoup(res.text, "lxml")
158
- nonce_match = re.search(r"formData\.append\('nonce', '([^']+)'\);", res.text)
159
- nonce = nonce_match.group(1) if nonce_match else None
160
-
161
- buttons = soup.select("button[data-url]")
162
-
163
- results = []
164
- for btn in buttons:
165
- url = btn.get("data-url")
166
- quality = btn.get("data-quality")
167
- has_audio = btn.get("data-has-audio")
168
-
169
- if not url:
170
- continue
171
-
172
- results.append({
173
- "url": url,
174
- "quality": quality or "audio",
175
- "has_audio": has_audio,
176
- })
177
-
178
- return results, nonce
179
-
180
- # ===== 最高画質映像 + 音声を選択 =====
181
- def select_best_video_and_audio(items):
182
- video_items = []
183
- audio_items = []
184
-
185
- for item in items:
186
- if item["quality"] == "audio":
187
- audio_items.append(item)
188
- elif item["has_audio"] == "false":
189
- m = re.match(r"(\d+)p", item["quality"])
190
- if m:
191
- video_items.append((int(m.group(1)), item))
192
-
193
- if not video_items or not audio_items:
194
- return None, None
195
-
196
- # 最高画質の動画を選択
197
- video_items.sort(key=lambda x: x[0], reverse=True)
198
- best_video = video_items[0][1]
199
-
200
- # 最高音質の音声を選択(例: 128kbps > 64kbps)
201
- # 実際の音声品質情報に基づいて選択する場合は要修正
202
- best_audio = audio_items[0] if audio_items else None
203
-
204
- return best_video, best_audio
205
-
206
- def select_quality_video(items, quality):
207
- """
208
- ★ 指定画質に最適な動画を選択(音声なし映像のみ)
209
- """
210
- video_candidates = []
211
- for item in items:
212
- if item["has_audio"] == "false":
213
- m = re.match(r"(\d+)p", item["quality"])
214
- if m:
215
- q = int(m.group(1))
216
- video_candidates.append((q, item))
217
-
218
- if not video_candidates:
219
- return None
220
-
221
- # 最大画質
222
- video_candidates.sort(key=lambda x: x[0])
223
-
224
- if quality == "f": # 最大画質
225
- return video_candidates[-1][1]
226
-
227
- req = int(quality)
228
- best = None
229
-
230
- # 指定画質以上で一番近いもの
231
- for q, item in video_candidates:
232
- if q >= req:
233
- best = item
234
- break
235
-
236
- # 無ければ最大画質
237
- return best if best else video_candidates[-1][1]
238
-
239
- # ===== ssyoutube サーバー側結合 =====
240
- def merge_video_on_server(video_url, audio_url, video_id, quality, video_title, nonce):
241
- """
242
- ssyoutubeサーバーで動画と音声を結合する
243
- """
244
- # IDと画質からリクエストIDを生成
245
- request_id = f"{video_id}_{quality}p"
246
-
247
- # リクエストデータの構築
248
- request_data = {
249
- "id": request_id,
250
- "ttl": 3600000,
251
- "inputs": [
252
- {
253
- "url": video_url,
254
- "ext": "mp4",
255
- "chunkDownload": {
256
- "type": "header",
257
- "size": 52428800,
258
- "concurrency": 3
259
- }
260
- },
261
- {
262
- "url": audio_url,
263
- "ext": "m4a"
264
- }
265
- ],
266
- "output": {
267
- "ext": "mp4",
268
- "downloadName": f"{video_title}_{quality}p.mp4",
269
- "chunkUpload": {
270
- "size": 104857600,
271
- "concurrency": 3
272
- }
273
- },
274
- "operation": {
275
- "type": "replace_audio_in_video"
276
- }
277
- }
278
-
279
- # 結合開始リクエスト
280
- payload = {
281
- "action": "process_video_merge",
282
- "nonce": nonce,
283
- "request_data": json.dumps(request_data)
284
- }
285
-
286
- headers = {
287
- "User-Agent": "Mozilla/5.0",
288
- "Referer": "https://ssyoutube.online/",
289
- "Origin": "https://ssyoutube.online",
290
- "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
291
- }
292
-
293
- res = requests.post(
294
- "https://ssyoutube.online/wp-admin/admin-ajax.php",
295
- data=payload,
296
- headers=headers,
297
- timeout=60
298
  )
 
 
299
  res.raise_for_status()
300
-
301
- result = res.json()
302
-
303
- if not result.get("success") or not result.get("data", {}).get("success"):
304
- raise RuntimeError(f"結合開始に失敗しました: {result}")
305
-
306
- # ステータス監視用URLを取得
307
- monitor_url = result["data"]["result"]["monitor"]["http"]
308
-
309
- # 完了するまでポーリング
310
- max_attempts = 5 # 最大60回試行(30秒間隔で約30分)
311
- attempt = 0
312
-
313
- while attempt < max_attempts:
314
- time.sleep(20) # 30秒間隔でチェック
315
-
316
- status_res = requests.get(monitor_url, timeout=30)
317
- status_res.raise_for_status()
318
- status_data = status_res.json()
319
-
320
- if not status_data.get("success"):
321
- raise RuntimeError(f"ステータス取得に失敗しました: {status_data}")
322
-
323
- status = status_data["result"]["status"]
324
-
325
- if status == "done":
326
- # 完了したらダウンロードURLを返す
327
- return status_data["result"]["output"]["url"]
328
- elif status == "error":
329
- error_msg = status_data["result"]["error"]
330
- raise RuntimeError(f"結合処理でエラーが発生しました: {error_msg}")
331
- # "processing" の場合は継続
332
-
333
- attempt += 1
334
- print(f"結合処理中... 進捗: {status_data['result'].get('progress_in_percent', 0)}%")
335
-
336
- raise RuntimeError("結合処理がタイムアウトしました")
337
 
 
338
  def send_to_channel(text):
339
  payload = {
340
  "requestId": f"desk-web-{int(time.time() * 1000)}",
@@ -351,42 +133,34 @@ def send_to_channel(text):
351
  )
352
  res.raise_for_status()
353
 
354
-
355
- def upload_file_to_channel(file_url):
356
- # mp4バイナリ取得
357
- file_res = requests.get(file_url, timeout=60)
358
- file_res.raise_for_status()
359
-
360
  upload_url = "https://media.channel.io/cht/v1/pri-file/200605/groups/519217/message/send_yt_video_file.mp4"
361
 
362
  headers = {
363
  "x-account": X_ACCOUNT,
364
  "Content-Type": "video/mp4",
365
- "Content-Length": str(len(file_res.content)),
366
  }
367
 
368
-
369
  res = requests.post(
370
  upload_url,
371
  headers=headers,
372
- data=file_res.content,
373
- timeout=60
374
  )
375
-
376
  res.raise_for_status()
377
 
378
- return res.json() # ←これを messages POST の body.files に入れる
379
-
380
 
381
  def send_video_message(file_json):
382
  request_id = f"desk-web-{int(time.time() * 1000)}"
383
-
384
  payload = {
385
  "requestId": request_id,
386
  "blocks": [
387
  {"type": "text", "value": "プレビュー:"}
388
  ],
389
- "files": [file_json], # ←ここが重要!
390
  }
391
 
392
  res = requests.post(
@@ -399,8 +173,8 @@ def send_video_message(file_json):
399
 
400
  # ===== Main =====
401
  def main():
402
- processed_messages = set() # 処理済みメッセージを追跡
403
-
404
  while True:
405
  try:
406
  res = requests.get(
@@ -422,8 +196,7 @@ def main():
422
 
423
  if not plain_text or updated_at is None:
424
  continue
425
-
426
- # 既に処理済みのメッセージはスキップ
427
  if msg_id in processed_messages:
428
  continue
429
 
@@ -443,99 +216,55 @@ def main():
443
  if not video_id:
444
  time.sleep(10)
445
  continue
446
-
447
- valid_options = ["144", "240", "360", "480", "720", "1080", "1440", "2160", "4320", "4k", "8k", "2k", "a", "f"]
448
 
449
- # 画質入力チェックを normalize_quality の前にする
 
 
 
 
450
  if option is None:
451
  send_to_channel(
452
  "画質が指定されていないため、デフォルトの720pを使用します。\n"
453
  "指定する場合は、動画URLの後にスペースを入れて、"
454
- "144/240/360/480/720/1080/2k(または1440)/4k(または2160)/8k(または4320)/a(結合しない)/f(最高画質) のいずれかを入力してください。\n注意:最大画質や大きい画質、長い動画だと処理に時間がかかるからタイムアウトしやすいです。"
 
455
  )
 
456
  else:
457
- opt_lower = option.lower()
458
- if opt_lower not in valid_options:
459
  send_to_channel(
460
  "不明な画質、または予期しない形式でした。\n"
461
  "対応画質: 144, 240, 360, 480, 720, 1080, "
462
- "2k(1440), 4k(2160), 8k(4320), a(結合無効), f(最大画質)\n"
463
- "動画のURLの後にスペースを入れて指定してください。\n"
464
- "例: https://youtu.be/abcdEFGHijk 1080\n注意:最大画質や大きい画質、長い動画だと処理に時間がかかるからタイムアウトしやすいです。"
465
  )
466
  processed_messages.add(latest_msg["id"])
467
  continue
468
 
469
- quality_opt = normalize_quality(option)
470
-
471
- youtube_url = f"https://www.youtube.com/watch?v={video_id}"
472
- send_to_channel(f"{video_id} のダウンロードを開始します。")
473
-
474
- video_title = get_video_title(youtube_url)
475
- items, nonce = fetch_download_links(youtube_url)
476
-
477
- all_items = items[:]
478
- audio_items = [i for i in items if i["quality"] == "audio"]
479
-
480
- selected_video = None
481
- selected_audio = audio_items[0] if audio_items else None
482
-
483
- if quality_opt not in ["a"]:
484
- selected_video = select_quality_video(items, quality_opt)
485
-
486
- # ダウンロードリスト送信
487
- message_lines = []
488
- message_lines.append(f"<b>{video_title}</b>\n")
489
- message_lines.append("<b>ダウンロードリンク</b>")
490
-
491
- # すべての動画リンクを追加
492
- for item in all_items:
493
- quality = item["quality"]
494
- if item["quality"] == "audio":
495
- quality_display = "音声"
496
- elif item["has_audio"] == "true":
497
- quality_display = f"{quality} (音声付き)"
498
- else:
499
- quality_display = f"{quality} (映像のみ)"
500
-
501
- message_lines.append(f"<link type=\"url\" value=\"{item['url']}\"> {quality_display} </link>")
502
-
503
- message_lines.append("★動画ファイルと音声ファイルをダウンロードして<link type=\"url\" value=\"https://qooly.com/ja/merge-video-and-audio-online\">ここ</link>などで結合すると早くて確実です。")
504
- send_to_channel("\n".join(message_lines))
505
-
506
- # ★ 結合フラグ
507
- do_merge = quality_opt not in ["a"]
508
-
509
- if do_merge and selected_video and selected_audio:
510
- m = re.match(r"(\d+)p", selected_video["quality"])
511
- if m:
512
- q = m.group(1)
513
- send_to_channel(f"{q}pで音声との結合を開始しました。")
514
- try:
515
- merged_url = merge_video_on_server(
516
- selected_video["url"],
517
- selected_audio["url"],
518
- video_id,
519
- q,
520
- video_title,
521
- nonce
522
- )
523
- send_to_channel(f"<link type=\"url\" value=\"{merged_url}\">{q}p 結合済み動画</link>")
524
- file_json = upload_file_to_channel(merged_url)
525
- send_video_message(file_json)
526
- except Exception as e:
527
- send_to_channel(f"結合失敗: {e}")
528
- send_to_channel("完了しました!")
529
- processed_messages.add(latest_msg["id"])
530
 
531
- print(f"送信完了: {video_id}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
532
 
533
  except Exception as e:
534
  print("エラー:", e)
535
- import traceback
536
- traceback.print_exc()
537
 
538
  time.sleep(15)
539
 
540
  if __name__ == "__main__":
541
- main()
 
4
  import json
5
  import requests
6
  from datetime import datetime, timezone
 
7
 
8
  # ===== Channel.io 設定 =====
9
  GET_URL = "https://desk-api.channel.io/desk/channels/200605/groups/519217/messages"
 
32
  "x-account": X_ACCOUNT,
33
  }
34
 
 
 
 
35
  # ===== Utils =====
36
  def parse_updated_at(value):
37
  if isinstance(value, (int, float)):
 
42
 
43
  def extract_youtube_id(text):
44
  patterns = [
 
45
  r"(?:[?&](?:v|vi)=)([A-Za-z0-9_-]{11})",
 
46
  r"youtu\.be/([A-Za-z0-9_-]{11})",
 
47
  r"youtube(?:-nocookie)?\.com/(?:embed|v|shorts)/([A-Za-z0-9_-]{11})",
 
48
  r"youtube(?:-nocookie)?\.com/watch\?.*?v=([A-Za-z0-9_-]{11})",
 
49
  r"[#&]v=([A-Za-z0-9_-]{11})"
50
  ]
51
  for p in patterns:
 
55
  return None
56
 
57
  def extract_youtube_and_option(text):
 
 
 
 
 
58
  parts = re.split(r"[ \u3000]+", text.strip())
 
59
  video_id = extract_youtube_id(parts[0])
60
  option = None
61
  if len(parts) > 1:
62
  option = parts[1].lower()
 
63
  return video_id, option
64
 
65
  def normalize_quality(option):
 
 
 
66
  if option is None:
67
  return "720"
68
 
 
 
69
  trans_table = str.maketrans(
70
  "0123456789kKpP",
71
  "0123456789kKpP"
 
102
  if option in quality_map:
103
  return quality_map[option]
104
 
105
+ return None # 不正な指定
106
+
107
+ # ===== izu-ytdl API =====
108
+ def download_video_from_api(video_id, quality):
109
+ api_url = (
110
+ f"https://izu-ytdl-api.servernux.com/download"
111
+ f"?v={video_id}"
112
+ f"&q=bv*[height<={quality}]+ba/b"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  )
114
+
115
+ res = requests.get(api_url, timeout=300)
116
  res.raise_for_status()
117
+ return res.content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
+ # ===== Channel送信 =====
120
  def send_to_channel(text):
121
  payload = {
122
  "requestId": f"desk-web-{int(time.time() * 1000)}",
 
133
  )
134
  res.raise_for_status()
135
 
136
+ def upload_file_to_channel(file_bytes):
 
 
 
 
 
137
  upload_url = "https://media.channel.io/cht/v1/pri-file/200605/groups/519217/message/send_yt_video_file.mp4"
138
 
139
  headers = {
140
  "x-account": X_ACCOUNT,
141
  "Content-Type": "video/mp4",
142
+ "Content-Length": str(len(file_bytes)),
143
  }
144
 
 
145
  res = requests.post(
146
  upload_url,
147
  headers=headers,
148
+ data=file_bytes,
149
+ timeout=300
150
  )
 
151
  res.raise_for_status()
152
 
153
+ return res.json()
 
154
 
155
  def send_video_message(file_json):
156
  request_id = f"desk-web-{int(time.time() * 1000)}"
157
+
158
  payload = {
159
  "requestId": request_id,
160
  "blocks": [
161
  {"type": "text", "value": "プレビュー:"}
162
  ],
163
+ "files": [file_json],
164
  }
165
 
166
  res = requests.post(
 
173
 
174
  # ===== Main =====
175
  def main():
176
+ processed_messages = set()
177
+
178
  while True:
179
  try:
180
  res = requests.get(
 
196
 
197
  if not plain_text or updated_at is None:
198
  continue
199
+
 
200
  if msg_id in processed_messages:
201
  continue
202
 
 
216
  if not video_id:
217
  time.sleep(10)
218
  continue
 
 
219
 
220
+ valid_options = [
221
+ "144","240","360","480","720","1080",
222
+ "1440","2160","4320","4k","8k","2k"
223
+ ]
224
+
225
  if option is None:
226
  send_to_channel(
227
  "画質が指定されていないため、デフォルトの720pを使用します。\n"
228
  "指定する場合は、動画URLの後にスペースを入れて、"
229
+ "144/240/360/480/720/1080/2k(または1440)/4k(または2160)/8k(または4320) のいずれかを入力してください。\n"
230
+ "注意:高画質や長い動画は時間がかかる場合があります。"
231
  )
232
+ quality_opt = "720"
233
  else:
234
+ if option.lower() not in valid_options:
 
235
  send_to_channel(
236
  "不明な画質、または予期しない形式でした。\n"
237
  "対応画質: 144, 240, 360, 480, 720, 1080, "
238
+ "2k(1440), 4k(2160), 8k(4320)\n"
239
+ "例: https://youtu.be/abcdEFGHijk 1080"
 
240
  )
241
  processed_messages.add(latest_msg["id"])
242
  continue
243
 
244
+ quality_opt = normalize_quality(option)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
 
246
+ send_to_channel(f"{video_id} のダウンロードを開始します。({quality_opt}p)")
247
+
248
+ try:
249
+ video_bytes = download_video_from_api(video_id, quality_opt)
250
+
251
+ send_to_channel("動画をアップロードしています...")
252
+
253
+ file_json = upload_file_to_channel(video_bytes)
254
+
255
+ send_video_message(file_json)
256
+
257
+ send_to_channel("完了しました!")
258
+
259
+ except Exception as e:
260
+ send_to_channel(f"エラーが発生しました: {e}")
261
+
262
+ processed_messages.add(latest_msg["id"])
263
 
264
  except Exception as e:
265
  print("エラー:", e)
 
 
266
 
267
  time.sleep(15)
268
 
269
  if __name__ == "__main__":
270
+ main()