import os import re import time import json import requests from datetime import datetime, timezone # ===== 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("dmsendertoken") 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, } # ===== 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 = [ r"(?:[?&](?:v|vi)=)([A-Za-z0-9_-]{11})", r"youtu\.be/([A-Za-z0-9_-]{11})", r"youtube(?:-nocookie)?\.com/(?:embed|v|shorts)/([A-Za-z0-9_-]{11})", r"youtube(?:-nocookie)?\.com/watch\?.*?v=([A-Za-z0-9_-]{11})", 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 extract_youtube_and_option(text): parts = re.split(r"[ \u3000]+", text.strip()) video_id = extract_youtube_id(parts[0]) option = None if len(parts) > 1: option = parts[1].lower() return video_id, option def normalize_quality(option): if option is None: return "720" trans_table = str.maketrans( "0123456789kKpP", "0123456789kKpP" ) option = option.translate(trans_table).lower() quality_map = { "144": "144", "240": "240", "360": "360", "480": "480", "720": "720", "1080": "1080", "2k": "1440", "1440": "1440", "4k": "2160", "2160": "2160", "8k": "4320", "4320": "4320", "144p": "144", "240p": "240", "360p": "360", "480p": "480", "720p": "720", "1080p": "1080", "2kp": "1440", "1440p": "1440", "4kp": "2160", "2160p": "2160", "8kp": "4320", "4320p": "4320", } if option in quality_map: return quality_map[option] return None # 不正な指定 # ===== izu-ytdl API ===== def download_video_from_api(video_id, quality): api_url = ( f"https://render-ytdlp-awqz.onrender.com/download" f"?v={video_id}" f"&q=bv*[height<={quality}]+ba/b" ) res = requests.get(api_url, timeout=300) res.raise_for_status() return res.content # ===== Channel送信 ===== 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() def upload_file_to_channel(file_bytes): upload_url = "https://media.channel.io/cht/v1/pri-file/200605/groups/519217/message/send_yt_video_file.mp4" headers = { "x-account": X_ACCOUNT, "Content-Type": "video/mp4", "Content-Length": str(len(file_bytes)), } res = requests.post( upload_url, headers=headers, data=file_bytes, timeout=300 ) res.raise_for_status() return res.json() def send_video_message(file_json): request_id = f"desk-web-{int(time.time() * 1000)}" payload = { "requestId": request_id, "blocks": [ {"type": "text", "value": "プレビュー:"} ], "files": [file_json], } 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 video_id, option = extract_youtube_and_option(latest_msg["plainText"]) if not video_id: time.sleep(10) continue valid_options = [ "144","240","360","480","720","1080", "1440","2160","4320","4k","8k","2k" ] if option is None: send_to_channel( "画質が指定されていないため、デフォルトの720pを使用します。\n" "指定する場合は、動画URLの後にスペースを入れて、" "144/240/360/480/720/1080/2k(または1440)/4k(または2160)/8k(または4320) のいずれかを入力してください。\n" "注意:高画質や長い動画は時間がかかる場合があります。" ) quality_opt = "720" else: if option.lower() not in valid_options: send_to_channel( "不明な画質、または予期しない形式でした。\n" "対応画質: 144, 240, 360, 480, 720, 1080, " "2k(1440), 4k(2160), 8k(4320)\n" "例: https://youtu.be/abcdEFGHijk 1080" ) processed_messages.add(latest_msg["id"]) continue quality_opt = normalize_quality(option) send_to_channel(f"{video_id} のダウンロードを開始します。({quality_opt}p)") try: video_bytes = download_video_from_api(video_id, quality_opt) send_to_channel("動画をアップロードしています...") file_json = upload_file_to_channel(video_bytes) send_video_message(file_json) send_to_channel("完了しました!") except Exception as e: send_to_channel(f"エラーが発生しました: {e}") processed_messages.add(latest_msg["id"]) except Exception as e: print("エラー:", e) time.sleep(15) if __name__ == "__main__": main()