izuemon commited on
Commit
2c246f8
·
verified ·
1 Parent(s): eb2f4e1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +291 -77
app.py CHANGED
@@ -1,107 +1,321 @@
1
- from flask import Flask, request, jsonify, redirect, Response
2
  import os
 
 
 
3
  import requests
4
- import subprocess
5
- import sys
 
6
 
7
- app = Flask(__name__)
 
 
8
 
9
- watcher_process = None
 
 
 
 
10
 
11
- def start_watcher():
12
- global watcher_process
 
13
 
14
- if watcher_process is None or watcher_process.poll() is not None:
15
- watcher_process = subprocess.Popen(
16
- [sys.executable, "watcher.py"],
17
- stdout=sys.stdout,
18
- stderr=sys.stderr,
19
- env=os.environ, # ← 環境変数を引き継ぐ
20
- )
21
- print("watcher.py を起動しました")
22
 
 
 
 
 
 
 
23
 
24
- #----------
 
 
 
 
25
 
26
- @app.route("/drive.com/files")
27
- def index():
28
- ip = request.remote_addr
29
- print(f"アクセスIP: {ip}")
30
- return redirect("https://drive.google.com/")
 
 
31
 
32
- @app.route("/channel-io-managers")
33
- def get_managers():
34
- limit = request.args.get("limit")
35
- since = request.args.get("since")
 
 
 
 
 
 
 
 
 
36
 
37
- params = {}
38
- if limit:
39
- params["limit"] = limit
40
- if since:
41
- params["since"] = since
 
 
42
 
43
- channel_id = request.args.get("channelid", "200605")
44
- url = f"https://desk-api.channel.io/desk/channels/{channel_id}/managers"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
- headers = {
47
- "accept": "application/json",
48
- "x-account": os.getenv("channeliotokenmain"),
 
 
 
49
  }
 
 
 
 
 
 
 
50
 
51
- res = requests.get(url, headers=headers, params=params)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
- if res.status_code != 200:
54
- return jsonify({"error": res.text}), res.status_code
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
- return jsonify(res.json().get("managers", []))
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
- @app.route("/cors-proxy", methods=["GET"])
59
- def corsproxy():
60
- url = request.args.get("url")
61
- if not url:
62
- return "url パラメータが必要です", 400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
- if not url.startswith(("http://", "https://")):
65
- return "http または https のURLのみ使用できます", 400
 
 
 
 
 
66
 
67
- resp = requests.get(url, headers=request.headers, timeout=60)
 
 
 
 
 
 
68
 
69
- response = Response(resp.content, resp.status_code)
70
- response.headers["Access-Control-Allow-Origin"] = "*"
71
- response.headers["Access-Control-Allow-Headers"] = "*"
72
- response.headers["Access-Control-Allow-Methods"] = "GET, POST, PATCH, OPTIONS"
 
 
 
73
 
74
- if "Content-Type" in resp.headers:
75
- response.headers["Content-Type"] = resp.headers["Content-Type"]
 
76
 
77
- return response
 
78
 
79
- @app.route("/cors-proxy", methods=["POST", "PATCH"])
80
- def corsproxy_post():
81
- url = request.args.get("url")
82
- if not url:
83
- return "url パラメータが必要です", 400
84
 
85
- if not url.startswith(("http://", "https://")):
86
- return "http または https のURLのみ使用できます", 400
 
 
 
 
 
 
 
 
 
 
87
 
88
- resp = requests.request(
89
- method=request.method,
90
- url=url,
91
- headers=request.headers,
92
- data=request.data,
93
- timeout=60,
94
- )
95
 
96
- headers = {
97
- "Access-Control-Allow-Origin": "*",
98
- "Access-Control-Allow-Headers": "*",
99
- "Access-Control-Allow-Methods": "GET, POST, PATCH, OPTIONS",
100
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
- return Response(resp.content, resp.status_code, headers=headers)
103
 
104
  if __name__ == "__main__":
105
- if os.environ.get("WERKZEUG_RUN_MAIN") != "true":
106
- start_watcher()
107
- app.run(debug=True, host="0.0.0.0", port=7860)
 
1
+ # -*- coding: utf-8 -*-
2
  import os
3
+ import re
4
+ import time
5
+ import json
6
  import requests
7
+ from datetime import datetime, timezone
8
+ from bs4 import BeautifulSoup
9
+ from urllib.parse import quote_plus
10
 
11
+ # ===== Channel.io 設定 =====
12
+ GET_URL = "https://desk-api.channel.io/desk/channels/200605/groups/519217/messages"
13
+ POST_URL = GET_URL
14
 
15
+ PARAMS = {
16
+ "sortOrder": "desc",
17
+ "limit": 36,
18
+ "logFolded": "false",
19
+ }
20
 
21
+ X_ACCOUNT = os.getenv("channeliotokenbot2")
22
+ if not X_ACCOUNT:
23
+ raise RuntimeError("環境変数 channeliotokenbot2 が設定されていません")
24
 
25
+ HEADERS_GET = {
26
+ "accept": "application/json",
27
+ "accept-language": "ja",
28
+ "x-account": X_ACCOUNT,
29
+ }
 
 
 
30
 
31
+ HEADERS_POST = {
32
+ "accept": "application/json",
33
+ "accept-language": "ja",
34
+ "content-type": "application/json",
35
+ "x-account": X_ACCOUNT,
36
+ }
37
 
38
+ # ===== SaveNow 設定 =====
39
+ # 例示の API キー(実運用では環境変数 SAVENOW_API を設定してください)
40
+ DEFAULT_SAVENOW_API = "dfcb6d76f2f6a9894gjkege8a4ab232222"
41
+ SAVENOW_API = os.getenv("SAVENOW_API", DEFAULT_SAVENOW_API)
42
+ SAVENOW_BASE = "https://p.savenow.to/ajax/download.php"
43
 
44
+ # ===== Utils =====
45
+ def parse_updated_at(value):
46
+ if isinstance(value, (int, float)):
47
+ return datetime.fromtimestamp(value / 1000, tz=timezone.utc)
48
+ elif isinstance(value, str):
49
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
50
+ return None
51
 
52
+ def extract_youtube_id(text):
53
+ patterns = [
54
+ r"(?:[?&](?:v|vi)=)([A-Za-z0-9_-]{11})",
55
+ r"youtu\.be/([A-Za-z0-9_-]{11})",
56
+ r"youtube(?:-nocookie)?\.com/(?:embed|v|shorts)/([A-Za-z0-9_-]{11})",
57
+ r"youtube(?:-nocookie)?\.com/watch\?.*?v=([A-Za-z0-9_-]{11})",
58
+ r"[#&]v=([A-Za-z0-9_-]{11})"
59
+ ]
60
+ for p in patterns:
61
+ m = re.search(p, text)
62
+ if m:
63
+ return m.group(1)
64
+ return None
65
 
66
+ def extract_youtube_and_option(text):
67
+ parts = re.split(r"[ \u3000]+", text.strip())
68
+ video_id = extract_youtube_id(parts[0])
69
+ option = None
70
+ if len(parts) > 1:
71
+ option = parts[1].lower()
72
+ return video_id, option
73
 
74
+ def normalize_quality(option):
75
+ if option is None:
76
+ return "720"
77
+ trans_table = str.maketrans(
78
+ "0123456789kKpP",
79
+ "0123456789kKpP"
80
+ )
81
+ option = option.translate(trans_table).lower()
82
+ quality_map = {
83
+ "144": "144", "240": "240", "360": "360", "480": "480",
84
+ "720": "720", "1080": "1080", "2k": "1440", "1440": "1440",
85
+ "4k": "2160", "2160": "2160", "8k": "4320", "4320": "4320",
86
+ "144p": "144", "240p": "240", "360p": "360", "480p": "480",
87
+ "720p": "720", "1080p": "1080", "2kp": "1440", "1440p": "1440",
88
+ "4kp": "2160", "2160p": "2160", "8kp": "4320", "4320p": "4320",
89
+ }
90
+ if option in quality_map:
91
+ return quality_map[option]
92
+ if option in ["a", "f"]:
93
+ return option
94
+ return "720"
95
 
96
+ def send_to_channel(text):
97
+ payload = {
98
+ "requestId": f"desk-web-{int(time.time() * 1000)}",
99
+ "blocks": [
100
+ {"type": "text", "value": text}
101
+ ],
102
  }
103
+ res = requests.post(
104
+ POST_URL,
105
+ headers=HEADERS_POST,
106
+ data=json.dumps(payload),
107
+ timeout=30
108
+ )
109
+ res.raise_for_status()
110
 
111
+ # ===== SaveNow 用ユーティリティ =====
112
+ def build_savenow_initial_url(youtube_url, quality_opt, api_key=SAVENOW_API):
113
+ """
114
+ quality_opt:
115
+ - "a" -> mp3 (format=mp3)
116
+ - "f" -> omit format パラメータ(要求どおり 'f' は削除)
117
+ - numeric (例 "720") -> format=720
118
+ """
119
+ params = []
120
+ params.append("copyright=0")
121
+ # f は削除(つまり送らない)
122
+ if quality_opt == "a":
123
+ params.append("format=mp3")
124
+ elif quality_opt == "f":
125
+ # 指示通り 'f' パラメータは付けない(format自体を送らない)
126
+ pass
127
+ else:
128
+ # 数値系フォーマット
129
+ params.append(f"format={quote_plus(str(quality_opt))}")
130
+ params.append(f"url={quote_plus(youtube_url)}")
131
+ params.append(f"api={quote_plus(api_key)}")
132
+ return f"{SAVENOW_BASE}?{'&'.join(params)}"
133
 
134
+ def poll_progress_url(progress_url, max_attempts=30, interval=10):
135
+ """
136
+ progress_url をポーリングして download_url / alternative_download_urls を取得する。
137
+ interval は秒。max_attempts 回の試行を行う。
138
+ 成功時は (True, response_json) を返し、失敗(タイムアウト)は (False, last_json) を返す。
139
+ """
140
+ attempt = 0
141
+ last_json = None
142
+ while attempt < max_attempts:
143
+ try:
144
+ r = requests.get(progress_url, timeout=30)
145
+ r.raise_for_status()
146
+ data = r.json()
147
+ last_json = data
148
+ # 成功条件: download_url または alternative_download_urls が存在すれば成功とみなす
149
+ if data.get("download_url") or data.get("alternative_download_urls"):
150
+ return True, data
151
+ # 進捗表示などを channel に流しても良い
152
+ progress = data.get("progress")
153
+ status_text = data.get("text") or data.get("message") or ""
154
+ # ログとして出力(開発時用)
155
+ print(f"poll {attempt+1}/{max_attempts} progress={progress} text={status_text}")
156
+ except Exception as e:
157
+ print(f"poll error (attempt {attempt+1}): {e}")
158
+ # 失敗しても次へ(短時間の一時的なエラーを許容)
159
+ attempt += 1
160
+ time.sleep(interval)
161
+ return False, last_json
162
 
163
+ def process_savenow_flow(youtube_url, quality_opt):
164
+ """
165
+ SaveNow の初回 GET -> progress_url をポーリング -> 見つかったリンクを channel に送信
166
+ """
167
+ initial_url = build_savenow_initial_url(youtube_url, quality_opt)
168
+ send_to_channel(f"SaveNow にリクエストを送信しています: {initial_url}")
169
+ try:
170
+ r = requests.get(initial_url, timeout=30)
171
+ r.raise_for_status()
172
+ init_json = r.json()
173
+ except Exception as e:
174
+ send_to_channel(f"SaveNow へのリクエストに失敗しました: {e}")
175
+ return None
176
 
177
+ # 初回レスポンスに progress_url がある想定
178
+ progress_url = init_json.get("progress_url") or init_json.get("progressUrl") or init_json.get("progress")
179
+ # 一部の API は progress_url をそのまま返し、別のものは result に入れている場合があるため、念のため探索
180
+ if not progress_url:
181
+ # もし最初のレスポンスで直接 download_url があるなら即時処理
182
+ if init_json.get("download_url") or init_json.get("alternative_download_urls"):
183
+ # 直接リンクが来ている場合
184
+ urls = []
185
+ if init_json.get("download_url"):
186
+ urls.append(init_json["download_url"])
187
+ for alt in init_json.get("alternative_download_urls", []):
188
+ if alt.get("url"):
189
+ urls.append(alt["url"])
190
+ # channel に送る
191
+ for i, u in enumerate(urls, start=1):
192
+ send_to_channel(f"ダウンロードリンク #{i}: <link type=\"url\" value=\"{u}\">{u}</link>")
193
+ return urls
194
+ send_to_channel("progress_url が見つかりませんでした。レスポンスをログに出します。")
195
+ send_to_channel(f"初回レスポンス: {json.dumps(init_json, ensure_ascii=False)[:1000]}")
196
+ return None
197
 
198
+ # progress_url があればポーリング
199
+ send_to_channel(f"進捗監視を開始します: {progress_url} (最大30回、10秒間隔)")
200
+ ok, poll_json = poll_progress_url(progress_url, max_attempts=30, interval=10)
201
+ if not ok:
202
+ send_to_channel("進捗監視がタイムアウトしました。最後のレスポンスをログに出します。")
203
+ send_to_channel(f"最後のレスポンス: {json.dumps(poll_json, ensure_ascii=False)[:1000]}")
204
+ return None
205
 
206
+ # 成功した場合、download_url alternative_download_urls を収集して channel に送信
207
+ urls = []
208
+ if poll_json.get("download_url"):
209
+ urls.append(poll_json["download_url"])
210
+ for alt in poll_json.get("alternative_download_urls", []):
211
+ if alt.get("url"):
212
+ urls.append(alt["url"])
213
 
214
+ # 重複排除
215
+ seen = set()
216
+ unique_urls = []
217
+ for u in urls:
218
+ if u and u not in seen:
219
+ seen.add(u)
220
+ unique_urls.append(u)
221
 
222
+ if not unique_urls:
223
+ send_to_channel("ダウンロードリンクが見つかりませんでした(成功レスポンスでしたが link が空)。")
224
+ return None
225
 
226
+ for i, u in enumerate(unique_urls, start=1):
227
+ send_to_channel(f"ダウンロードリンク #{i}: <link type=\"url\" value=\"{u}\">{u}</link>")
228
 
229
+ return unique_urls
 
 
 
 
230
 
231
+ # ===== Main (既存フローの中に組み込む想定) =====
232
+ def main():
233
+ processed_messages = set()
234
+ while True:
235
+ try:
236
+ res = requests.get(
237
+ GET_URL,
238
+ headers=HEADERS_GET,
239
+ params=PARAMS,
240
+ timeout=30,
241
+ )
242
+ res.raise_for_status()
243
 
244
+ messages = res.json().get("messages", [])
245
+ latest_msg = None
246
+ latest_time = None
 
 
 
 
247
 
248
+ for msg in messages:
249
+ msg_id = msg.get("id")
250
+ plain_text = msg.get("plainText")
251
+ updated_at = msg.get("updatedAt")
252
+
253
+ if not plain_text or updated_at is None:
254
+ continue
255
+ if msg_id in processed_messages:
256
+ continue
257
+
258
+ t = parse_updated_at(updated_at)
259
+ if not t:
260
+ continue
261
+
262
+ if latest_time is None or t > latest_time:
263
+ latest_time = t
264
+ latest_msg = msg
265
+
266
+ if not latest_msg:
267
+ time.sleep(10)
268
+ continue
269
+
270
+ video_id, option = extract_youtube_and_option(latest_msg["plainText"])
271
+ if not video_id:
272
+ # YouTube ID が取れないメッセージはスキップ
273
+ processed_messages.add(latest_msg["id"])
274
+ time.sleep(10)
275
+ continue
276
+
277
+ valid_options = ["144", "240", "360", "480", "720", "1080", "1440", "2160", "4320", "4k", "8k", "2k", "a", "f"]
278
+
279
+ if option is None:
280
+ send_to_channel(
281
+ "画質が指定されていないため、デフォルトの720pを使用します。\n"
282
+ "指定する場合は、動画URLの後にスペースを入れて、"
283
+ "144/240/360/480/720/1080/2k(または1440)/4k(または2160)/8k(または4320)/a(音声/mp3)/f(最大画質・formatパラメータ未送信) のいずれかを入力してください。"
284
+ )
285
+ else:
286
+ opt_lower = option.lower()
287
+ if opt_lower not in valid_options:
288
+ send_to_channel(
289
+ "不明な画質、または予期しない形式でした。\n"
290
+ "対応画質: 144, 240, 360, 480, 720, 1080, "
291
+ "2k(1440), 4k(2160), 8k(4320), a(音声/mp3), f(最大画質)\n"
292
+ "動画のURLの後にスペースを入れて指定してください。\n"
293
+ "例: https://youtu.be/abcdEFGHijk 1080"
294
+ )
295
+ processed_messages.add(latest_msg["id"])
296
+ continue
297
+
298
+ quality_opt = normalize_quality(option)
299
+ youtube_url = f"https://www.youtube.com/watch?v={video_id}"
300
+ send_to_channel(f"{video_id} のダウンロード(SaveNow)を開始します。")
301
+
302
+ # SaveNow フローを使う(要求にあった処理)
303
+ urls = process_savenow_flow(youtube_url, quality_opt)
304
+
305
+ if urls:
306
+ send_to_channel("リンクを取得しました。必要ならファイルをアップロードする処理を追加してください。")
307
+ else:
308
+ send_to_channel("リンクが取得できませんでした。別の方法を試すか、ログを確認してください。")
309
+
310
+ processed_messages.add(latest_msg["id"])
311
+ print(f"処理完了: {video_id}")
312
+
313
+ except Exception as e:
314
+ print("エラー:", e)
315
+ import traceback
316
+ traceback.print_exc()
317
 
318
+ time.sleep(15)
319
 
320
  if __name__ == "__main__":
321
+ main()