# -*- coding: utf-8 -*- """ 定时检查 YouTube 频道是否有直播存档视频,有就删除 优化重点: - 新增开关:DELETE_ONLY_RECENT_48H(只删最近48小时内的存档) - 慢速删除:每删一个随机等待 5~20 秒,每 5 个长休息 1~5 分钟 - 每日上限:默认每天最多删 30 个,避免风控/配额耗尽 - 限流保护:遇到 quota/rate 错误自动暂停 30 分钟 - 支持 uploads playlist + search fallback """ # 网址 获取频道ID # https://www.tunepocket.com/youtube-channel-id-finder # 申请OATH2 # https://console.cloud.google.com/apis/library/youtube.googleapis.com # 启用 # YouTube Data API v3 import os import pickle import time import random from datetime import datetime, timedelta, timezone from googleapiclient.discovery import build from google_auth_oauthlib.flow import InstalledAppFlow from google.auth.transport.requests import Request import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) # ===================== 配置 ===================== CLIENT_SECRETS_FILE = "client_secrets.json" SCOPES = [ "https://www.googleapis.com/auth/youtube", "https://www.googleapis.com/auth/youtube.force-ssl", "https://www.googleapis.com/auth/youtubepartner" ] API_SERVICE_NAME = "youtube" API_VERSION = "v3" CHECK_INTERVAL_SECONDS = 1800 # 检查间隔:30 分钟 # CHANNEL_ID = "UC-8V3pDMlM2B75fvbYRBsNg" #vlog CHANNEL_ID = "UCXFErW9DeE8EXrInlnyCnVw" #food # 安全删除参数 MAX_DAILY_DELETES = 30 # 每天最多删 30 个(可调 10~50) SHORT_DELAY_MIN = 5 # 每删一个最小等待秒 SHORT_DELAY_MAX = 20 # 每删一个最大等待秒 LONG_DELAY_AFTER = 5 # 每删几个后长休息 LONG_DELAY_MIN = 60 # 长休息最小秒 LONG_DELAY_MAX = 300 # 长休息最大秒 LIMIT_PAUSE_SECONDS = 1800 # 配额/限流时暂停 30 分钟 # 新增开关:是否只删除最近 48 小时内的直播存档 DELETE_ONLY_RECENT_48H = True # True = 只删 48 小时内;False = 删除所有 # ================================================ CREDENTIALS_FILE = "youtube_token.pickle" def get_authenticated_service(): credentials = None if os.path.exists(CREDENTIALS_FILE): with open(CREDENTIALS_FILE, "rb") as token: credentials = pickle.load(token) if not credentials or not credentials.valid: if credentials and credentials.expired and credentials.refresh_token: credentials.refresh(Request()) else: flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRETS_FILE, SCOPES) credentials = flow.run_local_server(port=0) with open(CREDENTIALS_FILE, "wb") as token: pickle.dump(credentials, token) return build(API_SERVICE_NAME, API_VERSION, credentials=credentials) def delete_live_archive_videos(youtube): try: deleted_count = 0 uploads_playlist_id = None # 尝试获取 uploads playlist try: channels_response = youtube.channels().list( part="contentDetails", id=CHANNEL_ID ).execute() if channels_response.get("items"): related_playlists = channels_response["items"][0].get("contentDetails", {}).get("relatedPlaylists", {}) uploads_playlist_id = related_playlists.get("uploads") if uploads_playlist_id: logger.info(f"找到 uploads playlist ID: {uploads_playlist_id}") else: logger.warning("未找到 uploads playlist - 尝试 search fallback") except Exception as e: logger.warning(f"获取 uploads playlist 失败: {e}") video_ids = [] # 优先用 uploads playlist if uploads_playlist_id: next_page_token = None while True: playlist_response = youtube.playlistItems().list( part="snippet", playlistId=uploads_playlist_id, maxResults=50, pageToken=next_page_token ).execute() for item in playlist_response.get("items", []): video_ids.append(item["snippet"]["resourceId"]["videoId"]) next_page_token = playlist_response.get("nextPageToken") if not next_page_token: break time.sleep(2) logger.info(f"从 uploads playlist 发现 {len(video_ids)} 个视频 ID") # fallback: search 已结束直播 else: next_page_token = None while True: search_response = youtube.search().list( part="id,snippet", channelId=CHANNEL_ID, eventType="completed", type="video", maxResults=50, pageToken=next_page_token, order="date" ).execute() for item in search_response.get("items", []): if item["id"]["kind"] == "youtube#video": video_ids.append(item["id"]["videoId"]) next_page_token = search_response.get("nextPageToken") if not next_page_token: break time.sleep(2) logger.info(f"从 search 发现 {len(video_ids)} 个潜在直播存档视频 ID") if not video_ids: logger.info("未发现任何视频 ID,本次检查结束") return # 计算 48 小时前的 UTC 时间(如果开关开启) cutoff_time = None if DELETE_ONLY_RECENT_48H: cutoff_time = datetime.now(timezone.utc) - timedelta(hours=48) logger.info(f"只删除结束时间晚于 {cutoff_time.isoformat()} 的存档") # 批量检查 + 慢速删除 batch_size = 20 for i in range(0, len(video_ids), batch_size): if deleted_count >= MAX_DAILY_DELETES: logger.info(f"达到每日上限 {MAX_DAILY_DELETES} 个,停止本次检查") break batch = video_ids[i:i + batch_size] try: videos_response = youtube.videos().list( part="id,snippet,liveStreamingDetails", id=",".join(batch) ).execute() except Exception as e: logger.error(f"批量获取视频详情失败: {e}") continue for video in videos_response.get("items", []): if deleted_count >= MAX_DAILY_DELETES: break video_id = video["id"] title = video["snippet"].get("title", "无标题") live_details = video.get("liveStreamingDetails", {}) actual_end_time_str = live_details.get("actualEndTime") if not actual_end_time_str: continue # 解析结束时间 try: end_time = datetime.fromisoformat(actual_end_time_str.replace("Z", "+00:00")) except: continue # 如果开关开启,只删 48 小时内的 if DELETE_ONLY_RECENT_48H and end_time < cutoff_time: continue # 慢速删除 try: delay = random.uniform(SHORT_DELAY_MIN, SHORT_DELAY_MAX) logger.info(f"找到直播存档: {title} | {video_id} | 结束于 {actual_end_time_str}") logger.info(f"等待 {delay:.1f} 秒后删除...") time.sleep(delay) youtube.videos().delete(id=video_id).execute() logger.info(f"成功删除: {title} | {video_id}") deleted_count += 1 if deleted_count % LONG_DELAY_AFTER == 0: long_delay = random.uniform(LONG_DELAY_MIN, LONG_DELAY_MAX) logger.info(f"已删 {deleted_count} 个,长休息 {long_delay/60:.1f} 分钟...") time.sleep(long_delay) except Exception as e: err_str = str(e) logger.error(f"删除失败 {video_id}: {err_str}") if any(word in err_str.lower() for word in ["quota", "rate", "limit", "exceeded"]): logger.warning(f"触发限流/配额,暂停 {LIMIT_PAUSE_SECONDS//60} 分钟...") time.sleep(LIMIT_PAUSE_SECONDS) time.sleep(3) # 批次间额外间隔 if deleted_count > 0: logger.info(f"本次检查删除了 {deleted_count} 个直播存档") else: logger.info("本次未发现或未删除任何直播存档") except Exception as e: logger.exception("检查/删除过程中发生严重错误") def main(): youtube = get_authenticated_service() logger.info("YouTube 直播存档自动删除任务启动...") logger.info(f"监控频道: {CHANNEL_ID}") logger.info(f"检查间隔: {CHECK_INTERVAL_SECONDS} 秒") logger.info(f"每日删除上限: {MAX_DAILY_DELETES} 个") logger.info(f"只删除最近 48 小时内存档: {'是' if DELETE_ONLY_RECENT_48H else '否'}\n") while True: try: delete_live_archive_videos(youtube) except Exception as e: logger.exception("主循环异常") time.sleep(CHECK_INTERVAL_SECONDS) if __name__ == "__main__": main()