commit c13916168e50c83107f094cbd2ba6243c62b846c Author: Claude Date: Sat Oct 24 23:57:52 2026 +0900 SNS予約:YouTube の本編の X 投稿も、公開予約に合わせて予定表に入れる - tools/sync_youtube_shorts.py が sns/VIDEO_*_sns_posts.json も読む(タイトル、なければ語と長さで結びつけ、URL は youtu.be) - 1本目:何台入れたら店は変わる(配膳ロボット導入シミュレーション 第1回) - 試し:いまの youtube_videos.json では変わるものなし(前と同じ)。タイトルを直して上げた想定の偽の行では、19:30・20:00 の重なりを避けて 20:30 に入り、check も通った Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CF5RbAfS7xaH2fgqEUe7xG diff --git a/docs/sns_autopost.md b/docs/sns_autopost.md index dc9ef32..b7f70a9 100644 --- a/docs/sns_autopost.md +++ b/docs/sns_autopost.md @@ -64,9 +64,25 @@ Facebook は個人アカウントへの API 投稿ができないので、これ - 同じ時刻に X の予約があれば、19:30 なら 20:00、それ以外は30分ずつ後ろへずらす - YouTube で公開の時刻を変えたら、まだ投稿していない予約の時刻も次の同期で直る。公開の時刻を過ぎてから見つけたものは入れない - LinkedIn には入れない(動画は LinkedIn に直接アップロードする決まりのため、手で出す) -- 同期は予定のタスク「ショートのX予約をYouTubeに合わせる」が1日2回(7:47・17:47)動かす。Mac が眠っていると、その回は何もしない +- 同期は予定のタスク「ショートと本編のX予約をYouTubeに合わせる」が1日2回(7:47・17:47)動かす(本編も同じ回で入る)。Mac が眠っていると、その回は何もしない - YouTube へのアップロードと公開予約は、まだ手で行う(API で上げるには Google の審査が要る。審査前に API で上げた動画は非公開から変えられない) +### YouTube の本編(sns/VIDEO_{名前}_sns_posts.json) + +本編の動画も、YouTube の公開予約に合わせて X に出す(2026-10-24 エイイチの指示。「何台入れたら店は変わる」から)。ショートと同じ同期(`tools/sync_youtube_shorts.py`)で入る。 + +```json +{"meta": {"kind": "youtube_video", "title": "動画名(作業フォルダの名前)", + "youtube_title": "タイトル.txt のタイトル", "title_match": ["配膳ロボット", "何台"], "seconds": 860, + "url": "(自動)", "youtube_id": "(自動)"}, + "posts": {"x_template": "本文…\n{url}\n#R2R2R #PixelsToMotors", "x": "(自動)"}} +``` + +- 文は動画を作った会話で、投稿文_X_LinkedIn.txt の X の文から作って置く(`{url}` が `https://youtu.be/{id}` に替わる。URL込み140字)。ファイル名の `{名前}` は英数字とハイフン +- 動画との結びつけ: YouTube のタイトルが `youtube_title` と同じもの。なければ、`title_match` の語をすべて含み、長さ(秒)が `seconds` と3秒以内のもの(上げるときにタイトルを直しても結びつく) +- 時刻の決まり(19:30 が重なったら 20:00、ほかは30分ずつ後ろへ)・公開の時刻を過ぎてから見つけたものは入れない・時刻を変えたら次の同期で直る、はショートと同じ +- LinkedIn には入れない(動画は LinkedIn に直接アップロードする決まり) + ## 結果(sns/posted.json) | status | 意味 | どうするか | diff --git a/tools/sync_youtube_shorts.py b/tools/sync_youtube_shorts.py index 558c08c..b6fcf83 100644 --- a/tools/sync_youtube_shorts.py +++ b/tools/sync_youtube_shorts.py @@ -1,13 +1,17 @@ #!/usr/bin/env python3 -"""sync_youtube_shorts.py — YouTube で公開予約したショートの X 投稿を、公開の時刻に合わせて予定表に入れる。 +"""sync_youtube_shorts.py — YouTube で公開予約したショートと本編の X 投稿を、公開の時刻に合わせて予定表に入れる。 元になるもの: sns/SHORT_ep{N}_sns_posts.json ショートの X の文(posts.x_template。{url} がショートの URL に替わる) + sns/VIDEO_{名前}_sns_posts.json 本編の X の文(2026-10-24 から。{url} が https://youtu.be/{id} に替わる)。 + 動画との結びつけは、タイトルが meta.youtube_title と同じもの、 + なければ meta.title_match の語をすべて含み、長さが meta.seconds と3秒以内のもの + (YouTube に上げるときにタイトルを直しても結びつくように) youtube_videos.json エイイチの Mac が1時間ごとに書き出す自分のチャンネルの一覧 (_制作ベース/youtube_schedule.py。説明文の manga/ep{N} から manga_ep を拾う) やること: - 1. manga_ep(なければタイトル)で、ショートの json と YouTube の動画を結びつける + 1. ショートは manga_ep(なければタイトル)、本編はタイトル(なければ語と長さ)で、json と YouTube の動画を結びつける 2. 公開予約(または公開済み)の時刻を X の予約の時刻にする。同じ時刻に別の X の予約があれば、 19:30 なら 20:00、それ以外は30分ずつ後ろへずらす(2026-10-24 エイイチの指示「19:30が重なったら動画は20:00」) 3. まだ投稿していない予約は、YouTube 側で時刻を変えていたら合わせて直す @@ -41,29 +45,41 @@ def main(): ap.add_argument("--videos", required=True) ap.add_argument("--dry-run", action="store_true") a = ap.parse_args() - vids = [v for v in load(a.videos, {}).get("動画", []) if v.get("is_short")] + allv = load(a.videos, {}).get("動画", []) + vids = [v for v in allv if v.get("is_short")] + mains = [v for v in allv if not v.get("is_short")] sched = load(SCHEDULE, {"posts": []}) posted = load(POSTED, {}) done_ids = set(posted.keys()) if isinstance(posted, dict) else {p.get("id") for p in posted.get("posts", posted) if isinstance(p, dict)} now = datetime.now(timezone.utc) changes = [] - for path in sorted(glob.glob(os.path.join(ROOT, "sns", "SHORT_ep*_sns_posts.json"))): + paths = sorted(glob.glob(os.path.join(ROOT, "sns", "SHORT_ep*_sns_posts.json"))) + \ + sorted(glob.glob(os.path.join(ROOT, "sns", "VIDEO_*_sns_posts.json"))) + for path in paths: src = json.load(open(path, encoding="utf-8")) meta, posts = src["meta"], src["posts"] - ep = meta["episode"] - cand = [v for v in vids if v.get("manga_ep") == ep] or [v for v in vids if norm(v["title"]) == norm(meta.get("youtube_title"))] + is_main = os.path.basename(path).startswith("VIDEO_") + if is_main: + words = meta.get("title_match") or [] + cand = [v for v in mains if norm(v["title"]) == norm(meta.get("youtube_title"))] or \ + [v for v in mains if words and all(w in v["title"] for w in words) + and meta.get("seconds") and v.get("seconds") and abs(v["seconds"] - meta["seconds"]) <= 3] + sid = os.path.basename(path)[:-len("_sns_posts.json")] + "-x" + else: + ep = meta["episode"] + cand = [v for v in vids if v.get("manga_ep") == ep] or [v for v in vids if norm(v["title"]) == norm(meta.get("youtube_title"))] + sid = f"SHORT_ep{ep}-x" cand = [v for v in cand if v.get("publishAt") or v.get("published")] if not cand: continue v = sorted(cand, key=lambda v: v.get("publishAt") or v.get("published"))[-1] when = datetime.fromisoformat((v.get("publishAt") or v.get("published")).replace("Z", "+00:00")) - sid = f"SHORT_ep{ep}-x" entry = next((e for e in sched["posts"] if e["id"] == sid), None) if entry and sid in done_ids: continue if not entry and when < now: continue - url = f"https://youtube.com/shorts/{v['id']}" + url = f"https://youtu.be/{v['id']}" if is_main else f"https://youtube.com/shorts/{v['id']}" text = posts["x_template"].replace("{url}", url) if posts.get("x") != text or meta.get("url") != url or meta.get("youtube_id") != v["id"]: posts["x"], meta["url"], meta["youtube_id"] = text, url, v["id"]