izuemon commited on
Commit
ec8aead
·
verified ·
1 Parent(s): f655bda

Update watcher.py

Browse files
Files changed (1) hide show
  1. watcher.py +267 -81
watcher.py CHANGED
@@ -8,6 +8,8 @@ import subprocess
8
  import requests
9
  from datetime import datetime, timezone
10
  from bs4 import BeautifulSoup
 
 
11
 
12
  # ===== Channel.io 設定 =====
13
  GET_URL = "https://desk-api.channel.io/desk/channels/200605/groups/519217/messages"
@@ -38,6 +40,7 @@ HEADERS_POST = {
38
 
39
  # ===== ssyoutube =====
40
  SSYOUTUBE_URL = "https://ssyoutube.online/yt-video-detail/"
 
41
 
42
  # ===== Utils =====
43
  def parse_updated_at(value):
@@ -72,13 +75,25 @@ def fetch_download_links(youtube_url):
72
  res.raise_for_status()
73
 
74
  soup = BeautifulSoup(res.text, "lxml")
 
 
 
 
 
 
 
 
 
 
 
75
  buttons = soup.select("button[data-url]")
76
-
77
  results = []
 
78
  for btn in buttons:
79
  url = btn.get("data-url")
80
  quality = btn.get("data-quality")
81
  has_audio = btn.get("data-has-audio")
 
82
 
83
  if not url:
84
  continue
@@ -87,6 +102,9 @@ def fetch_download_links(youtube_url):
87
  "url": url,
88
  "quality": quality or "audio",
89
  "has_audio": has_audio,
 
 
 
90
  })
91
 
92
  return results
@@ -110,44 +128,138 @@ def select_best_video_and_audio(items):
110
  video_items.sort(key=lambda x: x[0], reverse=True)
111
  return video_items[0][1], audio_item
112
 
 
 
 
113
 
114
- HEADERS = {
115
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
116
- "AppleWebKit/537.36 (KHTML, like Gecko) "
117
- "Chrome/131.0.0.0 Safari/537.36",
118
- "Accept": "*/*",
119
- "Accept-Language": "ja,en-US;q=0.9,en;q=0.8",
120
- "Referer": "https://www.youtube.com/",
121
- "Origin": "https://www.youtube.com",
122
- "Range": "bytes=0-",
123
- }
124
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
- def download_file(url, path):
127
- with requests.get(
128
- url,
129
- headers=HEADERS,
130
- stream=True,
131
- timeout=60,
132
- ) as r:
133
- r.raise_for_status()
134
- with open(path, "wb") as f:
135
- for chunk in r.iter_content(chunk_size=8192):
136
- if chunk:
137
- f.write(chunk)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
- # ===== ffmpeg 結合 =====
140
- def merge_video_audio(video_path, audio_path, output_path):
141
- cmd = [
142
- "ffmpeg",
143
- "-y",
144
- "-i", video_path,
145
- "-i", audio_path,
146
- "-c:v", "copy",
147
- "-c:a", "aac",
148
- output_path
149
- ]
150
- subprocess.run(cmd, check=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
  # ===== file.io アップロード =====
153
  def upload_to_fileio(file_path):
@@ -175,8 +287,70 @@ def send_to_channel(text):
175
  )
176
  res.raise_for_status()
177
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  # ===== Main =====
179
  def main():
 
 
180
  while True:
181
  try:
182
  res = requests.get(
@@ -188,58 +362,70 @@ def main():
188
  res.raise_for_status()
189
 
190
  messages = res.json().get("messages", [])
191
- latest_msg = None
192
- latest_time = None
193
-
194
  for msg in messages:
 
195
  plain_text = msg.get("plainText")
196
- updated_at = msg.get("updatedAt")
197
-
198
- if not plain_text or updated_at is None:
199
  continue
200
-
201
- t = parse_updated_at(updated_at)
202
- if not t:
203
  continue
204
-
205
- if latest_time is None or t > latest_time:
206
- latest_time = t
207
- latest_msg = msg
208
-
209
- if not latest_msg:
210
- time.sleep(10)
211
- continue
212
-
213
- youtube_id = extract_youtube_id(latest_msg["plainText"])
214
- if not youtube_id:
215
- time.sleep(10)
216
- continue
217
-
218
- youtube_url = f"https://www.youtube.com/watch?v={youtube_id}"
219
- items = fetch_download_links(youtube_url)
220
-
221
- video_item, audio_item = select_best_video_and_audio(items)
222
- if not video_item:
223
- time.sleep(10)
224
- continue
225
-
226
- with tempfile.TemporaryDirectory() as tmp:
227
- video_path = os.path.join(tmp, "video.mp4")
228
- audio_path = os.path.join(tmp, "audio.m4a")
229
- output_path = os.path.join(tmp, "merged.mp4")
230
-
231
- download_file(video_item["url"], video_path)
232
- download_file(audio_item["url"], audio_path)
233
-
234
- merge_video_audio(video_path, audio_path, output_path)
235
-
236
- link = upload_to_fileio(output_path)
237
- send_to_channel(f"🎬 結合済み動画(最高画質)\n{link}")
238
-
239
- print("送信完了:", link)
 
 
 
 
 
 
 
 
 
 
 
240
 
241
  except Exception as e:
242
  print("エラー:", e)
 
 
243
 
244
  time.sleep(15)
245
 
 
8
  import requests
9
  from datetime import datetime, timezone
10
  from bs4 import BeautifulSoup
11
+ import threading
12
+ from concurrent.futures import ThreadPoolExecutor
13
 
14
  # ===== Channel.io 設定 =====
15
  GET_URL = "https://desk-api.channel.io/desk/channels/200605/groups/519217/messages"
 
40
 
41
  # ===== ssyoutube =====
42
  SSYOUTUBE_URL = "https://ssyoutube.online/yt-video-detail/"
43
+ SSYOUTUBE_AJAX_URL = "https://ssyoutube.online/wp-admin/admin-ajax.php"
44
 
45
  # ===== Utils =====
46
  def parse_updated_at(value):
 
75
  res.raise_for_status()
76
 
77
  soup = BeautifulSoup(res.text, "lxml")
78
+
79
+ # 非ceトークンを抽出
80
+ script_text = str(soup)
81
+ nonce_match = re.search(r"formData\.append\('nonce', '([^']+)'\)", script_text)
82
+ nonce = nonce_match.group(1) if nonce_match else None
83
+
84
+ # 動画タイトルを抽出
85
+ title_tag = soup.select_one("meta[property='og:title']")
86
+ video_title = title_tag.get("content", "動画") if title_tag else "動画"
87
+
88
+ # ダウンロードボタンからURLを取得
89
  buttons = soup.select("button[data-url]")
 
90
  results = []
91
+
92
  for btn in buttons:
93
  url = btn.get("data-url")
94
  quality = btn.get("data-quality")
95
  has_audio = btn.get("data-has-audio")
96
+ data_id = btn.get("data-id") # 追加: data-id属性を取得
97
 
98
  if not url:
99
  continue
 
102
  "url": url,
103
  "quality": quality or "audio",
104
  "has_audio": has_audio,
105
+ "data_id": data_id, # 追加
106
+ "nonce": nonce,
107
+ "video_title": video_title
108
  })
109
 
110
  return results
 
128
  video_items.sort(key=lambda x: x[0], reverse=True)
129
  return video_items[0][1], audio_item
130
 
131
+ # ===== サーバーサイド結合リクエスト =====
132
+ def request_server_merge(video_item, audio_item, youtube_id):
133
+ """ssyoutubeのサーバーサイド結合をリクエスト"""
134
 
135
+ # data-idからIDを抽出(例: "3aY1Z15vTw0_1080p")
136
+ video_id_with_quality = video_item["data_id"]
137
+
138
+ # リクエストデータの構築
139
+ request_data = {
140
+ "id": video_id_with_quality,
141
+ "ttl": 3600000,
142
+ "inputs": [
143
+ {
144
+ "url": video_item["url"],
145
+ "ext": "mp4",
146
+ "chunkDownload": {
147
+ "type": "header",
148
+ "size": 52428800,
149
+ "concurrency": 3
150
+ }
151
+ },
152
+ {
153
+ "url": audio_item["url"],
154
+ "ext": "m4a"
155
+ }
156
+ ],
157
+ "output": {
158
+ "ext": "mp4",
159
+ "downloadName": f"{video_item['video_title'].replace(' ', '_')}_{video_item['quality']}.mp4",
160
+ "chunkUpload": {
161
+ "size": 104857600,
162
+ "concurrency": 3
163
+ }
164
+ },
165
+ "operation": {
166
+ "type": "replace_audio_in_video"
167
+ }
168
+ }
169
+
170
+ # フォームデータを作成
171
+ form_data = {
172
+ "action": "process_video_merge",
173
+ "nonce": video_item["nonce"],
174
+ "request_data": json.dumps(request_data)
175
+ }
176
+
177
+ # リクエスト送信
178
+ headers = {
179
+ "User-Agent": "Mozilla/5.0",
180
+ "Referer": "https://ssyoutube.online/",
181
+ "Origin": "https://ssyoutube.online",
182
+ "Content-Type": "application/x-www-form-urlencoded"
183
+ }
184
+
185
+ response = requests.post(
186
+ SSYOUTUBE_AJAX_URL,
187
+ data=form_data,
188
+ headers=headers,
189
+ timeout=30
190
+ )
191
+ response.raise_for_status()
192
+
193
+ return response.json()
194
 
195
+ # ===== 結合ステータスの監視 =====
196
+ def monitor_merge_status(http_status_url, max_attempts=60, interval=5):
197
+ """結合処理のステータスを監視"""
198
+
199
+ headers = {
200
+ "User-Agent": "Mozilla/5.0",
201
+ "Accept": "application/json"
202
+ }
203
+
204
+ for attempt in range(max_attempts):
205
+ try:
206
+ response = requests.get(http_status_url, headers=headers, timeout=30)
207
+ response.raise_for_status()
208
+ status_data = response.json()
209
+
210
+ if status_data.get("success") and status_data.get("result", {}).get("status") == "done":
211
+ output = status_data["result"].get("output", {})
212
+ if output.get("url"):
213
+ return output["url"]
214
+
215
+ # 進行中の場合は待機
216
+ progress = status_data.get("result", {}).get("progress_in_percent", 0)
217
+ print(f"結合進行中: {progress}%")
218
+
219
+ except Exception as e:
220
+ print(f"ステータス取得エラー: {e}")
221
+
222
+ time.sleep(interval)
223
+
224
+ return None
225
 
226
+ # ===== ダウンロードと結合の新しいメイン関数 =====
227
+ def download_and_merge_with_server(youtube_id, video_item, audio_item):
228
+ """サーバーサイド結合を使用"""
229
+
230
+ try:
231
+ print(f"サーバーサイド結合を開始: {youtube_id} {video_item['quality']}")
232
+
233
+ # 1. サーバー結合をリクエスト
234
+ merge_response = request_server_merge(video_item, audio_item, youtube_id)
235
+
236
+ if not merge_response.get("success"):
237
+ print("サーバー結合リクエスト失敗")
238
+ return None
239
+
240
+ # 2. 監視URLを取得
241
+ monitor_info = merge_response.get("data", {}).get("result", {}).get("monitor", {})
242
+ http_status_url = monitor_info.get("http")
243
+
244
+ if not http_status_url:
245
+ print("監視URLが見つかりません")
246
+ return None
247
+
248
+ print(f"監視URL: {http_status_url}")
249
+
250
+ # 3. 結合完了を監視
251
+ merged_video_url = monitor_merge_status(http_status_url)
252
+
253
+ if not merged_video_url:
254
+ print("結合がタイムアウトしました")
255
+ return None
256
+
257
+ print(f"結合完了: {merged_video_url}")
258
+ return merged_video_url
259
+
260
+ except Exception as e:
261
+ print(f"サーバー結合中にエラー: {e}")
262
+ return None
263
 
264
  # ===== file.io アップロード =====
265
  def upload_to_fileio(file_path):
 
287
  )
288
  res.raise_for_status()
289
 
290
+ # ===== 古いダウンロード関数(フォールバック用) =====
291
+ def download_file_old(url, path):
292
+ headers = {
293
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
294
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
295
+ "Chrome/131.0.0.0 Safari/537.36",
296
+ "Accept": "*/*",
297
+ "Accept-Language": "ja,en-US;q=0.9,en;q=0.8",
298
+ "Referer": "https://www.youtube.com/",
299
+ "Origin": "https://www.youtube.com",
300
+ "Range": "bytes=0-",
301
+ }
302
+
303
+ with requests.get(
304
+ url,
305
+ headers=headers,
306
+ stream=True,
307
+ timeout=60,
308
+ ) as r:
309
+ r.raise_for_status()
310
+ with open(path, "wb") as f:
311
+ for chunk in r.iter_content(chunk_size=8192):
312
+ if chunk:
313
+ f.write(chunk)
314
+
315
+ def merge_video_audio_old(video_path, audio_path, output_path):
316
+ cmd = [
317
+ "ffmpeg",
318
+ "-y",
319
+ "-i", video_path,
320
+ "-i", audio_path,
321
+ "-c:v", "copy",
322
+ "-c:a", "aac",
323
+ output_path
324
+ ]
325
+ subprocess.run(cmd, check=True)
326
+
327
+ # ===== フォールバック処理 =====
328
+ def fallback_local_merge(video_item, audio_item, youtube_id):
329
+ """サーバー結合が失敗した場合のローカル結合フォールバック"""
330
+ try:
331
+ print("サーバー結合失敗、ローカル結合を試みます...")
332
+
333
+ with tempfile.TemporaryDirectory() as tmp:
334
+ video_path = os.path.join(tmp, "video.mp4")
335
+ audio_path = os.path.join(tmp, "audio.m4a")
336
+ output_path = os.path.join(tmp, "merged.mp4")
337
+
338
+ download_file_old(video_item["url"], video_path)
339
+ download_file_old(audio_item["url"], audio_path)
340
+
341
+ merge_video_audio_old(video_path, audio_path, output_path)
342
+
343
+ link = upload_to_fileio(output_path)
344
+ return link
345
+
346
+ except Exception as e:
347
+ print(f"ローカル結合も失敗: {e}")
348
+ return None
349
+
350
  # ===== Main =====
351
  def main():
352
+ processed_messages = set() # 処理済みメッセージを記録
353
+
354
  while True:
355
  try:
356
  res = requests.get(
 
362
  res.raise_for_status()
363
 
364
  messages = res.json().get("messages", [])
365
+
366
+ # 最新のYouTubeリンクを含むメッセージを探す
 
367
  for msg in messages:
368
+ msg_id = msg.get("id")
369
  plain_text = msg.get("plainText")
370
+
371
+ if not plain_text or not msg_id:
 
372
  continue
373
+
374
+ # 既に処理済みならスキップ
375
+ if msg_id in processed_messages:
376
  continue
377
+
378
+ youtube_id = extract_youtube_id(plain_text)
379
+ if not youtube_id:
380
+ continue
381
+
382
+ print(f"新しいYouTube動画を検出: {youtube_id}")
383
+
384
+ # ダウンロードリンクを取得
385
+ youtube_url = f"https://www.youtube.com/watch?v={youtube_id}"
386
+ items = fetch_download_links(youtube_url)
387
+
388
+ if not items:
389
+ print("ダウンロードリンクが見つかりません")
390
+ continue
391
+
392
+ # 最高画質の動画と音声を選択
393
+ video_item, audio_item = select_best_video_and_audio(items)
394
+ if not video_item or not audio_item:
395
+ print("適切な動画/音声が見つかりません")
396
+ continue
397
+
398
+ print(f"選択: 動画={video_item['quality']}, 音声={audio_item['quality']}")
399
+
400
+ # サーバーサイド結合を試みる
401
+ merged_url = download_and_merge_with_server(youtube_id, video_item, audio_item)
402
+
403
+ if merged_url:
404
+ # 成功した場合、直接URLを送信
405
+ send_to_channel(f"🎬 結合済み動画({video_item['quality']})\n{merged_url}")
406
+ print(f"送信完了: {merged_url}")
407
+ else:
408
+ # 失敗した場合はフォールバック
409
+ print("サーバー結合失敗、フォールバックを試みます...")
410
+ fallback_url = fallback_local_merge(video_item, audio_item, youtube_id)
411
+ if fallback_url:
412
+ send_to_channel(f"🎬 結合済み動画({video_item['quality']})\n{fallback_url}")
413
+ print(f"フォールバック送信完了: {fallback_url}")
414
+ else:
415
+ print("全ての結合方法が失敗しました")
416
+
417
+ # 処理済みとして記録
418
+ processed_messages.add(msg_id)
419
+ break # 一度に1つだけ処理
420
+
421
+ else:
422
+ # 新しいメッセージがない場合
423
+ pass
424
 
425
  except Exception as e:
426
  print("エラー:", e)
427
+ import traceback
428
+ traceback.print_exc()
429
 
430
  time.sleep(15)
431