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

Update watcher.py

Browse files
Files changed (1) hide show
  1. watcher.py +148 -155
watcher.py CHANGED
@@ -38,7 +38,6 @@ HEADERS_POST = {
38
 
39
  # ===== ssyoutube =====
40
  SSYOUTUBE_URL = "https://ssyoutube.online/yt-video-detail/"
41
- SSYOUTUBE_AJAX_URL = "https://ssyoutube.online/wp-admin/admin-ajax.php"
42
 
43
  # ===== Utils =====
44
  def parse_updated_at(value):
@@ -71,73 +70,65 @@ def fetch_download_links(youtube_url):
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
  {
140
- "url": video_item["url"],
141
  "ext": "mp4",
142
  "chunkDownload": {
143
  "type": "header",
@@ -146,13 +137,13 @@ def request_video_merge(youtube_id, video_item, audio_item, nonce, video_title):
146
  }
147
  },
148
  {
149
- "url": audio_item["url"],
150
  "ext": "m4a"
151
  }
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,6 +154,7 @@ def request_video_merge(youtube_id, video_item, audio_item, nonce, video_title):
163
  }
164
  }
165
 
 
166
  payload = {
167
  "action": "process_video_merge",
168
  "nonce": nonce,
@@ -170,69 +162,56 @@ def request_video_merge(youtube_id, video_item, audio_item, nonce, video_title):
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 = {
@@ -265,76 +244,90 @@ def main():
265
  res.raise_for_status()
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()
 
38
 
39
  # ===== ssyoutube =====
40
  SSYOUTUBE_URL = "https://ssyoutube.online/yt-video-detail/"
 
41
 
42
  # ===== Utils =====
43
  def parse_updated_at(value):
 
70
  }
71
  )
72
  res.raise_for_status()
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
+ # nonceを抽出
75
+ soup = BeautifulSoup(res.text, "lxml")
76
+ nonce_match = re.search(r"formData\.append\('nonce', '([^']+)'\);", res.text)
77
+ nonce = nonce_match.group(1) if nonce_match else None
78
 
 
79
  buttons = soup.select("button[data-url]")
80
+
81
+ results = []
 
 
82
  for btn in buttons:
83
  url = btn.get("data-url")
84
  quality = btn.get("data-quality")
85
  has_audio = btn.get("data-has-audio")
86
+
87
  if not url:
88
  continue
89
+
90
+ results.append({
91
+ "url": url,
92
+ "quality": quality or "audio",
93
+ "has_audio": has_audio,
94
+ })
95
+
96
+ return results, nonce
97
+
98
+ # ===== 最高画質映像 + 音声を選択 =====
99
+ def select_best_video_and_audio(items):
100
+ video_items = []
101
+ audio_item = None
102
+
103
+ for item in items:
104
+ if item["quality"] == "audio":
105
+ audio_item = item
106
+ elif item["has_audio"] == "false":
107
+ m = re.match(r"(\d+)p", item["quality"])
108
  if m:
109
+ video_items.append((int(m.group(1)), item))
110
+
 
 
 
 
111
  if not video_items or not audio_item:
112
+ return None, None
 
 
 
 
 
 
113
 
114
+ video_items.sort(key=lambda x: x[0], reverse=True)
115
+ return video_items[0][1], audio_item
116
+
117
+ # ===== ssyoutube サーバー側結合 =====
118
+ def merge_video_on_server(video_url, audio_url, video_id, quality, video_title, nonce):
119
+ """
120
+ ssyoutubeサーバーで動画と音声を結合する
121
+ """
122
+ # IDと画質からリクエストIDを生成
123
+ request_id = f"{video_id}_{quality}p"
124
 
125
  # リクエストデータの構築
126
  request_data = {
127
+ "id": request_id,
128
  "ttl": 3600000,
129
  "inputs": [
130
  {
131
+ "url": video_url,
132
  "ext": "mp4",
133
  "chunkDownload": {
134
  "type": "header",
 
137
  }
138
  },
139
  {
140
+ "url": audio_url,
141
  "ext": "m4a"
142
  }
143
  ],
144
  "output": {
145
  "ext": "mp4",
146
+ "downloadName": f"{video_title}_{quality}p.mp4",
147
  "chunkUpload": {
148
  "size": 104857600,
149
  "concurrency": 3
 
154
  }
155
  }
156
 
157
+ # 結合開始リクエスト
158
  payload = {
159
  "action": "process_video_merge",
160
  "nonce": nonce,
 
162
  }
163
 
164
  headers = {
165
+ "User-Agent": "Mozilla/5.0",
 
166
  "Referer": "https://ssyoutube.online/",
167
+ "Origin": "https://ssyoutube.online",
168
+ "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
169
  }
170
 
171
+ res = requests.post(
172
+ "https://ssyoutube.online/wp-admin/admin-ajax.php",
173
  data=payload,
174
  headers=headers,
175
  timeout=30
176
  )
177
+ res.raise_for_status()
178
 
179
+ result = res.json()
180
 
181
  if not result.get("success") or not result.get("data", {}).get("success"):
182
+ raise RuntimeError(f"結合開始に失敗しました: {result}")
183
 
184
+ # ステータス監視用URLを取得
185
  monitor_url = result["data"]["result"]["monitor"]["http"]
186
+
187
+ # 完了するまでポーリング
188
+ max_attempts = 60 # 最大60回試行(30秒間隔で約30分)
 
 
 
189
  attempt = 0
190
 
191
  while attempt < max_attempts:
192
+ time.sleep(30) # 30秒間隔でチェック
193
+
194
+ status_res = requests.get(monitor_url, timeout=30)
195
+ status_res.raise_for_status()
196
+ status_data = status_res.json()
197
+
198
+ if not status_data.get("success"):
199
+ raise RuntimeError(f"ステータス取得に失敗しました: {status_data}")
200
+
201
+ status = status_data["result"]["status"]
202
+
203
+ if status == "done":
204
+ # 完了したらダウンロードURLを返す
205
+ return status_data["result"]["output"]["url"]
206
+ elif status == "error":
207
+ error_msg = status_data["result"]["error"]
208
+ raise RuntimeError(f"結合処理でエラーが発生しました: {error_msg}")
209
+ # "processing" の場合は継続
210
+
211
+ attempt += 1
212
+ print(f"結合処理中... 進捗: {status_data['result'].get('progress_in_percent', 0)}%")
 
 
 
 
 
 
 
 
 
 
213
 
214
+ raise RuntimeError("結合処理がタイムアウトしました")
215
 
216
  def send_to_channel(text):
217
  payload = {
 
244
  res.raise_for_status()
245
 
246
  messages = res.json().get("messages", [])
247
+ latest_msg = None
248
+ latest_time = None
249
+
250
  for msg in messages:
251
  msg_id = msg.get("id")
252
  plain_text = msg.get("plainText")
253
  updated_at = msg.get("updatedAt")
254
 
255
+ if not plain_text or updated_at is None:
 
256
  continue
257
+
258
  # 既に処理済みのメッセージはスキップ
259
  if msg_id in processed_messages:
260
  continue
261
+
262
+ t = parse_updated_at(updated_at)
263
+ if not t:
 
264
  continue
265
+
266
+ if latest_time is None or t > latest_time:
267
+ latest_time = t
268
+ latest_msg = msg
269
+
270
+ if not latest_msg:
271
+ time.sleep(10)
272
+ continue
273
+
274
+ youtube_id = extract_youtube_id(latest_msg["plainText"])
275
+ if not youtube_id:
276
+ time.sleep(10)
277
+ continue
278
+
279
+ youtube_url = f"https://www.youtube.com/watch?v={youtube_id}"
280
+ items, nonce = fetch_download_links(youtube_url)
281
+
282
+ if not nonce:
283
+ print("nonceの取得に失敗しました")
284
+ time.sleep(10)
285
+ continue
286
+
287
+ video_item, audio_item = select_best_video_and_audio(items)
288
+ if not video_item or not audio_item:
289
+ print("適切な動画または音声が見つかりません")
290
+ time.sleep(10)
291
+ continue
292
+
293
+ # 画質情報を取得
294
+ quality_match = re.match(r"(\d+)p", video_item["quality"])
295
+ if not quality_match:
296
+ print("画質情報の取得に失敗しました")
297
+ time.sleep(10)
298
+ continue
299
 
300
+ quality = quality_match.group(1)
301
+
302
+ # 動画タイトル(簡易版 - 実際にはYouTubeから取得する方が良い)
303
+ video_title = f"youtube_video_{youtube_id}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
 
305
+ # サーバー側で結合
306
+ print(f"サーバー側での結合を開始します: {youtube_id} {quality}p")
307
+ merged_url = merge_video_on_server(
308
+ video_item["url"],
309
+ audio_item["url"],
310
+ youtube_id,
311
+ quality,
312
+ video_title,
313
+ nonce
314
+ )
315
+
316
+ # 結果を送信
317
+ message = f"🎬 結合済み動画({quality}p)\n{merged_url}"
318
+ send_to_channel(message)
319
+
320
+ # 処理済みとしてマーク
321
+ processed_messages.add(latest_msg.get("id"))
322
 
323
+ print("送信完了:", merged_url)
324
+
325
  except Exception as e:
326
+ print("エラー:", e)
327
+ import traceback
328
+ traceback.print_exc()
329
+
330
+ time.sleep(15)
331
 
332
  if __name__ == "__main__":
333
  main()