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

Update watcher.py

Browse files
Files changed (1) hide show
  1. watcher.py +143 -236
watcher.py CHANGED
@@ -8,8 +8,6 @@ import subprocess
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"
@@ -73,71 +71,69 @@ def fetch_download_links(youtube_url):
73
  }
74
  )
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
100
-
101
- results.append({
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
111
-
112
- # ===== 最高画質映像 + 音声を選択 =====
113
- def select_best_video_and_audio(items):
114
- video_items = []
115
- audio_item = None
116
-
117
- for item in items:
118
- if item["quality"] == "audio":
119
- audio_item = item
120
- elif item["has_audio"] == "false":
121
- m = re.match(r"(\d+)p", item["quality"])
122
  if m:
123
- video_items.append((int(m.group(1)), item))
124
-
 
 
 
 
125
  if not video_items or not audio_item:
126
- return None, None
127
-
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
  {
@@ -156,7 +152,7 @@ def request_server_merge(video_item, audio_item, youtube_id):
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
@@ -167,109 +163,76 @@ def request_server_merge(video_item, audio_item, youtube_id):
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):
266
- with open(file_path, "rb") as f:
267
- r = requests.post("https://file.io", files={"file": f}, timeout=60)
268
- r.raise_for_status()
269
- data = r.json()
270
- if not data.get("success"):
271
- raise RuntimeError("file.io アップロード失敗")
272
- return data["link"]
273
 
274
  def send_to_channel(text):
275
  payload = {
@@ -287,69 +250,9 @@ def send_to_channel(text):
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:
@@ -363,71 +266,75 @@ def main():
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
 
432
  if __name__ == "__main__":
433
  main()
 
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"
 
71
  }
72
  )
73
  res.raise_for_status()
74
+ return res.text
75
 
76
+ # ===== 最高画質映像 + 音声を選択 =====
77
+ def select_best_video_and_audio(html_content):
78
+ soup = BeautifulSoup(html_content, "lxml")
79
 
80
+ # nonce を抽出
81
+ nonce_pattern = r"formData\.append\('nonce',\s*'([^']+)'\)"
82
+ nonce_match = re.search(nonce_pattern, html_content)
83
+ if not nonce_match:
84
+ raise ValueError("nonce が見つかりません")
85
+ nonce = nonce_match.group(1)
86
 
87
  # 動画タイトルを抽出
88
+ title_elem = soup.select_one("h1.title")
89
+ video_title = title_elem.text.strip() if title_elem else "video"
90
 
91
+ # ボタンからURLと品質情報を取得
92
  buttons = soup.select("button[data-url]")
93
+
94
+ video_items = []
95
+ audio_item = None
96
 
97
  for btn in buttons:
98
  url = btn.get("data-url")
99
  quality = btn.get("data-quality")
100
  has_audio = btn.get("data-has-audio")
101
+
 
102
  if not url:
103
  continue
104
+
105
+ if quality == "audio":
106
+ audio_item = {
107
+ "url": url,
108
+ "quality": quality,
109
+ "has_audio": has_audio
110
+ }
111
+ elif has_audio == "false":
112
+ m = re.match(r"(\d+)p", quality)
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  if m:
114
+ video_items.append({
115
+ "quality": int(m.group(1)),
116
+ "url": url,
117
+ "data_quality": quality
118
+ })
119
+
120
  if not video_items or not audio_item:
121
+ return None, None, None, None
122
+
123
+ # 最高画質を選択
124
+ video_items.sort(key=lambda x: x["quality"], reverse=True)
125
+ best_video = video_items[0]
126
+
127
+ return best_video, audio_item, nonce, video_title
128
 
129
  # ===== サーバーサイド結合リクエスト =====
130
+ def request_video_merge(youtube_id, video_item, audio_item, nonce, video_title):
131
+ # ID生成(YouTubeID + 画質)
132
+ merge_id = f"{youtube_id}_{video_item['data_quality']}"
 
 
133
 
134
  # リクエストデータの構築
135
  request_data = {
136
+ "id": merge_id,
137
  "ttl": 3600000,
138
  "inputs": [
139
  {
 
152
  ],
153
  "output": {
154
  "ext": "mp4",
155
+ "downloadName": f"{video_title}_{video_item['data_quality']}.mp4",
156
  "chunkUpload": {
157
  "size": 104857600,
158
  "concurrency": 3
 
163
  }
164
  }
165
 
166
+ payload = {
 
167
  "action": "process_video_merge",
168
+ "nonce": nonce,
169
  "request_data": json.dumps(request_data)
170
  }
171
 
 
172
  headers = {
173
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
 
174
  "Origin": "https://ssyoutube.online",
175
+ "Referer": "https://ssyoutube.online/",
176
+ "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
177
  }
178
 
179
  response = requests.post(
180
  SSYOUTUBE_AJAX_URL,
181
+ data=payload,
182
  headers=headers,
183
  timeout=30
184
  )
185
  response.raise_for_status()
186
 
187
+ result = response.json()
 
 
 
 
188
 
189
+ if not result.get("success") or not result.get("data", {}).get("success"):
190
+ raise RuntimeError(f"結合リクエストに失敗: {result}")
 
 
191
 
192
+ # HTTP監視URLを取得
193
+ monitor_url = result["data"]["result"]["monitor"]["http"]
194
+ return monitor_url
195
+
196
+ # ===== 結合進捗監視 =====
197
+ def monitor_merge_progress(monitor_url):
198
+ """結合進捗を監視し、完了したらダウンロードURLを返す"""
199
+ max_attempts = 300 # 最大試行回数(5分間)
200
+ attempt = 0
201
+
202
+ while attempt < max_attempts:
203
  try:
204
+ response = requests.get(monitor_url, timeout=30)
205
  response.raise_for_status()
206
+ result = response.json()
207
+
208
+ if not result.get("success"):
209
+ time.sleep(1)
210
+ attempt += 1
211
+ continue
212
+
213
+ status = result["result"].get("status")
214
 
215
+ if status == "done":
216
+ download_url = result["result"]["output"]["url"]
217
+ return download_url
218
+ elif status == "error":
219
+ error_msg = result["result"].get("error", "不明なエラー")
220
+ raise RuntimeError(f"結合処理でエラー: {error_msg}")
221
+ elif status in ["processing", "started"]:
222
+ # 進捗表示(オプション)
223
+ progress = result["result"].get("progress_in_percent", 0)
224
+ print(f"結合中... {progress}%")
225
 
226
+ # 2秒待機
227
+ time.sleep(2)
228
+ attempt += 1
229
 
230
  except Exception as e:
231
+ print(f"監視中にエラー: {e}")
232
+ time.sleep(2)
233
+ attempt += 1
 
 
 
 
 
 
234
 
235
+ raise TimeoutError("結合処理がタイムアウトしました")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
 
237
  def send_to_channel(text):
238
  payload = {
 
250
  )
251
  res.raise_for_status()
252
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  # ===== Main =====
254
  def main():
255
+ processed_messages = set() # 処理済みメッセージを追跡
256
 
257
  while True:
258
  try:
 
266
 
267
  messages = res.json().get("messages", [])
268
 
 
269
  for msg in messages:
270
  msg_id = msg.get("id")
271
  plain_text = msg.get("plainText")
272
+ updated_at = msg.get("updatedAt")
273
+
274
+ # 必須フィールドのチェック
275
+ if not msg_id or not plain_text or updated_at is None:
276
  continue
277
 
278
+ # 既に処理済みのメッセージはスキップ
279
  if msg_id in processed_messages:
280
  continue
281
 
282
+ # YouTube ID を抽出
283
  youtube_id = extract_youtube_id(plain_text)
284
  if not youtube_id:
285
  continue
286
 
287
  print(f"新しいYouTube動画を検出: {youtube_id}")
288
 
289
+ try:
290
+ # YouTube URL
291
+ youtube_url = f"https://www.youtube.com/watch?v={youtube_id}"
292
+
293
+ # ssyoutubeから情報を取得
294
+ html_content = fetch_download_links(youtube_url)
295
+
296
+ # 最高画質の動画と音声を選択
297
+ video_item, audio_item, nonce, video_title = select_best_video_and_audio(html_content)
298
+
299
+ if not video_item or not audio_item:
300
+ print(f"適切な動画/音声が見つかりません: {youtube_id}")
301
+ continue
302
+
303
+ print(f"動画品質: {video_item['data_quality']}")
304
+ print(f"動画タイトル: {video_title}")
305
+
306
+ # サーバーサイド結合をリクエスト
307
+ monitor_url = request_video_merge(youtube_id, video_item, audio_item, nonce, video_title)
308
+ print(f"結合リクエスト送信: {monitor_url}")
309
+
310
+ # 結合進捗��監視
311
+ download_url = monitor_merge_progress(monitor_url)
312
+ print(f"結合完了: {download_url}")
313
+
314
+ # Channel.ioに送信
315
+ quality = video_item['data_quality']
316
+ message = f"🎬 結合済み動画 ({quality})\n{download_url}"
317
+ send_to_channel(message)
318
+
319
+ print(f"送信完了: {message}")
320
+
321
+ # 処理済みメッセージとして記録
322
+ processed_messages.add(msg_id)
323
+
324
+ # 次のメッセージを処理する前に少し待機
325
+ time.sleep(5)
326
+
327
+ except Exception as e:
328
+ print(f"動画処理中にエラー ({youtube_id}): {e}")
329
+ # エラーが発生しても処理済みにしない(再試行可能にする)
330
  continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
 
332
+ # 全てのメッセージをチェックしたら待機
333
+ time.sleep(10)
334
+
 
335
  except Exception as e:
336
+ print(f"メインループでエラー: {e}")
337
+ time.sleep(15)
 
 
 
338
 
339
  if __name__ == "__main__":
340
  main()