File size: 13,310 Bytes
259fab3
3f49d8c
259fab3
3f49d8c
2d4a386
259fab3
f87074b
259fab3
490219d
3f49d8c
 
 
259fab3
 
 
 
 
 
 
 
677e8ae
259fab3
3f49d8c
 
 
 
 
 
 
259fab3
 
3f49d8c
259fab3
 
 
2d4a386
490219d
265a509
3f49d8c
259fab3
 
 
490219d
259fab3
 
 
3f49d8c
0daba3a
 
 
 
3f49d8c
0daba3a
 
 
 
 
 
3f49d8c
 
 
 
 
 
 
523da2d
 
 
 
 
 
 
 
 
 
 
 
fe97e6f
 
292c560
490219d
292c560
490219d
2d4a386
 
490219d
 
2d4a386
 
ec8aead
bc9968c
 
 
 
ec8aead
2d4a386
bc9968c
 
2d4a386
 
63326c9
 
bc9968c
490219d
 
bc9968c
 
 
 
 
 
 
 
 
 
 
 
523da2d
bc9968c
 
 
523da2d
bc9968c
 
63326c9
bc9968c
 
523da2d
bc9968c
2905969
523da2d
bc9968c
523da2d
 
 
 
 
 
 
bc9968c
 
 
 
 
 
 
 
ec8aead
 
 
bc9968c
ec8aead
 
 
bc9968c
ec8aead
 
 
 
 
 
 
 
bc9968c
ec8aead
 
 
 
 
bc9968c
ec8aead
 
 
 
 
 
 
 
 
 
bc9968c
4f48744
ec8aead
4f48744
ec8aead
 
 
 
bc9968c
4f48744
bc9968c
 
ec8aead
 
bc9968c
 
4f48744
ec8aead
 
 
bc9968c
ec8aead
bc9968c
ec8aead
4f48744
bc9968c
ec8aead
bc9968c
4f48744
bc9968c
 
 
4f48744
 
 
bc9968c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ec8aead
bc9968c
3f49d8c
 
 
490219d
fe97e6f
63326c9
fe97e6f
3f49d8c
490219d
2d4a386
 
 
 
490219d
2d4a386
 
3f49d8c
26fb022
259fab3
4f48744
ec8aead
259fab3
 
fe97e6f
 
 
 
 
 
3f49d8c
490219d
3f49d8c
bc9968c
 
 
490219d
ec8aead
490219d
4f48744
 
bc9968c
490219d
bc9968c
4f48744
ec8aead
490219d
bc9968c
 
 
ec8aead
bc9968c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51eb7be
523da2d
 
 
bc9968c
 
523da2d
 
bc9968c
 
 
523da2d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28dbcaf
 
ec8aead
523da2d
 
 
 
 
 
 
 
 
ccdf8b7
32f5b18
523da2d
 
 
28dbcaf
523da2d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28dbcaf
523da2d
 
 
0daba3a
523da2d
 
 
bc9968c
 
 
4f48744
523da2d
bc9968c
259fab3
bc9968c
 
 
 
 
259fab3
 
292c560
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
import os
import re
import time
import json
import requests
from datetime import datetime, timezone
from bs4 import BeautifulSoup

# ===== Channel.io 設定 =====
GET_URL = "https://desk-api.channel.io/desk/channels/200605/groups/519217/messages"
POST_URL = GET_URL

PARAMS = {
    "sortOrder": "desc",
    "limit": 36,
    "logFolded": "false",
}

X_ACCOUNT = os.getenv("channeliotokenbot2")
if not X_ACCOUNT:
    raise RuntimeError("環境変数 channeliotokenbot2 が設定されていません")

HEADERS_GET = {
    "accept": "application/json",
    "accept-language": "ja",
    "x-account": X_ACCOUNT,
}

HEADERS_POST = {
    "accept": "application/json",
    "accept-language": "ja",
    "content-type": "application/json",
    "x-account": X_ACCOUNT,
}

# ===== ssyoutube =====
SSYOUTUBE_URL = "https://ssyoutube.online/yt-video-detail/"

# ===== Utils =====
def parse_updated_at(value):
    if isinstance(value, (int, float)):
        return datetime.fromtimestamp(value / 1000, tz=timezone.utc)
    elif isinstance(value, str):
        return datetime.fromisoformat(value.replace("Z", "+00:00"))
    return None

def extract_youtube_id(text):
    _patterns = [
        # クエリパラメータ v= または vi=
        r"(?:[?&](?:v|vi)=)([A-Za-z0-9_-]{11})",
        # youtu.be 短縮リンク
        r"youtu\.be/([A-Za-z0-9_-]{11})",
        # /embed/ /v/ /shorts/
        r"youtube(?:-nocookie)?\.com/(?:embed|v|shorts)/([A-Za-z0-9_-]{11})",
        # watch? の中に v= が含まれるケース(ioパラメータ等が混ざっても OK)
        r"youtube(?:-nocookie)?\.com/watch\?.*?v=([A-Za-z0-9_-]{11})",
        # hash や他パラメータに混ざるケース: ...#v=xxxx or &v=xxxx
        r"[#&]v=([A-Za-z0-9_-]{11})"
    ]
    for p in patterns:
        m = re.search(p, text)
        if m:
            return m.group(1)
    return None

def get_video_title(youtube_url):
    """YouTube動画のタイトルを取得する簡易的な関数"""
    try:
        res = requests.get(youtube_url, timeout=10)
        soup = BeautifulSoup(res.text, "lxml")
        title_tag = soup.find("meta", property="og:title")
        if title_tag:
            return title_tag.get("content", "").replace(" - YouTube", "")
    except:
        pass
    return "YouTube動画"

# ===== ssyoutube HTML 解析 =====
def fetch_download_links(youtube_url):
    res = requests.post(
        SSYOUTUBE_URL,
        data={"videoURL": youtube_url},
        timeout=30,
        headers={
            "User-Agent": "Mozilla/5.0",
            "Referer": "https://ssyoutube.online/",
        }
    )
    res.raise_for_status()
    
    # nonceを抽出
    soup = BeautifulSoup(res.text, "lxml")
    nonce_match = re.search(r"formData\.append\('nonce', '([^']+)'\);", res.text)
    nonce = nonce_match.group(1) if nonce_match else None
    
    buttons = soup.select("button[data-url]")

    results = []
    for btn in buttons:
        url = btn.get("data-url")
        quality = btn.get("data-quality")
        has_audio = btn.get("data-has-audio")

        if not url:
            continue

        results.append({
            "url": url,
            "quality": quality or "audio",
            "has_audio": has_audio,
        })

    return results, nonce

# ===== 最高画質映像 + 音声を選択 =====
def select_best_video_and_audio(items):
    video_items = []
    audio_items = []

    for item in items:
        if item["quality"] == "audio":
            audio_items.append(item)
        elif item["has_audio"] == "false":
            m = re.match(r"(\d+)p", item["quality"])
            if m:
                video_items.append((int(m.group(1)), item))

    if not video_items or not audio_items:
        return None, None

    # 最高画質の動画を選択
    video_items.sort(key=lambda x: x[0], reverse=True)
    best_video = video_items[0][1]
    
    # 最高音質の音声を選択(例: 128kbps > 64kbps)
    # 実際の音声品質情報に基づいて選択する場合は要修正
    best_audio = audio_items[0] if audio_items else None
    
    return best_video, best_audio

# ===== ssyoutube サーバー側結合 =====
def merge_video_on_server(video_url, audio_url, video_id, quality, video_title, nonce):
    """
    ssyoutubeサーバーで動画と音声を結合する
    """
    # IDと画質からリクエストIDを生成
    request_id = f"{video_id}_{quality}p"
    
    # リクエストデータの構築
    request_data = {
        "id": request_id,
        "ttl": 3600000,
        "inputs": [
            {
                "url": video_url,
                "ext": "mp4",
                "chunkDownload": {
                    "type": "header",
                    "size": 52428800,
                    "concurrency": 3
                }
            },
            {
                "url": audio_url,
                "ext": "m4a"
            }
        ],
        "output": {
            "ext": "mp4",
            "downloadName": f"{video_title}_{quality}p.mp4",
            "chunkUpload": {
                "size": 104857600,
                "concurrency": 3
            }
        },
        "operation": {
            "type": "replace_audio_in_video"
        }
    }
    
    # 結合開始リクエスト
    payload = {
        "action": "process_video_merge",
        "nonce": nonce,
        "request_data": json.dumps(request_data)
    }
    
    headers = {
        "User-Agent": "Mozilla/5.0",
        "Referer": "https://ssyoutube.online/",
        "Origin": "https://ssyoutube.online",
        "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
    }
    
    res = requests.post(
        "https://ssyoutube.online/wp-admin/admin-ajax.php",
        data=payload,
        headers=headers,
        timeout=30
    )
    res.raise_for_status()
    
    result = res.json()
    
    if not result.get("success") or not result.get("data", {}).get("success"):
        raise RuntimeError(f"結合開始に失敗しました: {result}")
    
    # ステータス監視用URLを取得
    monitor_url = result["data"]["result"]["monitor"]["http"]
    
    # 完了するまでポーリング
    max_attempts = 60  # 最大60回試行(30秒間隔で約30分)
    attempt = 0
    
    while attempt < max_attempts:
        time.sleep(30)  # 30秒間隔でチェック
        
        status_res = requests.get(monitor_url, timeout=30)
        status_res.raise_for_status()
        status_data = status_res.json()
        
        if not status_data.get("success"):
            raise RuntimeError(f"ステータス取得に失敗しました: {status_data}")
        
        status = status_data["result"]["status"]
        
        if status == "done":
            # 完了したらダウンロードURLを返す
            return status_data["result"]["output"]["url"]
        elif status == "error":
            error_msg = status_data["result"]["error"]
            raise RuntimeError(f"結合処理でエラーが発生しました: {error_msg}")
        # "processing" の場合は継続
        
        attempt += 1
        print(f"結合処理中... 進捗: {status_data['result'].get('progress_in_percent', 0)}%")
    
    raise RuntimeError("結合処理がタイムアウトしました")

def send_to_channel(text):
    payload = {
        "requestId": f"desk-web-{int(time.time() * 1000)}",
        "blocks": [
            {"type": "text", "value": text}
        ],
    }

    res = requests.post(
        POST_URL,
        headers=HEADERS_POST,
        data=json.dumps(payload),
        timeout=30
    )
    res.raise_for_status()

# ===== Main =====
def main():
    processed_messages = set()  # 処理済みメッセージを追跡
    
    while True:
        try:
            res = requests.get(
                GET_URL,
                headers=HEADERS_GET,
                params=PARAMS,
                timeout=30,
            )
            res.raise_for_status()

            messages = res.json().get("messages", [])
            latest_msg = None
            latest_time = None

            for msg in messages:
                msg_id = msg.get("id")
                plain_text = msg.get("plainText")
                updated_at = msg.get("updatedAt")

                if not plain_text or updated_at is None:
                    continue
                    
                # 既に処理済みのメッセージはスキップ
                if msg_id in processed_messages:
                    continue

                t = parse_updated_at(updated_at)
                if not t:
                    continue

                if latest_time is None or t > latest_time:
                    latest_time = t
                    latest_msg = msg

            if not latest_msg:
                time.sleep(10)
                continue

            youtube_id = extract_youtube_id(latest_msg["plainText"])
            if not youtube_id:
                time.sleep(10)
                continue

            youtube_url = f"https://www.youtube.com/watch?v={youtube_id}"
            send_to_channel(f"{youtube_id}のダウンロードを開始します。")
            # 動画タイトルを取得
            video_title = get_video_title(youtube_url)
            
            items, nonce = fetch_download_links(youtube_url)
            
            if not nonce:
                print("nonceの取得に失敗しました")
                time.sleep(10)
                continue

            # すべてのダウンロードリンクを収集
            video_items = []
            audio_items = []
            all_items = []
            
            for item in items:
                all_items.append(item)
                if item["quality"] == "audio":
                    audio_items.append(item)
                elif item["has_audio"] == "false":
                    video_items.append(item)
            
            # 最高画質の動画と音声を選択
            best_video, best_audio = select_best_video_and_audio(items)
            
            # メッセージを構築
            message_lines = []
            message_lines.append(f"<b>{video_title}</b>\n")
            message_lines.append("<b>ダウンロードリンク</b>")
            
            # すべての動画リンクを追加
            for item in all_items:
                quality = item["quality"]
                if item["quality"] == "audio":
                    quality_display = "音声"
                elif item["has_audio"] == "true":
                    quality_display = f"{quality} (音声付き)"
                else:
                    quality_display = f"{quality} (映像のみ)"
                
                message_lines.append(f"<link type=\"url\" value=\"{item['url']}\"> {quality_display} </link>")
            
            # 最高画質の結合動画を作成
            if best_video and best_audio and nonce:
                message_lines.append("\n <b>最高画質結合動画</b>")
                
                # 画質情報を取得
                quality_match = re.match(r"(\d+)p", best_video["quality"])
                if quality_match:
                    quality = quality_match.group(1)
                    
                    # サーバー側で結合
                    print(f"サーバー側での結合を開始します: {youtube_id} {quality}p")
                    try:
                        merged_url = merge_video_on_server(
                            best_video["url"],
                            best_audio["url"],
                            youtube_id,
                            quality,
                            video_title,
                            nonce
                        )
                        message_lines.append(f"<link type=\"url\" value=\"{merged_url}\">{quality}p 結合済み動画</link>")
                    except Exception as e:
                        print(f"結合処理エラー: {e}")
                        message_lines.append(f"結合処理に失敗: {str(e)}")
            message_lines.append(f"ダウンロードが完了しました。\nAPIの変更により、一次的に使用できなくなっておりました。今後は維持していけるように頑張ります。\nこのツールの使い方:ダウンロードしたいYoutube動画のURLを貼り付けると、自動でボットがダウンロードURLを生成します。\nシステムについて:このシステムは、Huggingfaceのサーバーより、自動で送信されています。本人がいなくても多分いつでも使えます。動画は、最高画質のものに音声を付けるようにしています。音声を付ける処理に時間がかかりますが、この方法しかありませんでした。")
            # メッセージを送信
            message = "\n".join(message_lines)
            send_to_channel(message)
            
            # 処理済みとしてマーク
            processed_messages.add(latest_msg.get("id"))
            
            print(f"送信完了: {youtube_id}")

        except Exception as e:
            print("エラー:", e)
            import traceback
            traceback.print_exc()

        time.sleep(15)

if __name__ == "__main__":
    main()