效率工具

Disney+ 家庭看片计划:按工作日与周末时长安排儿童观影

用 Python 根据工作日、周末的观看时长上限,把自选片单排入日历并生成 .ics 文件;配合 Disney+ 家长控制管理儿童观看。

BINBHO · 图文教程

给孩子排一份看片计划:工作日晚上 19:00 最多看 30 分钟,周末上午最多 90 分钟。脚本把片单按集排进这些时间段,生成 .ics 导入家庭日历,事件里会写明用哪个档案观看(比如开启了 Junior Mode 的儿童档案),观看时长的管理和 Disney+ 自带的家长控制配合使用。

先看清成品

pytest 跑了 4 个测试,全部通过;示例片单从 2026-10-22(周四)开始排,周四、周五各 1 集,周六 2 集,周日 1 部 85 分钟的电影,一共 5 个事件,用 icalendar 解析通过。片名是虚构的示例,没有连接 Disney+,也没有在真实的日历 App 里导入测试过。家长控制的说明来自帮助中心页面(2026-10-21 查看)。

注意:Disney+ 没有面向个人开发者的公开 API。本系列只用官方帮助中心公布的数字和规则,以及你自己填写的数据。不要用 VPN 或代理切换地区,不要绕过 DRM、不要录屏或导出下载文件,不要在家庭以外共享账号(以订阅协议为准)。套餐价格和可用地区以官方页面为准,本文不列数字。

做完之后你会得到什么?

成果 说明
watch_plan.py 按时间段和时长上限排片,生成 .ics
watchlist.json 档案名、时间段和片单(片名、集数、每集分钟数)
报错保护 一集比时间段还长时直接报错,不会排成死循环

需要准备哪些环境?

项目 要求 说明
Python 3.8 以上 只用标准库(测试用 pytest)
Disney+ 档案 建议给孩子单独建一个档案 在网页版或手机 App 里设置家长控制

第 1 步:先在 Disney+ 里设好家长控制

帮助中心列出的功能有:Content ratings(每个档案的内容分级)、Junior Mode、Profile PIN、Profile creation restriction(限制新建档案),以及 Not Rated & Live Content 设置。页面说明这些只能在支持的网页浏览器或 Disney+ 手机 App 里管理,路径是 EDIT PROFILES → 选档案 → PARENTAL CONTROLS。另外,Junior Mode 档案不支持内容分级设置。

这个脚本只负责“什么时候看、看多久”,内容是否适合孩子交给上面这些官方功能来管。

文件结构

34-disney-plus-C-src/
├── watch_plan.py
├── watchlist.json
└── test_plan.py

第 2 步:脚本

watch_plan.py:

"""把家庭片单排成日历(.ics),导入手机或电脑日历。
不连接 Disney+、不读账号数据:片名、集数和时长都由你自己在 watchlist.json 里填写。
用法:python watch_plan.py watchlist.json 开始日期YYYY-MM-DD [输出.ics]"""
import hashlib, json, sys
from datetime import date, datetime, timedelta

def schedule(cfg, start):
    """每天按 slots 的时间段排片;每个时间段的时长 = 分钟上限,放得下几集就排几集。"""
    queue = [(w["title"], ep, w["minutes"]) for w in cfg["watchlist"] for ep in range(1, w["episodes"] + 1)]
    for t, _, m in queue:
        if m <= 0: raise ValueError(f"{t}:minutes 必须大于 0")
    events, day = [], start
    while queue:
        slot = cfg["slots"]["weekend" if day.weekday() >= 5 else "weekday"]
        if slot:
            hh, mm = map(int, slot["start"].split(":"))
            t, used = datetime.combine(day, datetime.min.time()).replace(hour=hh, minute=mm), 0
            while queue and used + queue[0][2] <= slot["max_minutes"]:
                title, ep, m = queue.pop(0)
                events.append((title, ep, t, t + timedelta(minutes=m)))
                t += timedelta(minutes=m); used += m
            if used == 0:
                raise ValueError(f"《{queue[0][0]}》一集 {queue[0][2]} 分钟,超过时间段上限 {slot['max_minutes']} 分钟")
        day += timedelta(days=1)
    return events

def esc(s): return s.replace("\\", "\\\\").replace(",", "\\,").replace(";", "\\;")

def to_ics(events, profile, stamp="20261021T000000Z"):
    out = ["BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//binbho//watch-plan//ZH"]
    for title, ep, s, e in events:
        out += ["BEGIN:VEVENT", f"UID:{s:%Y%m%dT%H%M}-{ep}-{hashlib.sha1(title.encode()).hexdigest()[:8]}@watch-plan", f"DTSTAMP:{stamp}",
                f"DTSTART:{s:%Y%m%dT%H%M%S}", f"DTEND:{e:%Y%m%dT%H%M%S}",
                f"SUMMARY:{esc(title)} 第{ep}集", f"DESCRIPTION:{esc('用「' + profile + '」档案观看')}", "END:VEVENT"]
    return "\r\n".join(out + ["END:VCALENDAR"]) + "\r\n"

if __name__ == "__main__":
    cfg = json.load(open(sys.argv[1], encoding="utf-8"))
    ev = schedule(cfg, date.fromisoformat(sys.argv[2]))
    for title, ep, s, e in ev: print(f"{s:%m-%d %a %H:%M}-{e:%H:%M}  {title} 第{ep}集")
    open(sys.argv[3] if len(sys.argv) > 3 else "plan.ics", "w", encoding="utf-8", newline="").write(to_ics(ev, cfg["profile"]))

watchlist.json:

{
  "profile": "孩子(Junior Mode)",
  "slots": {"weekday": {"start": "19:00", "max_minutes": 30},
            "weekend": {"start": "10:00", "max_minutes": 90}},
  "watchlist": [
    {"title": "示例动画剧集", "episodes": 4, "minutes": 22},
    {"title": "示例动画电影", "episodes": 1, "minutes": 85}
  ]
}

第 3 步:测试和运行

test_plan.py:

from datetime import date
import pytest
from watch_plan import schedule, to_ics
CFG = {"profile": "p", "slots": {"weekday": {"start": "19:00", "max_minutes": 30}, "weekend": {"start": "10:00", "max_minutes": 90}},
       "watchlist": [{"title": "S", "episodes": 4, "minutes": 22}, {"title": "M, 电影", "episodes": 1, "minutes": 85}]}

def test_schedule():
    ev = schedule(CFG, date(2026, 10, 22))         # 周四
    days = [s.date().isoformat() for _, _, s, _ in ev]
    assert days == ["2026-10-22", "2026-10-23", "2026-10-24", "2026-10-24", "2026-10-25"]
    assert ev[3][2].strftime("%H:%M") == "10:22"

def test_too_long():
    bad = dict(CFG, slots={"weekday": {"start": "19:00", "max_minutes": 30}, "weekend": {"start": "10:00", "max_minutes": 60}})
    with pytest.raises(ValueError, match="超过时间段上限"): schedule(bad, date(2026, 10, 22))

def test_ics_escape():
    s = to_ics(schedule(CFG, date(2026, 10, 22)), "p")
    assert "SUMMARY:M\\, 电影 第1集" in s and s.count("BEGIN:VEVENT") == 5

def test_uid_stable():
    import subprocess, sys
    code = "from watch_plan import *;from test_plan import CFG;from datetime import date;print(to_ics(schedule(CFG,date(2026,10,22)),'p'))"
    a, b = (subprocess.run([sys.executable, "-c", code], capture_output=True, text=True).stdout for _ in range(2))
    assert a == b
$ python -m pytest -q
4 passed
$ python watch_plan.py watchlist.json 2026-10-22
10-22 Thu 19:00-19:22  示例动画剧集 第1集
10-23 Fri 19:00-19:22  示例动画剧集 第2集
10-24 Sat 10:00-10:22  示例动画剧集 第3集
10-24 Sat 10:22-10:44  示例动画剧集 第4集
10-25 Sun 10:00-11:25  示例动画电影 第1集

检查点:周六两集紧挨着排(10:22 开始第 4 集),加起来 44 分钟,没有超过 90 分钟;电影 85 分钟放不进工作日的 30 分钟,所以排到了周日。片名里的逗号在 .ics 里转义成了 ``(见测试)。

调试示例:每次生成的 UID 都不一样

第一版用 hash(title) 拼 UID。连续运行两次,同一个事件的 UID 分别是:

UID:20261022T1900-1-10926250@watch-plan
UID:20261022T1900-1-40459461@watch-plan

原因是 Python 的字符串 hash 每个进程都会带随机盐。UID 一变,日历 App 就会把重新导入的事件当成新事件,结果全部重复。改成 hashlib.sha1(title) 的前 8 位以后,UID 就稳定了;test_uid_stable 会启动两个进程,比较两次的输出是否完全一致。

常见问题

现象 原因 处理
超过时间段上限 某一集(或电影)比所有时间段都长 调大周末的 max_minutes,或者把这部片拿出来单独安排
日历里的时间差了几个小时 事件没有带时区,按设备的本地时间显示 在同一个时区的设备上导入
孩子切换到了大人的档案 档案没有加锁 用 Profile PIN 给大人的档案加锁

模板规则

  • 时长由计划管理,内容由官方家长控制管理,两者配合使用。
  • 片单自己填写,不要爬取 Disney+ 的片库。
  • UID 要稳定,重新导入时才不会重复。

项目资源

用途 地址
家长控制 https://help.disneyplus.com/article/disneyplus-parental-controls
iCalendar 规范 RFC 5545 https://www.rfc-editor.org/rfc/rfc5545
icalendar(用于验证) https://pypi.org/project/icalendar/
Disney+ 订阅协议 https://www.disneyplus.com/legal/subscriber-agreement

一个 JSON 文件加上一个日历文件,就能把“每天看多久”排得清清楚楚。账号和内容分级仍然交给 Disney+ 自己的家长控制。