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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +77 -291
app.py CHANGED
@@ -1,321 +1,107 @@
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()
 
 
 
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)