Preserve valuable Douyin videos into an Obsidian vault: a lightweight Markdown note records metadata and the caption, while the video file is stored separately in the local Downloads directory, so large binaries are not stuffed into a git-tracked knowledge base.
Customer-readable description
What this skill can do
| Customer wants | Skill will do | Customer can see |
|---|---|---|
| Archive a Douyin video link | Use Playwright to open the video page and intercept the signed API to get the author/caption/duration/interaction counts and a downloadable direct link; download the MP4 locally; write a lightweight Markdown note in the vault (including media_local_path) |
A new .md file is added to the vault, and the terminal prints the file path, author, publish time, download path, and interaction data |
| Only want to see whether this link can be parsed first, without downloading the video yet | Add --metadata-only: only scrape metadata and caption to write the note, skipping video download |
In the note, media_local_path is empty,media_fetched: false, and the log indicates the number of candidate direct links |
| Archiving the same video repeatedly | Deduplicate by the frontmatter url: of existing notes in the vault |
Print ⚠️ Already archived and exit, without re-downloading or re-writing the note |
| Missing Playwright dependency | Stop and provide the install command, without guessing at a fallback | ❌ error message starting with + pip install playwright && python -m playwright install chromium |
How customers use it
- Provide a Douyin video link (
https://www.douyin.com/video/<id>, a link with themodal_id=/resource_id=parameter, or av.douyin.comshort link). - Run
python3 scripts/archive_douyin.py <URL>in the vault root directory; if not inside the vault, add--vault <path>。 - The script automatically deduplicates, scrapes, downloads, and writes the note; if anything fails throughout the process, it will use
❌to report the specific reason, and will not leave a half-finished note. - After archiving is complete, follow the "Next step" prompt in the receipt to fill in
## 摘要、topics、people。
Dependencies and installation
| Dependency | Type | Install / configure | What to do if missing |
|---|---|---|---|
playwright(Python package) |
Hard dependency | pip install playwright && python -m playwright install chromium |
Stop and print the install command, exit with a non-zero code, and do not make any speculative fallback |
| Obsidian vault | Hard dependency | Any structure, as long as it can be located by one of the three: --vault / OBSIDIAN_VAULT / cwd auto-discovery |
Report an error and explain how to specify the path |
Python 3 stdlib(urllib) |
Hard dependency | Comes with Python, no installation required | — |
Install this skill (single skill):
npx skills add soia-team/soia-open-skills -g -a '*' -s soia-pkm-clip-douyin -y
Configuration conventions:
~/.config/soia-skills/soia-open-skills/soia-pkm/soia-pkm-clip-douyin/config.yml
SOIA_PKM_CLIP_DOUYIN_CONFIG_FILE=<custom-config-path>
- If you only archive occasionally and do not want a fixed vault path, you can skip creating
config.yml, and just pass--vaultas an argument each time. - Supported variable names:
OBSIDIAN_VAULT、OBSIDIAN_ARTICLES. The real path is only written in the privateconfig.yml, see config.example.yml。 - Priority:
--vault/--articles-dirCLI argument >OBSIDIAN_VAULT/OBSIDIAN_ARTICLESprocess environment variable >SOIA_PKM_CLIP_DOUYIN_CONFIG_FILEpoints to the privateconfig.yml> Auto-discover the vault upward from the current working directory (look forAGENTS.md/.obsidian) > Generic fallbackArticles/(and print a warning to stderr).
Private information and intermediate data
- The video file itselfdoes not go into the vault(the vault is a git-tracked Markdown knowledge base, not suitable for storing large binary files); MP4 lands in
~/Downloads/soia-pkm-clip-douyin/<video_id>/video.mp4, and the vault note only stores an index of this local absolute path. - Signed direct links intercepted by Playwright (including
msToken/a_bogusand other token parameters)are not written to the vault, not written to logs, not written to stdout—failure receipts only print the host+path with the query string removed, to avoid leaving authentication tokens in plaintext in logs/notes that may be shared. - The signed direct link itself is short-lived: by design, the script will not cache the bare original video direct link to disk or pass it across processes;
--metadata-onlyafterward, if the user wants the video, the script must be rerun (see "Boundaries and Exceptions"). - Playwright uses a one-off headless browser context (does not persist login state, does not use the user's real Douyin account cookies), and only performs anonymous web browsing; it does not write a provider login-state directory.
- It does not cache or persist to disk any private information of anyone other than the user; captured author nicknames/UIDs are only used for the frontmatter/body of this note.
Logs and completion receipts
完成:<一句话说明本次归档了什么>。
日志摘要:
- started: 已定位 vault / video_id=<id>
- processed: 1 条抖音视频
- created/updated: <vault 内笔记相对路径>
- skipped/failed: <0 或具体原因>
文件变化:
- <vault 笔记绝对路径>
- <本地 MP4 绝对路径,或"未下载(--metadata-only)">
验证:
- 笔记内 `media_local_path` 文件是否存在、MP4 文件头是否为有效 ftyp box
问题与下一步:
- <缺 playwright / 视频不可见 / 需要客户确认 / 建议下一条命令;没有则写"无">
Location
Videos within the Douyin App are inconvenient to accumulate over time into a searchable textual knowledge asset: the copy gets buried in the feed, cannot be found, cannot be searched, and cannot be bidirectionally linked with other notes. This skill archives the metadata and copy of a single Douyin video into Obsidian with one click, while the video itself stays on the local disk, keeping the vault lightweight.
Prerequisites
- Obsidian vault (any structure)
- Python 3 +
playwright(required for intercepting the signed API, see the installation command above); the download step itself is pure stdliburllib, no additional dependencies are needed - Vault location priority:
- Command line
--vault <path> - Environment variable
OBSIDIAN_VAULT - Auto-discover the vault upward from the current working directory (look for
AGENTS.md/.obsidian)
- Command line
- Article directory location priority:
- Command line
--articles-dir <subdir> - Environment variable
OBSIDIAN_ARTICLES - Fall back to the generic default
Articles; the script will print a warning to stderr, and the exit code remains 0
- Command line
Note: tools such as Claude Code / Codex / Gemini CLI / opencode often execute commands in a non-login shell and may not inherit the
exportfrom the shell startup file. The script will first load the privateconfig.yml, then auto-discover the vault from cwd; if the agent is already in the vault directory, just run it directly.
-
Private
config.yml(optional,do not place in the vault or an open-source skill repository):Priority:
$SOIA_PKM_CLIP_DOUYIN_CONFIG_FILE(or compatible alias$SOIA_PKM_CLIP_DOUYIN_ENV_FILE)>~/.config/soia-skills/soia-open-skills/soia-pkm/soia-pkm-clip-douyin/config.yml。Supported variable names:
OBSIDIAN_VAULT、OBSIDIAN_ARTICLES. The real values are only written in the privateconfig.ymland are not written into this file.
Core mechanism: Playwright intercepts the signed API
The Douyin web playback address is dynamically generated by client-side signing (msToken/a_bogus); the page SSR JSON (RENDER_DATA) does not contain play_addr, there is no pure HTTP way to fetch the data, and a real browser must be used once:
playwright.chromium.launch(headless=True)+new_context(user_agent=<desktop UA>, locale="zh-CN")。page.on("response", handler)listens for alldouyin.comJSON responses under the domain.- Matching strategyBy content, not endpoint name: recursively search each response body for a
str(node["aweme_id"]) == 目标iddictionary that itself has a"video"key—regardless of whether it appears in the expectedaweme/detail/endpoint, or is mixed into theaweme/mix/array returned by collection endpoints likeaweme_list[]. - Poll until a hit or timeout (default 60s,
--timeoutadjustable). - After a hit, take direct links by priority:
play_addr→play_addr_h264→download_addr, trying downloads one by one until success.
| Key field | Purpose |
|---|---|
aweme["video"]["play_addr"]["url_list"][0] |
Video direct link (h264/download_addr fallback) |
aweme["desc"] |
Caption/title |
aweme["author"]["nickname"] / ["uid"] |
Author |
aweme["create_time"] |
Publish time (Unix seconds) |
aweme["video"]["duration"] |
Duration (milliseconds) |
aweme["statistics"] |
digg_count/comment_count/collect_count/share_count/play_count/admire_count/recommend_count |
The download step uses pure stdlib urllib.request:User-Agent + Referer: https://www.douyin.com/ + Range: bytes=0-, streams to disk and verifies the byte count and MP4 magic bytes (ftyp box)。
Trigger words
| User says | Invoke |
|---|---|
| "Archive this Douyin: URL" / "clip this Douyin video" | Run in the vault root archive_douyin.py <URL>; when not inside the vault, add --vault <path> |
| "First see if this link can be parsed" / "only the caption, no video" | Add --metadata-only, skip video download |
| "Re-archive this Douyin" (already exists) | Add --force, will keep the existing topics/people/## 摘要 |
Workflow (default archive)
- Parse URL →
video_id(short links first undergo one HTTP redirect resolution before matching) - Call script
archive_douyin.py <URL> - The script checks whether the vault has already archived it (by whether the note frontmatter
url:line contains thatvideo_id)- Already exists → output
SKIP, exit 0 - Otherwise → start Playwright to intercept the signed response and get the
awemeobject
- Already exists → output
- When not
--metadata-only: download the MP4 to~/Downloads/soia-pkm-clip-douyin/<video_id>/video.mp4; if any step fails, do not write the note - Write the vault note (frontmatter + body)
- AI fills in afterward:
- 1-sentence Chinese
## 摘要 - 1-3
topicsdouble links +peopledouble links
- 1-sentence Chinese
- The "My take" section is always left blank for the user
File naming
<YYYY-MM-DD>-抖音-<作者昵称前 20 字>-<文案前 50 字>.md
Multiple entries by the same author on the same day → append -<video_id 后 6 位>。
Output frontmatter
---
tags: [视频摘抄]
source: 抖音
url: <用户给出的原始链接>
author: <昵称>
published_at: YYYY-MM-DD HH:mm
captured_at: YYYY-MM-DD HH:mm
language: zh
type: video
topics: [] # AI 填
people: [] # AI 填
media_local_path: "<本地 MP4 绝对路径,--metadata-only 时为空>"
content_complete: true
metrics:
likes: N
comments: N
collects: N
shares: N
plays: N
admires: N
recommends: N
---
Body structure:# <标题> → > [!source]- source collapsible block (source/capture method/metadata/media status) → ## 摘要(AI fills in) → ## 原文(original Douyin caption, unprocessed) → ## 我的看法(left blank) → ## 关联。
Boundaries and exceptions
| Scenario | Handling |
|---|---|
URL does not contain /video/<id>、modal_id=、resource_id=, and is not a short link that can be resolved by redirect |
Reject, with an error explaining the supported formats |
Playwright polling times out without intercepting the target aweme |
Video deleted/private/region-blocked, or Douyin changed the API structure; report the error clearly, do not guess |
Intercepted aweme but all candidate direct-link downloads fail |
Print the failure reason for each one (URL with query string removed), exit non-zero,do not write the note |
With --metadata-only after archiving, if you want the video |
you must rerun the script completely once more — signed direct links are short-lived, and the script does not use a two-stage design of "record the direct link first, download later" |
| the same video is archived again using a short link, after it was previously archived using the full link | the short-link text itself does not contain the numeric video_id, so deduplication by frontmatter url: line substring matching may fail; it is recommended to prioritize archiving with the full douyin.com/video/<id> link |
| same day, same author, duplicate filename | append at the end -<video_id 后 6 位> |
| already archived in the vault | default SKIP and exit 0;--force only then overwrite (preserving existing topics/people/## 摘要) |
command-line reference
python3 scripts/archive_douyin.py <URL>
python3 scripts/archive_douyin.py <URL> --force # 覆盖已归档
python3 scripts/archive_douyin.py <URL> --vault /path/to/vault # 覆盖环境变量
python3 scripts/archive_douyin.py <URL> --articles-dir <articles-subdir>
python3 scripts/archive_douyin.py <URL> --metadata-only # 只抓元数据,跳过视频下载
python3 scripts/archive_douyin.py <URL> --timeout 90 # 拦截等待超时秒数(默认 60)
verification and testing
adversarial review (correctness + security dual lens) found 1 should-fix:find_existing_archive deduplication matching was originally a bare substring test against the entire url: line, without adding boundary anchors like archive_x.py 's /status/{id} —video_id when it happens to be a substring of a longer numeric string (or some query parameter value) in another already-archived link, it will be misjudged as "already archived," silently missing a genuinely new video. Changed to (?<!\d){video_id}(?!\d) numeric boundary matching: does not assume url: which link form is stored (/video/、modal_id=、resource_id= or the form after short-link resolution are both possible), only requires that neither side is a digit, balancing false-positive prevention and format independence.
real end-to-end verification (not mock, 2026-07-21, real public Douyin video):
- full archive: Playwright intercepts
aweme/detailobtains a 132MB video candidate direct link, download completes,fileconfirms a valid MP4, frontmatter like/comment/favorite/share counts match the manually verified real-time numbers (the slight difference between two readings of the same video within 40 minutes is real like growth, not a bug) - rerun the same video (without adding
--force): correctlySKIP - deduplication boundary fix verified with real archive results: normal scenario (real
/video/<id>link) unaffected, still correctly identifies as archived
py_compile + audit_skills.py: no findings.
receipt after completion
delivery order: first write the files to disk, then output the receipt below, not the other way around; uncertain metadata (such as author or publish time parsing failure) must be explicitly marked "unverified" in the receipt, do not fabricate.
after executionmustoutput to the user (do not do it silently):
- what was done — a one-sentence summary of the completed work.
- file changes — list the newly created vault note paths and local MP4 paths (full paths);
--metadata-onlywhen , explain that the video was not downloaded.- if
Articles/fallback was used, you must explicitly note that this directory is not the official archive location of this vault, and recommend that the user configure--articles-dir、OBSIDIAN_ARTICLESor privateconfig.ymland then relocate it.
- if
- next steps — optional follow-up suggestions (such as adding
## 摘要/topics/people, or linking tosoia-pkm-organize-article-moc)。