reborn-ytdlpkit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 REBORN LABS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # reborn-ytdlpkit
2
+
3
+ **yt-dlp — 깔기 전에 볼 것 네 가지.** 클로드 코드 스킬 설치기입니다.
4
+
5
+ ```bash
6
+ npx --yes reborn-ytdlpkit
7
+ ```
8
+
9
+ `~/.claude/skills/ytdlp-guide/` 에 스킬을 깝니다. **클로드 코드를 껐다 켜야 잡힙니다.**
10
+
11
+ ## 이 킷이 파는 것
12
+
13
+ yt-dlp 는 한 줄로 깔립니다. 아무도 그걸 못 하는 게 아닙니다.
14
+ 사람들이 막히는 건 **아무도 말해 주지 않는 조건 네 가지**에서입니다.
15
+
16
+ | | 무엇 | 왜 무서운가 |
17
+ |---|---|---|
18
+ | ① | **ffmpeg 가 없으면 4K 가 안 됩니다** | 오류가 안 납니다. 낮은 화질로 **조용히** 떨어집니다 |
19
+ | ② | `pip install youtube-dl` 은 다른 걸 깝니다 | PyPI 최신판이 **2021-12-16** 에 멈춰 있습니다 |
20
+ | ③ | 유튜브 전체 지원엔 **JS 런타임**이 필요합니다 | 2026년에 생긴 조건. 옛 안내글엔 없습니다 |
21
+ | ④ | **Unlicense 는 「도구」 이야기입니다** | 받았다고 영상의 권리가 넘어오지 않습니다 |
22
+
23
+ ## 명령
24
+
25
+ ```bash
26
+ npx --yes reborn-ytdlpkit 스킬을 깝니다
27
+ npx --yes reborn-ytdlpkit doctor 이 컴퓨터에 조건이 갖춰졌는지 실제로 잽니다
28
+ npx --yes reborn-ytdlpkit how 맞는 설치 명령과 틀린 명령을 나란히
29
+ npx --yes reborn-ytdlpkit facts 원장 전체를 찍습니다
30
+ npx --yes reborn-ytdlpkit verify 저장소·PyPI 가 지금 응답하는지 (네트워크)
31
+ npx --yes reborn-ytdlpkit --check 깔렸는지 + 원장이 규칙을 지키는지
32
+ npx --yes reborn-ytdlpkit --self-test 자기시험 (네트워크 안 씀)
33
+ ```
34
+
35
+ `doctor` 는 **적혀 있는 걸 읽지 않고 이 컴퓨터에서 실제로 불러 봅니다** —
36
+ yt-dlp · ffmpeg/ffprobe · JS 런타임이 지금 응답하는지.
37
+
38
+ ## 숫자에 관하여
39
+
40
+ 원장(`skill/ytdlp.json`)의 모든 숫자에는 **확인일과 출처**가 붙어 있습니다.
41
+ 스타 수도 최신 버전도 밖에서 바뀌므로, 스킬은 기억이 아니라 **그 파일을 읽고** 답합니다.
42
+
43
+ - 스타 187,390 · 지원 사이트 1,731개 (2026-08-28 · GitHub API, `supportedsites.md` 계수)
44
+ - 「1,731개」는 **목록에 이름이 올라 있는** 수입니다. 로그인·지역 제한은 그대로 남습니다.
45
+
46
+ ## 만든 곳
47
+
48
+ 문의 <https://mobility.rebornlabs.kr/cs> · 안내 <https://rebornlabs.kr/ytdlp?utm_source=installer&utm_medium=readme&utm_campaign=ytdlp>
49
+ MIT. yt-dlp 자체는 별개 프로젝트이고 Unlicense 입니다.
package/cli.mjs ADDED
@@ -0,0 +1,560 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * yt-dlp — 깔기 전에 볼 것. 스킬 설치기.
4
+ *
5
+ * npx --yes reborn-ytdlpkit 클로드 코드에 스킬을 깐다
6
+ * npx --yes reborn-ytdlpkit how 맞는 설치 명령과 틀린 명령을 나란히 (안 깔고)
7
+ * npx --yes reborn-ytdlpkit facts 원장 전체를 찍는다
8
+ * npx --yes reborn-ytdlpkit doctor 이 컴퓨터에 조건이 갖춰졌는지 실제로 잰다
9
+ * npx --yes reborn-ytdlpkit verify 저장소·PyPI 가 지금 응답하는지 (네트워크)
10
+ * npx --yes reborn-ytdlpkit --check 깔렸는지 + 원장이 규칙을 지키는지
11
+ * npx --yes reborn-ytdlpkit --self-test 자기시험 (네트워크 안 씀)
12
+ *
13
+ * ★★이 설치기가 파는 건 도구가 아니라 **조건표**다.
14
+ * yt-dlp 는 한 줄로 깔린다. 아무도 그걸로 못 하는 게 아니다.
15
+ * 사람들이 막히는 건 **아무도 말해 주지 않는 조건 네 가지**에서다:
16
+ *
17
+ * ① `pip install youtube-dl` — 2021-12-16 에 멈춘 원조를 깐다. 오늘 유튜브에서 실패한다.
18
+ * ② **ffmpeg 바이너리가 없으면 4K 가 안 된다.** 그런데 오류가 안 난다 —
19
+ * 합칠 필요 없는 낮은 화질로 **조용히** 떨어진다. 이게 이 킷의 첫 번째 이유다.
20
+ * ③ 유튜브 전체 지원엔 **yt-dlp-ejs + JS 런타임**이 필요하다(2026년에 생긴 조건).
21
+ * 오래된 설치 안내글을 따라 하면 유튜브에서만 이상하게 실패한다.
22
+ * ④ **Unlicense 는 「도구」 이야기다.** 내려받은 영상의 권리는 넘어오지 않는다.
23
+ *
24
+ * ★★자기시험은 **양방향**이다. 좋은 원장을 통과시키는 것만으로는 아무것도 증명 못 한다.
25
+ * 일부러 조건을 지운 가짜 원장을 만들어 검사기가 **그걸 잡는지** 같이 본다.
26
+ * 전건 통과하는 검사기는 검사기가 아니다. [[verifier-must-be-tested-both-ways]]
27
+ */
28
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
29
+ import { execFileSync } from "node:child_process";
30
+ import { homedir } from "node:os";
31
+ import { dirname, join, relative } from "node:path";
32
+ import { fileURLToPath } from "node:url";
33
+ import { 링크, TAIL, 마지막 } from "./tail.mjs";
34
+
35
+ const HOME = homedir();
36
+ const 여기 = dirname(fileURLToPath(import.meta.url));
37
+ const c = { g: "\x1b[32m", r: "\x1b[31m", y: "\x1b[33m", d: "\x1b[90m", cy: "\x1b[36m", b: "\x1b[1m", 0: "\x1b[0m" };
38
+
39
+ const 스킬이름 = "ytdlp-guide";
40
+ const 퍼널 = "ytdlp";
41
+ const 원본 = join(여기, "skill");
42
+ const 대상 = join(HOME, ".claude", "skills", 스킬이름);
43
+
44
+ /** ★반드시 「틀린 명령」으로 적혀 있어야 하는 것들. 이게 이 킷의 존재 이유다. */
45
+ const 반드시틀린명령 = ["pip install youtube-dl", "npm i yt-dlp", "pip install ffmpeg"];
46
+
47
+ /** ★공식 README 가 적어 둔 원문. 번역만 남기면 나중에 「몰랐다」가 된다. */
48
+ const 화질원문핵심 = /Required for merging separate video and audio files/;
49
+ const 유튜브원문핵심 = /Required for full YouTube support/;
50
+
51
+ /** ★조건 없이 단정하는 말. 홍보문에 섞이면 잡는다. [[kmong-no-unbounded-guarantee]] */
52
+ const 금칙 = [/완전\s*무료/, /무제한/, /평생/, /100\s*%\s*보장/, /언제나\s*성공/, /어디서든\s*받/];
53
+
54
+ const 머리말 = () => {
55
+ console.log(`\n${c.b}yt-dlp — 깔기 전에 볼 것${c[0]} ${c.d}· REBORN LABS${c[0]}`);
56
+ console.log(`${c.d}${"─".repeat(52)}${c[0]}`);
57
+ };
58
+
59
+ function 파일목록(뿌리) {
60
+ const 모음 = [];
61
+ for (const 이름 of readdirSync(뿌리)) {
62
+ const p = join(뿌리, 이름);
63
+ if (statSync(p).isDirectory()) 모음.push(...파일목록(p));
64
+ else 모음.push(p);
65
+ }
66
+ return 모음;
67
+ }
68
+
69
+ function 원장읽기(폴더) {
70
+ const f = join(폴더, "ytdlp.json");
71
+ if (!existsSync(f)) return null;
72
+ try { return JSON.parse(readFileSync(f, "utf8")); } catch { return null; }
73
+ }
74
+
75
+ /* ══ 원장검사 — 「복사가 됐나」가 아니라 「조건을 숨기지 않았나」로 판정한다 ══ */
76
+ function 원장검사(d) {
77
+ if (!d) return { 됨: false, 이유: "원장(ytdlp.json)이 없거나 깨졌습니다" };
78
+ if (!d.확인일) return { 됨: false, 이유: "확인일이 없습니다 — 숫자는 매일 바뀝니다" };
79
+ if (!d.확인방법) return { 됨: false, 이유: "확인방법이 없습니다 — 어디서 셌는지 없으면 지어낸 것과 구분이 안 됩니다" };
80
+ if (!d.제품?.무엇 || !d.제품?.무엇이아닌가)
81
+ return { 됨: false, 이유: "제품 칸에 「무엇인가/무엇이 아닌가」가 다 있어야 합니다" };
82
+
83
+ /* ── ① ★설치 — 이름이 비슷한 걸 까는 사고가 제일 많다 ─────────── */
84
+ const I = d["★설치"];
85
+ if (!I) return { 됨: false, 이유: "설치 칸이 없습니다" };
86
+ if (!I.맞는명령 || !/yt-dlp/.test(I.맞는명령))
87
+ return { 됨: false, 이유: "맞는 설치 명령이 없습니다" };
88
+ if (!I.필요한것) return { 됨: false, 이유: "설치에 필요한 것(파이썬 판)이 없습니다" };
89
+ if (!Array.isArray(I["★틀린명령"]) || !I["★틀린명령"].length)
90
+ return { 됨: false, 이유: "「틀린명령」이 비었습니다 — 이 킷이 존재하는 이유가 바로 그 칸입니다" };
91
+ for (const 틀 of 반드시틀린명령) {
92
+ const 있 = I["★틀린명령"].find((x) => String(x?.명령).trim() === 틀);
93
+ if (!있) return { 됨: false, 이유: `「${틀}」 이 틀린 명령으로 적혀 있지 않습니다 — 사람들이 실제로 이걸 칩니다` };
94
+ if (!있.무슨일) return { 됨: false, 이유: `「${틀}」 에 실제로 무슨 일이 생기는지가 없습니다 — 「쓰지 마라」만으로는 안 지킵니다` };
95
+ }
96
+
97
+ /* ── ② ★화질조건 — 이 킷의 첫 번째 이유. 무음 실패다 ─────────── */
98
+ const Q = d["★화질조건"];
99
+ if (!Q) return { 됨: false, 이유: "화질조건 칸이 없습니다 — 「4K 됩니다」만 남으면 사고를 파는 것입니다" };
100
+ if (!화질원문핵심.test(String(Q.원문 || "")))
101
+ return { 됨: false, 이유: "공식 README 원문(\"Required for merging separate video and audio files\")이 없습니다 — 번역만 남기면 나중에 못 다툽니다" };
102
+ if (!Q.출처) return { 됨: false, 이유: "화질조건의 출처가 없습니다" };
103
+ if (!Q["★무음실패"])
104
+ return { 됨: false, 이유: "「오류 없이 조용히 낮은 화질로 떨어진다」가 없습니다 — 이게 이 조건의 핵심입니다" };
105
+ if (!Q.그래서어떻게)
106
+ return { 됨: false, 이유: "조건만 있고 「그래서 어떻게」가 없습니다 — 막기만 하면 아무도 안 씁니다" };
107
+
108
+ /* ── ③ ★유튜브조건 — 2026년에 새로 생겼다 ─────────────────────── */
109
+ const Y = d["★유튜브조건"];
110
+ if (!Y) return { 됨: false, 이유: "유튜브조건 칸이 없습니다" };
111
+ if (!유튜브원문핵심.test(String(Y.원문 || "")))
112
+ return { 됨: false, 이유: "공식 README 원문(\"Required for full YouTube support\")이 없습니다" };
113
+ if (!Array.isArray(Y.런타임) || Y.런타임.length < 2)
114
+ return { 됨: false, 이유: "쓸 수 있는 JS 런타임이 2개 미만입니다 — 하나만 적으면 그것만 되는 줄 압니다" };
115
+ if (!Y.그래서어떻게) return { 됨: false, 이유: "유튜브조건에 「그래서 어떻게」가 없습니다" };
116
+
117
+ /* ── ④ ★낡으면막힌다 — 실패의 대부분이 여기다 ─────────────────── */
118
+ const U = d["★낡으면막힌다"];
119
+ if (!U) return { 됨: false, 이유: "「낡으면 막힌다」 칸이 없습니다 — 실패 문의의 대부분이 이겁니다" };
120
+ if (!U.최신판) return { 됨: false, 이유: "최신판이 없습니다" };
121
+ if (!/-U|--upgrade|upgrade/.test(String(U.그래서어떻게 || "")))
122
+ return { 됨: false, 이유: "올리는 명령이 「그래서 어떻게」에 없습니다" };
123
+
124
+ /* ── ⑤ ★권리 — 원본 릴스가 틀린 자리다 ───────────────────────── */
125
+ const R = d["★권리"];
126
+ if (!R) return { 됨: false, 이유: "권리 칸이 없습니다 — 「받으면 내 것」이 되게 두면 안 됩니다" };
127
+ if (!/Unlicense/.test(String(R.핵심 || "")))
128
+ return { 됨: false, 이유: "라이선스가 「도구」를 가리킨다는 설명에 Unlicense 근거가 없습니다" };
129
+ if (!R.무엇은괜찮나 || !R.무엇이아닌가)
130
+ return { 됨: false, 이유: "「괜찮은 것」과 「아닌 것」이 둘 다 있어야 합니다 — 금지만 적으면 아무것도 못 하게 됩니다" };
131
+ if (!R.출처) return { 됨: false, 이유: "권리 설명의 출처가 없습니다" };
132
+
133
+ /* ── ⑥ 규모 — 「엄청 많다」가 아니라 몇 개인지 ─────────────────── */
134
+ const G = d.규모;
135
+ if (!G) return { 됨: false, 이유: "규모 칸이 없습니다" };
136
+ if (!Number.isFinite(G.스타)) return { 됨: false, 이유: "스타 수가 숫자가 아닙니다" };
137
+ if (!Number.isFinite(G.지원사이트)) return { 됨: false, 이유: "지원 사이트 수가 숫자가 아닙니다 — 「수천 개」는 셀 수 없습니다" };
138
+ if (!G.출처) return { 됨: false, 이유: "지원 사이트 수의 출처가 없습니다 — 어디서 셌는지 없으면 지어낸 것과 구분이 안 됩니다" };
139
+ if (!G.확인일) return { 됨: false, 이유: "규모에 확인일이 없습니다 — 숫자는 매일 바뀝니다" };
140
+ if (!G.주의) return { 됨: false, 이유: "지원 사이트 수에 「주의」가 없습니다 — 목록에 있는 것과 지금 받히는 것은 다릅니다" };
141
+
142
+ /* ── ⑦ 쓸모 — 숫자만 있고 무엇인지 없으면 소용없다 ────────────── */
143
+ if (!Array.isArray(d.쓸모) || d.쓸모.length < 3)
144
+ return { 됨: false, 이유: "쓸모가 3개 미만입니다" };
145
+ for (const m of d.쓸모)
146
+ if (!m?.이름 || !m?.예) return { 됨: false, 이유: "쓸모에 실제 명령(예)이 빠진 항목이 있습니다" };
147
+
148
+ /* ── ⑧ 금칙어 — 조건 없이 단정하지 않는다 ─────────────────────── */
149
+ const 홍보문 = [
150
+ d.제품?.무엇, d.제품?.무엇이아닌가,
151
+ Q.그래서어떻게, Y.그래서어떻게, U.그래서어떻게, R.그래서어떻게,
152
+ ...(d.쓸모 || []).map((m) => m.이름),
153
+ ].join(" ");
154
+ for (const re of 금칙)
155
+ if (re.test(홍보문))
156
+ return { 됨: false, 이유: `설명에 「${re.source}」 에 걸리는 말이 있습니다 — 조건 없이 단정하지 않습니다` };
157
+
158
+ /* ── ⑨ 안 하는 말 — 무엇을 뺐느냐가 원장의 절반이다 ───────────── */
159
+ if (!Array.isArray(d.안하는말) || d.안하는말.length < 3)
160
+ return { 됨: false, 이유: "「안하는말」 이 3개 미만입니다 — 무엇을 안 말하는지가 원장의 절반입니다" };
161
+ for (const x of d.안하는말)
162
+ if (!x?.말 || !x?.이유) return { 됨: false, 이유: "「안하는말」 에 이유가 없는 항목이 있습니다" };
163
+ if (!d.안하는말.some((x) => /내 것이 된다|내것이 된다/.test(x.말)))
164
+ return { 됨: false, 이유: "「안하는말」 에 「받으면 내 것이 된다」 금지가 없습니다 — 원본 릴스가 틀린 바로 그 자리입니다" };
165
+ if (!d.안하는말.some((x) => /4K/.test(x.말)))
166
+ return { 됨: false, 이유: "「안하는말」 에 「깔기만 하면 4K」 금지가 없습니다" };
167
+
168
+ return { 됨: true, 스타: G.스타, 사이트: G.지원사이트, 확인일: d.확인일, 최신판: U.최신판 };
169
+ }
170
+
171
+ /** 깔린 스킬이 실제로 읽히는지 — 머리말까지 확인한다. */
172
+ function 진짜도나() {
173
+ const md = join(대상, "SKILL.md");
174
+ if (!existsSync(md)) return { 됨: false, 이유: "SKILL.md 가 없습니다" };
175
+ const 본문 = readFileSync(md, "utf8");
176
+ if (!new RegExp("^---[\\s\\S]*?\\bname:\\s*" + 스킬이름 + "\\b", "m").test(본문))
177
+ return { 됨: false, 이유: "SKILL.md 머리말이 깨졌습니다(name 을 못 읽습니다)" };
178
+ return 원장검사(원장읽기(대상));
179
+ }
180
+
181
+ const 원장고르기 = () => 원장읽기(existsSync(join(대상, "ytdlp.json")) ? 대상 : 원본);
182
+ const 원장이나쁘면멈춤 = (d) => {
183
+ const r = 원장검사(d);
184
+ if (!r.됨) { console.error(`${c.r}✗ 원장이 이상합니다${c[0]} — ${r.이유}`); process.exit(1); }
185
+ return r;
186
+ };
187
+
188
+ /* ── install (안 깔고 보여주기만) ───────────────────────────────── */
189
+ function 설치보기() {
190
+ const d = 원장고르기();
191
+ 원장이나쁘면멈춤(d);
192
+ 머리말();
193
+ const I = d["★설치"];
194
+
195
+ console.log(`${c.g}${c.b}✅ 맞는 명령${c[0]}`);
196
+ console.log(` ${c.cy}${c.b}${I.맞는명령}${c[0]}`);
197
+ console.log(` ${c.d}${I.무엇이뜨나}${c[0]}`);
198
+ console.log(` ${c.d}필요한 것 — ${I.필요한것}${c[0]}`);
199
+ for (const x of I.다른길 || []) console.log(` ${c.d}${x.환경.padEnd(10)} ${x.명령}${c[0]}`);
200
+
201
+ console.log(`\n${c.r}${c.b}✗ 이렇게 치면 딴 게 깔립니다${c[0]}`);
202
+ for (const x of I["★틀린명령"]) {
203
+ console.log(` ${c.r}${x.명령}${c[0]}`);
204
+ console.log(` ${c.d}→ ${x.무슨일}${c[0]}`);
205
+ if (x.왜생기나) console.log(` ${c.d} (${x.왜생기나})${c[0]}`);
206
+ }
207
+
208
+ const Q = d["★화질조건"];
209
+ console.log(`\n${c.y}${c.b}★ 깔고 나서 바로 막히는 자리${c[0]}`);
210
+ console.log(` ${c.y}${Q.무엇}${c[0]}`);
211
+ console.log(` ${c.d}${Q.왜}${c[0]}`);
212
+ console.log(` ${c.r}${Q["★무음실패"]}${c[0]}`);
213
+ console.log(` ${c.g}${Q.그래서어떻게}${c[0]}\n`);
214
+ console.log(`${c.d}확인일 ${d.확인일}. 버전과 스타 수는 밖에서 바뀝니다.${c[0]}\n`);
215
+ }
216
+
217
+ /* ── facts ──────────────────────────────────────────────────────── */
218
+ function 원장보기() {
219
+ const d = 원장고르기();
220
+ 원장이나쁘면멈춤(d);
221
+ 머리말();
222
+
223
+ const P = d.제품;
224
+ console.log(`${c.b}${P.정식이름}${c[0]} ${c.d}· ${P.만든곳} · ${P.라이선스}${c[0]}`);
225
+ console.log(`${c.d}${P.무엇}${c[0]}`);
226
+ console.log(`${c.y}아닌 것 — ${P.무엇이아닌가}${c[0]}\n`);
227
+
228
+ const I = d["★설치"];
229
+ console.log(`${c.g}${c.b}설치${c[0]} ${c.cy}${I.맞는명령}${c[0]}`);
230
+ for (const x of I["★틀린명령"]) console.log(` ${c.r}✗ ${x.명령.padEnd(24)}${c[0]}${c.d}${x.무슨일.slice(0, 58)}…${c[0]}`);
231
+
232
+ const Q = d["★화질조건"];
233
+ console.log(`\n${c.y}${c.b}화질${c[0]} ${c.y}${Q.무엇}${c[0]}`);
234
+ console.log(` ${c.d}"${Q.원문.replace(/\*\*/g, "")}"${c[0]}`);
235
+ console.log(` ${c.r}${Q["★무음실패"]}${c[0]}`);
236
+
237
+ const Y = d["★유튜브조건"];
238
+ console.log(`\n${c.b}유튜브${c[0]} ${c.y}${Y.무엇}${c[0]}`);
239
+ console.log(` ${c.d}"${Y.원문.replace(/\*\*/g, "")}"${c[0]}`);
240
+ console.log(` ${c.d}쓸 수 있는 것 — ${Y.런타임.join(" · ")}${c[0]}`);
241
+
242
+ const U = d["★낡으면막힌다"];
243
+ console.log(`\n${c.b}판올림${c[0]} ${c.cy}최신 ${U.최신판}${c[0]}`);
244
+ console.log(` ${c.d}${U.그래서어떻게}${c[0]}`);
245
+
246
+ const R = d["★권리"];
247
+ console.log(`\n${c.b}권리${c[0]} ${c.d}(${R.출처})${c[0]}`);
248
+ console.log(` ${c.r}${R.핵심}${c[0]}`);
249
+ console.log(` ${c.g}괜찮은 것 — ${R.무엇은괜찮나}${c[0]}`);
250
+ console.log(` ${c.y}아닌 것 — ${R.무엇이아닌가}${c[0]}`);
251
+
252
+ const G = d.규모;
253
+ console.log(`\n${c.b}규모${c[0]} ${c.cy}스타 ${G.스타.toLocaleString()} · 지원 사이트 ${G.지원사이트.toLocaleString()}${c[0]} ${c.d}(${G.확인일} 기준)${c[0]}`);
254
+ console.log(` ${c.d}${G.출처}${c[0]}`);
255
+ console.log(` ${c.y}${G.주의}${c[0]}`);
256
+
257
+ console.log(`\n${c.b}자주 쓰는 한 줄${c[0]}`);
258
+ for (const m of d.쓸모) console.log(` ${c.d}${m.이름.padEnd(14)}${c[0]}${c.cy}${m.예}${c[0]}`);
259
+
260
+ console.log(`\n${c.b}이 스킬이 하지 않는 말${c[0]}`);
261
+ for (const x of d.안하는말) console.log(` ${c.r}✗${c[0]} ${x.말} ${c.d}— ${x.이유}${c[0]}`);
262
+ console.log(`\n${c.d}확인일 ${d.확인일} · ${d.확인방법}${c[0]}\n`);
263
+ }
264
+
265
+ /* ── doctor — 이 컴퓨터에 조건이 갖춰졌는지 **실제로** 잰다 ────────
266
+ ★「있다고 적혀 있다」가 아니라 「지금 응답하는가」로 본다.
267
+ [[python-interpreter-must-be-probed]] 와 같은 자리다. */
268
+ function 있나(명령, 인자) {
269
+ try {
270
+ const out = execFileSync(명령, 인자, { stdio: ["ignore", "pipe", "ignore"], timeout: 15000 });
271
+ return { 됨: true, 판: String(out).split("\n")[0].trim().slice(0, 60) };
272
+ } catch { return { 됨: false, 판: null }; }
273
+ }
274
+
275
+ function 진단() {
276
+ const d = 원장고르기();
277
+ 원장이나쁘면멈춤(d);
278
+ 머리말();
279
+ console.log(`${c.d}적혀 있는 걸 읽는 게 아니라, 이 컴퓨터에서 실제로 불러 봅니다.${c[0]}\n`);
280
+
281
+ const yt = 있나("yt-dlp", ["--version"]);
282
+ const ff = 있나("ffmpeg", ["-version"]);
283
+ const fp = 있나("ffprobe", ["-version"]);
284
+ const js = ["deno", "node", "bun"].map((n) => ({ 이름: n, ...있나(n, ["--version"]) })).find((x) => x.됨);
285
+
286
+ const 줄 = (표, 이름, 말) => console.log(` ${표} ${이름.padEnd(22)} ${c.d}${말}${c[0]}`);
287
+ 줄(yt.됨 ? `${c.g}✓${c[0]}` : `${c.r}✗${c[0]}`, "yt-dlp", yt.됨 ? yt.판 : "없습니다");
288
+ 줄(ff.됨 && fp.됨 ? `${c.g}✓${c[0]}` : `${c.y}!${c[0]}`, "ffmpeg / ffprobe", ff.됨 && fp.됨 ? ff.판 : "없습니다 — 4K 가 조용히 막힙니다");
289
+ 줄(js ? `${c.g}✓${c[0]}` : `${c.y}!${c[0]}`, "JS 런타임", js ? `${js.이름} ${js.판}` : "없습니다 — 유튜브 전체 지원이 막힙니다");
290
+
291
+ const 문제 = [];
292
+ if (!yt.됨) 문제.push([`yt-dlp 가 없습니다`, d["★설치"].맞는명령]);
293
+ else if (yt.판 && d["★낡으면막힌다"].최신판 && yt.판 < d["★낡으면막힌다"].최신판)
294
+ 문제.push([`yt-dlp 가 ${yt.판} 입니다 — 최신은 ${d["★낡으면막힌다"].최신판} 입니다`, "pip install -U yt-dlp"]);
295
+ if (!(ff.됨 && fp.됨)) 문제.push([d["★화질조건"]["★무음실패"], "macOS: brew install ffmpeg · Windows: winget install Gyan.FFmpeg"]);
296
+ if (!js) 문제.push([d["★유튜브조건"].무엇, "deno 나 node 중 하나를 까세요"]);
297
+
298
+ if (!문제.length) {
299
+ console.log(`\n${c.g}${c.b}✓ 조건이 다 갖춰져 있습니다.${c[0]} ${c.d}받는 것만 남았습니다.${c[0]}`);
300
+ console.log(` ${c.cy}yt-dlp -F <URL>${c[0]} ${c.d}로 무엇이 있는지 먼저 보세요.${c[0]}\n`);
301
+ process.exit(0);
302
+ }
303
+ console.log(`\n${c.y}${c.b}★ 이대로 받으면 막히는 자리${c[0]}`);
304
+ for (const [무엇, 어떻게] of 문제) {
305
+ console.log(` ${c.y}· ${무엇}${c[0]}`);
306
+ console.log(` ${c.cy}${어떻게}${c[0]}`);
307
+ }
308
+ console.log("");
309
+ process.exit(1);
310
+ }
311
+
312
+ /* ── verify (네트워크) ──────────────────────────────────────────── */
313
+ async function 확인명령() {
314
+ const d = 원장고르기();
315
+ 원장이나쁘면멈춤(d);
316
+ 머리말();
317
+ console.log(`${c.d}주소가 지금 응답하는지 봅니다. 응답한다고 이 컴퓨터에서 받힌다는 뜻은 아닙니다 —${c[0]}`);
318
+ console.log(`${c.d}그건 조건 이야기이고, ${c.cy}doctor${c[0]}${c.d} 로 봅니다.${c[0]}\n`);
319
+
320
+ const 볼곳 = [
321
+ ["저장소", d.제품.저장소],
322
+ ["PyPI", "https://pypi.org/pypi/yt-dlp/json"],
323
+ ["지원목록", "https://raw.githubusercontent.com/yt-dlp/yt-dlp/master/supportedsites.md"],
324
+ ];
325
+ let 끊김 = 0;
326
+ for (const [이름, 주소] of 볼곳) {
327
+ let 표, 말;
328
+ try {
329
+ const res = await fetch(주소, { redirect: "follow", signal: AbortSignal.timeout(20000) });
330
+ if (res.ok) { 표 = `${c.g}✓${c[0]}`; 말 = String(res.status); }
331
+ else { 표 = `${c.y}!${c[0]}`; 말 = String(res.status); 끊김++; }
332
+ } catch (e) { 표 = `${c.r}✗${c[0]}`; 말 = String(e.message).slice(0, 40); 끊김++; }
333
+ console.log(` ${표} ${이름.padEnd(12)} ${c.d}${말}${c[0]}`);
334
+ }
335
+ console.log(끊김
336
+ ? `\n${c.y}${끊김}건이 응답하지 않습니다 — 원장을 다시 봐야 합니다.${c[0]}\n`
337
+ : `\n${c.g}전부 응답합니다.${c[0]} ${c.d}(이 컴퓨터의 조건은 ${c.cy}npx --yes reborn-ytdlpkit doctor${c[0]}${c.d} 로 보세요)${c[0]}\n`);
338
+ process.exit(끊김 ? 1 : 0);
339
+ }
340
+
341
+ /* ── --check ────────────────────────────────────────────────────── */
342
+ function 확인만() {
343
+ 머리말();
344
+ const r = 진짜도나();
345
+ if (r.됨) {
346
+ console.log(`${c.g}✓ 설치되어 있고 원장이 규칙을 지킵니다${c[0]} ${c.d}(${대상})${c[0]}`);
347
+ console.log(`${c.d} 스타 ${r.스타.toLocaleString()} · 지원 사이트 ${r.사이트.toLocaleString()} · 대상 판 ${r.최신판} · 확인일 ${r.확인일}${c[0]}\n`);
348
+ } else console.log(`${c.y}· 아직 없습니다${c[0]} ${c.d}— ${r.이유}${c[0]}\n`);
349
+ process.exit(r.됨 ? 0 : 1);
350
+ }
351
+
352
+ /* ── 자기시험 (네트워크 안 씀) ─────────────────────────────────── */
353
+ function 자기시험() {
354
+ const 결과 = [];
355
+ const T = (n, ok) => 결과.push([n, !!ok]);
356
+ const 원 = 원장읽기(원본);
357
+ const 복 = () => JSON.parse(JSON.stringify(원));
358
+ const 스킬본문 = () => readFileSync(join(원본, "SKILL.md"), "utf8");
359
+
360
+ T("패키지 안에 skill/ytdlp.json 이 있다", !!원);
361
+ T("★정본 원장이 검사를 통과한다", 원장검사(원).됨);
362
+ T("★`pip install youtube-dl` 이 틀린 명령으로 적혀 있다",
363
+ (원?.["★설치"]?.["★틀린명령"] || []).some((x) => x.명령 === "pip install youtube-dl"));
364
+ T("★`npm i yt-dlp` 도 틀린 명령으로 적혀 있다",
365
+ (원?.["★설치"]?.["★틀린명령"] || []).some((x) => x.명령 === "npm i yt-dlp"));
366
+ T("★`pip install ffmpeg` 함정이 적혀 있다",
367
+ (원?.["★설치"]?.["★틀린명령"] || []).some((x) => x.명령 === "pip install ffmpeg"));
368
+ T("★4K 조건의 README 원문이 그대로 남아 있다", 화질원문핵심.test(원?.["★화질조건"]?.원문 || ""));
369
+ T("★「조용히 낮은 화질로 떨어진다」가 적혀 있다", !!원?.["★화질조건"]?.["★무음실패"]);
370
+ T("★유튜브 조건의 README 원문이 그대로 남아 있다", 유튜브원문핵심.test(원?.["★유튜브조건"]?.원문 || ""));
371
+ T("★JS 런타임이 2개 이상 적혀 있다", (원?.["★유튜브조건"]?.런타임 || []).length >= 2);
372
+ T("★판올림 명령이 적혀 있다", /-U|upgrade/.test(원?.["★낡으면막힌다"]?.그래서어떻게 || ""));
373
+ T("★권리 칸이 Unlicense 를 근거로 든다", /Unlicense/.test(원?.["★권리"]?.핵심 || ""));
374
+ T("★권리 칸에 「괜찮은 것」이 함께 있다", !!원?.["★권리"]?.무엇은괜찮나);
375
+ T("★규모에 확인일과 주의가 붙어 있다", !!원?.규모?.확인일 && !!원?.규모?.주의);
376
+ T("★지원 사이트 수가 숫자이고 출처가 있다",
377
+ Number.isFinite(원?.규모?.지원사이트) && !!원?.규모?.출처);
378
+ T("★「안하는말」 에 이유가 전부 적혀 있다", (원?.안하는말 || []).every((x) => x.말 && x.이유));
379
+ T("★SKILL.md 가 옆에 있고 name 이 맞다", (() => {
380
+ const f = join(원본, "SKILL.md");
381
+ return existsSync(f) && new RegExp("^---[\\s\\S]*?\\bname:\\s*" + 스킬이름 + "\\b", "m").test(readFileSync(f, "utf8"));
382
+ })());
383
+ T("★SKILL.md 가 ytdlp.json 을 읽으라고 못 박는다", 스킬본문().includes("ytdlp.json"));
384
+ T("★SKILL.md 가 ffmpeg 조건을 적어 뒀다", /ffmpeg/.test(스킬본문()));
385
+ T("★SKILL.md 가 youtube-dl 을 틀린 것으로 적어 뒀다",
386
+ /youtube-dl/.test(스킬본문()) && /2021/.test(스킬본문()));
387
+ T("★SKILL.md 가 「받아도 내 것이 아니다」를 적어 뒀다", /권리|저작권/.test(스킬본문()));
388
+ T("★package.json files 에 skill/ 과 안내스킬.md 가 들어 있다", (() => {
389
+ const pj = join(여기, "package.json");
390
+ if (!existsSync(pj)) return false;
391
+ const f = JSON.parse(readFileSync(pj, "utf8")).files || [];
392
+ return f.includes("skill/") && f.includes("안내스킬.md");
393
+ })());
394
+ T("★안내스킬.md 가 스킬팩을 말한다", (() => {
395
+ const f = join(여기, "안내스킬.md");
396
+ return existsSync(f) && /스킬팩|rebornlabs\.kr\/skillpack/.test(readFileSync(f, "utf8"));
397
+ })());
398
+ T("★안내스킬.md 가 클로드킷을 말하지 않는다(계단 유지)", (() => {
399
+ const f = join(여기, "안내스킬.md");
400
+ if (!existsSync(f)) return false;
401
+ return !/클로드킷|자비스\s*풀스택|rebornlabs\.kr\/(claudekit|kit|jarvis)/.test(readFileSync(f, "utf8"));
402
+ })());
403
+
404
+ /* ── ★역시험 — 일부러 깨뜨려 본다. 여기서 통과하면 검사기가 고장난 것이다. ── */
405
+ T("★★`pip install youtube-dl` 경고를 지우면 잡는다", (() => {
406
+ const d = 복();
407
+ d["★설치"]["★틀린명령"] = d["★설치"]["★틀린명령"].filter((x) => x.명령 !== "pip install youtube-dl");
408
+ return !원장검사(d).됨;
409
+ })());
410
+ T("★★`pip install ffmpeg` 함정을 지우면 잡는다", (() => {
411
+ const d = 복();
412
+ d["★설치"]["★틀린명령"] = d["★설치"]["★틀린명령"].filter((x) => x.명령 !== "pip install ffmpeg");
413
+ return !원장검사(d).됨;
414
+ })());
415
+ T("★★틀린 명령에서 「무슨일」을 지우면 잡는다", (() => {
416
+ const d = 복(); delete d["★설치"]["★틀린명령"][0].무슨일; return !원장검사(d).됨;
417
+ })());
418
+ T("★★틀린 명령을 통째로 비우면 잡는다", (() => {
419
+ const d = 복(); d["★설치"]["★틀린명령"] = []; return !원장검사(d).됨;
420
+ })());
421
+ T("★★화질조건 칸을 통째로 지우면 잡는다", (() => {
422
+ const d = 복(); delete d["★화질조건"]; return !원장검사(d).됨;
423
+ })());
424
+ T("★★4K 원문을 번역만 남기면 잡는다", (() => {
425
+ const d = 복(); d["★화질조건"].원문 = "영상과 소리를 합치는 데 필요합니다"; return !원장검사(d).됨;
426
+ })());
427
+ T("★★「조용히 떨어진다」를 지우면 잡는다", (() => {
428
+ const d = 복(); delete d["★화질조건"]["★무음실패"]; return !원장검사(d).됨;
429
+ })());
430
+ T("★★화질조건에서 「그래서어떻게」를 지우면 잡는다", (() => {
431
+ const d = 복(); delete d["★화질조건"].그래서어떻게; return !원장검사(d).됨;
432
+ })());
433
+ T("★★유튜브조건 칸을 지우면 잡는다", (() => {
434
+ const d = 복(); delete d["★유튜브조건"]; return !원장검사(d).됨;
435
+ })());
436
+ T("★★JS 런타임을 하나만 남기면 잡는다", (() => {
437
+ const d = 복(); d["★유튜브조건"].런타임 = ["deno"]; return !원장검사(d).됨;
438
+ })());
439
+ T("★★「낡으면막힌다」 칸을 지우면 잡는다", (() => {
440
+ const d = 복(); delete d["★낡으면막힌다"]; return !원장검사(d).됨;
441
+ })());
442
+ T("★★판올림 명령을 지우면 잡는다", (() => {
443
+ const d = 복(); d["★낡으면막힌다"].그래서어떻게 = "다시 해 보세요"; return !원장검사(d).됨;
444
+ })());
445
+ T("★★권리 칸을 지우면 잡는다", (() => {
446
+ const d = 복(); delete d["★권리"]; return !원장검사(d).됨;
447
+ })());
448
+ T("★★권리에서 Unlicense 근거를 빼면 잡는다", (() => {
449
+ const d = 복(); d["★권리"].핵심 = "받은 영상은 알아서 쓰세요"; return !원장검사(d).됨;
450
+ })());
451
+ T("★★권리에서 「괜찮은 것」을 지우면 잡는다", (() => {
452
+ const d = 복(); delete d["★권리"].무엇은괜찮나; return !원장검사(d).됨;
453
+ })());
454
+ T("★★지원 사이트 수를 「수천 개」로 바꾸면 잡는다", (() => {
455
+ const d = 복(); d.규모.지원사이트 = "수천 개"; return !원장검사(d).됨;
456
+ })());
457
+ T("★★지원 사이트 수의 출처를 지우면 잡는다", (() => {
458
+ const d = 복(); delete d.규모.출처; return !원장검사(d).됨;
459
+ })());
460
+ T("★★규모의 「주의」를 지우면 잡는다", (() => {
461
+ const d = 복(); delete d.규모.주의; return !원장검사(d).됨;
462
+ })());
463
+ T("★★확인일을 지우면 잡는다", (() => { const d = 복(); delete d.확인일; return !원장검사(d).됨; })());
464
+ T("★★확인방법을 지우면 잡는다", (() => { const d = 복(); delete d.확인방법; return !원장검사(d).됨; })());
465
+ T("★★설명에 「완전 무료」를 넣으면 잡는다", (() => {
466
+ const d = 복(); d.제품.무엇 = "완전 무료로 아무거나 받는 도구"; return !원장검사(d).됨;
467
+ })());
468
+ T("★★설명에 「어디서든 받는다」를 넣으면 잡는다", (() => {
469
+ const d = 복(); d["★화질조건"].그래서어떻게 = "어디서든 받을 수 있습니다"; return !원장검사(d).됨;
470
+ })());
471
+ T("★★「안하는말」 을 비우면 잡는다", (() => { const d = 복(); d.안하는말 = []; return !원장검사(d).됨; })());
472
+ T("★★「안하는말」 에서 「내 것이 된다」 항목을 빼면 잡는다", (() => {
473
+ const d = 복(); d.안하는말 = d.안하는말.filter((x) => !/내 것이 된다/.test(x.말));
474
+ while (d.안하는말.length < 3) d.안하는말.push({ 말: "채움", 이유: "채움" });
475
+ return !원장검사(d).됨;
476
+ })());
477
+ T("★★「안하는말」 에서 4K 항목을 빼면 잡는다", (() => {
478
+ const d = 복(); d.안하는말 = d.안하는말.filter((x) => !/4K/.test(x.말));
479
+ while (d.안하는말.length < 3) d.안하는말.push({ 말: "채움", 이유: "채움" });
480
+ return !원장검사(d).됨;
481
+ })());
482
+ T("★없는 원장은 통과하지 않는다", !원장검사(null).됨);
483
+
484
+ for (const [n, ok] of 결과) console.log(` ${ok ? c.g + "✓" : c.r + "✗"}${c[0]} ${n}`);
485
+ const 실패 = 결과.filter(([, ok]) => !ok).length;
486
+ console.log(실패 ? `\n${c.r}✗ 자기시험 ${실패}건 실패${c[0]}` : `\n${c.g}✓ 자기시험 ${결과.length}건 통과${c[0]}`);
487
+ return 실패 ? 1 : 0;
488
+ }
489
+
490
+ /* ── 설치 ───────────────────────────────────────────────────────── */
491
+ function 설치() {
492
+ 머리말();
493
+ if (!existsSync(원본)) {
494
+ console.error(`${c.r}✗ 패키지 안에 skill/ 폴더가 없습니다${c[0]} — 배포가 잘못된 것입니다.`);
495
+ process.exit(2);
496
+ }
497
+ let 쓴수 = 0;
498
+ for (const src of 파일목록(원본)) {
499
+ const 상대 = relative(원본, src);
500
+ const dst = join(대상, 상대);
501
+ mkdirSync(dirname(dst), { recursive: true });
502
+ writeFileSync(dst, readFileSync(src));
503
+ 쓴수++;
504
+ console.log(` ${c.d}·${c[0]} ${상대}`);
505
+ }
506
+ console.log(`\n${c.d}${쓴수}개 파일 → ${대상}${c[0]}`);
507
+ console.log(`${c.d}원장이 조건을 숨기지 않았는지 다시 확인하는 중…${c[0]}`);
508
+
509
+ const r = 진짜도나();
510
+ if (!r.됨) {
511
+ console.error(`\n${c.r}✗ 깔았는데 확인에서 걸렸습니다${c[0]} — ${r.이유}\n`);
512
+ process.exit(1);
513
+ }
514
+
515
+ const d = 원장읽기(대상);
516
+ const I = d["★설치"], Q = d["★화질조건"], R = d["★권리"];
517
+ console.log(`\n${c.g}${c.b}✓ 설치 완료${c[0]} ${c.d}— 지원 사이트 ${r.사이트.toLocaleString()}개 · 스타 ${r.스타.toLocaleString()} (확인일 ${r.확인일})${c[0]}`);
518
+
519
+ console.log(`\n${c.g}${c.b}★ 도구를 까는 건 이 한 줄입니다${c[0]}`);
520
+ console.log(` ${c.cy}${c.b}${I.맞는명령}${c[0]}`);
521
+ console.log(`\n${c.r}${c.b}★ 이렇게 치면 딴 게 깔립니다${c[0]}`);
522
+ for (const x of I["★틀린명령"]) console.log(` ${c.r}${x.명령.padEnd(24)}${c[0]}${c.d}${x.무슨일.slice(0, 58)}…${c[0]}`);
523
+
524
+ console.log(`\n${c.y}${c.b}★ 그리고 깔기만 해서는 4K 가 안 나옵니다.${c[0]}`);
525
+ console.log(` ${c.r}${Q["★무음실패"]}${c[0]}`);
526
+ console.log(` ${c.g}${Q.그래서어떻게}${c[0]}`);
527
+ console.log(` ${c.d}지금 이 컴퓨터가 어떤지는 ${c[0]}${c.cy}npx --yes reborn-ytdlpkit doctor${c[0]}${c.d} 가 재 줍니다.${c[0]}`);
528
+
529
+ console.log(`\n${c.y}${c.b}★ 받았다고 내 것이 되지는 않습니다.${c[0]}`);
530
+ console.log(` ${c.d}${R.핵심}${c[0]}`);
531
+
532
+ console.log(`\n${c.y}★ 클로드 코드를 껐다 켜야 스킬이 잡힙니다.${c[0]}`);
533
+ console.log(`\n${c.b}쓰는 법${c[0]}`);
534
+ console.log(` 클로드에게 그냥 말하세요 — ${c.cy}"이 영상 받아줘"${c[0]} ${c.d}또는${c[0]} ${c.cy}"yt-dlp 4K 가 안 돼"${c[0]}`);
535
+ console.log(` 이 컴퓨터 조건 재기: ${c.cy}npx --yes reborn-ytdlpkit doctor${c[0]}`);
536
+ console.log(` 맞는/틀린 설치 명령: ${c.cy}npx --yes reborn-ytdlpkit how${c[0]}`);
537
+ console.log(` 원장 전체: ${c.cy}npx --yes reborn-ytdlpkit facts${c[0]}`);
538
+ console.log(` ${c.d}버전·스타 수는 밖에서 바뀝니다. 확인일을 같이 적어 둔 이유입니다.${c[0]}\n`);
539
+ console.log(`${c.d}설명 · https://rebornlabs.kr/ytdlp?utm_source=installer&utm_medium=cli&utm_campaign=${퍼널}${c[0]}`);
540
+
541
+ /* ★끝난 뒤 딱 한 칸. 못 깔았으면 여기까지 오지 않는다(위에서 exit 1).
542
+ ★가리키는 곳은 스킬팩(8,900원)이다. 클로드킷은 여기서 말하지 않는다 — 계단이 무너진다. */
543
+ const t = TAIL[퍼널];
544
+ console.log("");
545
+ for (const 줄 of t.이음) console.log(`${c.d}${줄.replace(/\*\*(.+?)\*\*/g, `${c[0]}${c.b}$1${c[0]}${c.d}`)}${c[0]}`);
546
+ console.log(`\n${c.b}${t.제목}${c[0]}${c.d} — ${t.한줄}${c[0]}`);
547
+ console.log(`${c.cy}${링크(퍼널)}${c[0]}`);
548
+ console.log(`${c.d}${마지막}${c[0]}\n`);
549
+ }
550
+
551
+ /* ══ 실행 ═══════════════════════════════════════════════════════ */
552
+ const 인자 = process.argv.slice(2);
553
+ const 첫 = 인자[0];
554
+ if (인자.includes("--self-test")) process.exit(자기시험());
555
+ else if (첫 === "how" || 첫 === "설치법") 설치보기();
556
+ else if (첫 === "facts" || 첫 === "원장") 원장보기();
557
+ else if (첫 === "doctor" || 첫 === "진단") 진단();
558
+ else if (첫 === "verify" || 첫 === "확인") await 확인명령();
559
+ else if (인자.includes("--check")) 확인만();
560
+ else 설치();
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "reborn-ytdlpkit",
3
+ "version": "1.0.0",
4
+ "description": "yt-dlp 로 영상을 받을 때 무엇이 조용히 막히는지 알려 주는 클로드 코드 스킬. 4K 가 안 나오는 진짜 이유(ffmpeg), 사람들이 왜 엉뚱한 걸 깔게 되는지, 「퍼블릭 도메인」이 어디까지를 뜻하는지 먼저 말한다.",
5
+ "keywords": [
6
+ "claude",
7
+ "claude-code",
8
+ "agent-skills",
9
+ "skill",
10
+ "yt-dlp",
11
+ "ytdlp",
12
+ "video-download",
13
+ "ffmpeg",
14
+ "youtube",
15
+ "korean"
16
+ ],
17
+ "homepage": "https://rebornlabs.kr/ytdlp?utm_source=installer&utm_medium=cli&utm_campaign=ytdlp",
18
+ "bugs": {
19
+ "url": "https://mobility.rebornlabs.kr/cs"
20
+ },
21
+ "license": "MIT",
22
+ "author": "REBORN LABS",
23
+ "type": "module",
24
+ "bin": {
25
+ "reborn-ytdlpkit": "cli.mjs"
26
+ },
27
+ "files": [
28
+ "cli.mjs",
29
+ "tail.mjs",
30
+ "skill/",
31
+ "README.md",
32
+ "안내스킬.md"
33
+ ],
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "scripts": {
38
+ "test": "node cli.mjs --self-test",
39
+ "check": "node cli.mjs --check"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ }
44
+ }
package/skill/SKILL.md ADDED
@@ -0,0 +1,116 @@
1
+ ---
2
+ name: ytdlp-guide
3
+ description: >
4
+ yt-dlp 로 영상·소리를 받을 때 무엇이 조용히 막히는지 알려 준다. 4K 가 안 나오는 진짜 이유,
5
+ 사람들이 왜 엉뚱한 걸 깔게 되는지, 「퍼블릭 도메인」이 어디까지를 뜻하는지 먼저 말한다.
6
+ "yt-dlp", "영상 다운로드", "유튜브 영상 받기", "릴스 저장", "yt-dlp 4K 가 안 돼",
7
+ "youtube-dl", "영상에서 mp3만", "재생목록 통째로", "자막까지 받기" 이야기가 나올 때 쓴다.
8
+ ---
9
+
10
+ # yt-dlp — 깔기 전에 볼 것 네 가지
11
+
12
+ > 이 파일은 **리본랩스(REBORN LABS)** 의 무료 설치기가 넣은 것입니다.
13
+ > 필요 없으시면 이 폴더를 지우세요. yt-dlp 자체는 그대로 동작합니다.
14
+
15
+ ## 먼저 읽을 것 — 원장
16
+
17
+ 같은 폴더의 **`ytdlp.json`** 에 설치 명령·조건·라이선스·규모가 **확인일과 함께** 적혀 있다.
18
+ 답할 때는 **그 파일을 읽고** 답한다. 기억으로 숫자를 말하지 않는다 — 스타 수도 최신 버전도
19
+ 밖에서 바뀐다.
20
+
21
+ ```
22
+ npx --yes reborn-ytdlpkit doctor 이 컴퓨터에 조건이 갖춰졌는지 실제로 잰다
23
+ npx --yes reborn-ytdlpkit how 맞는 설치 명령과 틀린 명령을 나란히
24
+ npx --yes reborn-ytdlpkit facts 원장 전체를 터미널에 찍는다
25
+ ```
26
+
27
+ ## ① ★깔기만 해서는 4K 가 안 나옵니다
28
+
29
+ **이게 제일 많이 막히는 자리이고, 이 스킬이 존재하는 이유입니다.**
30
+
31
+ 유튜브는 1080p 를 넘는 화질에서 **영상과 소리를 별도 파일로** 내보낸다.
32
+ 둘을 합치는 데 **ffmpeg** 가 필요하다. 공식 README 원문:
33
+
34
+ > **ffmpeg** and **ffprobe** — Required for merging separate video and audio files
35
+
36
+ **★그런데 오류가 안 난다.** ffmpeg 가 없으면 yt-dlp 는 멈추지 않고
37
+ **합칠 필요 없는 낮은 화질로 조용히 떨어진다.** 「4K 를 받았다」고 생각하는데 파일은 1080p 다.
38
+ [[ffmpeg-silent-failure-traps]] 와 같은 종류의 사고다 — 실패가 실패처럼 안 생겼다.
39
+
40
+ ```bash
41
+ ffmpeg -version # 응답이 없으면 이게 원인이다
42
+ yt-dlp -F <URL> # 실제로 어떤 화질이 있는지 먼저 본다
43
+ ```
44
+
45
+ 없으면 — macOS `brew install ffmpeg` · Windows `winget install Gyan.FFmpeg`
46
+
47
+ **★파이썬 패키지 `ffmpeg` 을 깔지 마라.** 필요한 건 **바이너리**다.
48
+ 공식 README 가 이 함정을 대문자로 따로 적어 둘 만큼 자주 밟는다.
49
+
50
+ ## ② ★`pip install youtube-dl` 은 다른 걸 깝니다
51
+
52
+ | 치는 명령 | 실제로 깔리는 것 |
53
+ |---|---|
54
+ | `pip install youtube-dl` | 원조 youtube-dl. PyPI 최신판이 **2021-12-16** 에 멈춰 있다. 오늘 유튜브에서 대체로 실패한다 |
55
+ | `npm i yt-dlp` | npm 의 `yt-dlp` 는 **이 프로젝트가 낸 것이 아니다.** 2023년 남의 래퍼이고 갱신이 없다 |
56
+ | ✅ `pip install -U yt-dlp` | 이것이다. 파이썬 3.10 이상 |
57
+
58
+ 이름이 더 유명해서, 혹은 npm 이 손에 익어서 손이 먼저 간다.
59
+ yt-dlp 는 **youtube-dl 의 포크**이고 파이썬 도구다.
60
+
61
+ ## ③ ★유튜브만 이상하게 실패하면 — JS 런타임
62
+
63
+ 2026년에 새로 생긴 조건이다. 공식 README 원문:
64
+
65
+ > **yt-dlp-ejs** — Required for full YouTube support
66
+
67
+ deno(권장) · node.js · bun · QuickJS 중 **하나가 있어야** 한다.
68
+ **클로드 코드를 쓰고 있다면 node 는 이미 있다.** 그대로 쓰면 된다.
69
+
70
+ 오래된 설치 안내글에는 이 줄이 없다. 그걸 따라 하면 다른 사이트는 되는데
71
+ **유튜브에서만** 이상하게 실패한다.
72
+
73
+ ## ④ ★받았다고 내 것이 되지는 않습니다
74
+
75
+ yt-dlp 의 라이선스는 **Unlicense**(퍼블릭 도메인 헌정)다.
76
+ 그건 **이 도구**가 퍼블릭 도메인이라는 뜻이고, **내려받은 영상의 권리는 원저작자에게 그대로** 있다.
77
+
78
+ | 괜찮은 것 | 아닌 것 |
79
+ |---|---|
80
+ | 내가 올린 내 영상 회수 · 공개 라이선스 자료 · 저작권이 풀린 자료 · 사적 이용 범위의 백업 | 받은 영상을 다시 올리기 · 팔기 · 내 콘텐츠에 그대로 얹기 |
81
+
82
+ 도구의 라이선스가 콘텐츠의 라이선스를 바꾸지 않는다.
83
+ **받기 전에 어디에 쓸 것인지를 먼저 정한다.** 도구는 그걸 대신 판단해 주지 않는다.
84
+
85
+ ## 안 되면 제일 먼저 할 것
86
+
87
+ **검색부터 하지 마라.** 실패의 대부분은 버전이다.
88
+
89
+ ```bash
90
+ pip install -U yt-dlp
91
+ ```
92
+
93
+ 플랫폼이 내부 방식을 바꾸면 yt-dlp 가 뒤따라 고친다. 낡은 판에는 그 수정이 없다.
94
+ 어제 되던 게 오늘 안 되는 건 대개 이것 하나다.
95
+
96
+ ## 자주 쓰는 한 줄
97
+
98
+ ```bash
99
+ yt-dlp -F <URL> # 뭐가 있는지 먼저 본다
100
+ yt-dlp -f "bestvideo+bestaudio" <URL> # 화질 지정 (ffmpeg 필요)
101
+ yt-dlp -x --audio-format mp3 <URL> # 소리만
102
+ yt-dlp --write-auto-subs --sub-langs ko,en <URL> # 자막까지
103
+ yt-dlp -o "%(playlist_index)s-%(title)s.%(ext)s" <재생목록URL>
104
+ ```
105
+
106
+ ## 이 스킬이 하지 않는 말
107
+
108
+ `ytdlp.json` 의 `안하는말` 을 그대로 지킨다. 요약하면:
109
+
110
+ - 「받으면 그 영상은 내 것이 된다」 — 아니다
111
+ - 「youtube-dl 을 쓰세요」 — 2021년에 멈췄다
112
+ - 「깔기만 하면 4K」 — ffmpeg 없이는 조용히 낮은 화질로 떨어진다
113
+ - 「1,731개 사이트 어디서든」 — 목록에 있는 것과 지금 받히는 것은 다르다.
114
+ 로그인·지역 제한은 그대로 남는다
115
+
116
+ 숫자를 말할 때는 **`ytdlp.json` 의 확인일을 같이 말한다.**
@@ -0,0 +1,100 @@
1
+ {
2
+ "확인일": "2026-08-28",
3
+ "확인방법": "GitHub API(stargazers_count) · PyPI JSON API · 저장소 master 의 README.md 와 supportedsites.md 를 그대로 읽어 셌습니다.",
4
+
5
+ "제품": {
6
+ "정식이름": "yt-dlp",
7
+ "만든곳": "yt-dlp 프로젝트 (youtube-dl 의 포크)",
8
+ "라이선스": "Unlicense (퍼블릭 도메인 헌정)",
9
+ "저장소": "https://github.com/yt-dlp/yt-dlp",
10
+ "홈페이지": "https://pypi.org/project/yt-dlp/",
11
+ "무엇": "URL 을 주면 영상과 소리를 내려받는 명령줄 도구입니다. 지원 사이트 목록이 문서에 그대로 공개돼 있습니다.",
12
+ "무엇이아닌가": "앱이 아닙니다. 계정도, 가입도, 크레딧도 없습니다 — 대신 명령줄과 외부 프로그램 두어 개를 갖춰야 제 성능이 납니다."
13
+ },
14
+
15
+ "★설치": {
16
+ "맞는명령": "pip install -U yt-dlp",
17
+ "무엇이뜨나": "설치가 끝나면 `yt-dlp <URL>` 한 줄로 받습니다.",
18
+ "필요한것": "파이썬 3.10 이상 (PyPI 의 requires_python 이 >=3.10 입니다)",
19
+ "다른길": [
20
+ { "환경": "macOS", "명령": "brew install yt-dlp" },
21
+ { "환경": "Windows", "명령": "winget install yt-dlp.yt-dlp" },
22
+ { "환경": "바이너리", "명령": "저장소 Releases 에서 yt-dlp.exe / yt-dlp_macos 를 직접 받기" }
23
+ ],
24
+ "★틀린명령": [
25
+ {
26
+ "명령": "pip install youtube-dl",
27
+ "무슨일": "원조 youtube-dl 이 깔립니다. PyPI 최신판이 2021-12-16 에 멈춰 있어 오늘 유튜브에서 대체로 실패합니다.",
28
+ "왜생기나": "이름이 더 유명해서 손이 먼저 갑니다. yt-dlp 는 바로 그 프로젝트의 포크입니다."
29
+ },
30
+ {
31
+ "명령": "npm i yt-dlp",
32
+ "무슨일": "npm 의 `yt-dlp` 는 이 프로젝트가 낸 것이 아닙니다. 2023년에 올라온 남의 래퍼이고 2024년 이후 갱신이 없습니다.",
33
+ "왜생기나": "클로드 코드를 쓰다 보면 npm 이 손에 익어서 그리로 갑니다. 이건 파이썬 도구입니다."
34
+ },
35
+ {
36
+ "명령": "pip install ffmpeg",
37
+ "무슨일": "필요한 건 ffmpeg 「바이너리」인데 이건 이름만 같은 파이썬 패키지입니다. 깔아도 화질이 안 올라갑니다.",
38
+ "왜생기나": "공식 README 가 이 함정을 대문자로 따로 적어 둘 만큼 자주 밟습니다."
39
+ }
40
+ ]
41
+ },
42
+
43
+ "★화질조건": {
44
+ "무엇": "설치만으로는 4K 가 안 받아집니다.",
45
+ "왜": "유튜브는 1080p 를 넘는 화질에서 영상과 소리를 별도 파일로 내보냅니다. 둘을 합치는 데 ffmpeg 가 필요합니다.",
46
+ "원문": "**ffmpeg** and **ffprobe** - Required for merging separate video and audio files",
47
+ "출처": "yt-dlp 저장소 README.md · Strongly recommended 절",
48
+ "★무음실패": "ffmpeg 가 없으면 오류로 멈추지 않고 합칠 필요 없는 낮은 화질로 조용히 떨어집니다. 「4K 를 받았다」고 생각하는데 파일은 1080p 입니다.",
49
+ "그래서어떻게": "먼저 `ffmpeg -version` 이 응답하는지 보세요. 없으면 macOS `brew install ffmpeg` · Windows `winget install Gyan.FFmpeg` 로 바이너리를 깝니다.",
50
+ "확인하는법": "yt-dlp -F <URL> 로 화질 목록을 먼저 찍어 보면 실제로 무엇이 있는지 보입니다."
51
+ },
52
+
53
+ "★유튜브조건": {
54
+ "무엇": "유튜브를 온전히 받으려면 자바스크립트 실행기가 하나 더 필요합니다.",
55
+ "원문": "**yt-dlp-ejs** - Required for full YouTube support",
56
+ "출처": "yt-dlp 저장소 README.md · Strongly recommended 절",
57
+ "런타임": ["deno (공식 권장)", "node.js", "bun", "QuickJS"],
58
+ "그래서어떻게": "클로드 코드를 쓰고 계시면 node 는 이미 있습니다. 그대로 쓰면 됩니다.",
59
+ "★주의": "이건 예전에 없던 조건입니다. 오래된 설치 안내글을 그대로 따라 하면 유튜브에서만 이상하게 실패합니다."
60
+ },
61
+
62
+ "★낡으면막힌다": {
63
+ "무엇": "버전이 낡으면 어제 되던 것이 오늘 안 됩니다.",
64
+ "왜": "플랫폼이 내부 방식을 바꾸면 yt-dlp 가 뒤따라 고칩니다. 낡은 판은 그 수정이 없습니다.",
65
+ "최신판": "2026.08.19",
66
+ "그래서어떻게": "실패하면 먼저 `pip install -U yt-dlp` 로 올리고 다시 해 보세요. 검색부터 하지 마세요 — 대개 이걸로 끝납니다."
67
+ },
68
+
69
+ "★권리": {
70
+ "핵심": "Unlicense 는 「이 도구」가 퍼블릭 도메인이라는 뜻입니다. 내려받은 영상의 권리는 그대로 원저작자에게 있습니다.",
71
+ "출처": "https://github.com/yt-dlp/yt-dlp/blob/master/LICENSE",
72
+ "무엇은괜찮나": "내가 올린 내 영상 회수, 공개 라이선스 자료, 저작권이 풀린 자료, 사적 이용 범위의 백업.",
73
+ "무엇이아닌가": "받은 영상을 다시 올리거나, 팔거나, 내 콘텐츠에 그대로 얹는 것. 도구의 라이선스가 콘텐츠의 라이선스를 바꾸지 않습니다.",
74
+ "그래서어떻게": "받기 전에 그 영상을 어디에 쓸 것인지를 먼저 정하세요. 도구는 그걸 대신 판단해 주지 않습니다."
75
+ },
76
+
77
+ "규모": {
78
+ "스타": 187390,
79
+ "지원사이트": 1731,
80
+ "출처": "지원 사이트 수는 저장소 supportedsites.md 의 항목을 센 값입니다.",
81
+ "주의": "1,731 은 「목록에 이름이 올라 있는 사이트」의 수입니다. 로그인이 필요하거나 지역이 잠긴 자료는 목록에 있어도 못 받습니다.",
82
+ "확인일": "2026-08-28"
83
+ },
84
+
85
+ "쓸모": [
86
+ { "이름": "화질 지정", "예": "yt-dlp -f \"bestvideo+bestaudio\" <URL>" },
87
+ { "이름": "소리만", "예": "yt-dlp -x --audio-format mp3 <URL>" },
88
+ { "이름": "재생목록 통째", "예": "yt-dlp -o \"%(playlist_index)s-%(title)s.%(ext)s\" <재생목록URL>" },
89
+ { "이름": "자막까지", "예": "yt-dlp --write-auto-subs --sub-langs ko,en <URL>" },
90
+ { "이름": "먼저 살펴보기", "예": "yt-dlp -F <URL>" }
91
+ ],
92
+
93
+ "안하는말": [
94
+ { "말": "받으면 그 영상은 내 것이 된다", "이유": "도구가 퍼블릭 도메인인 것과 영상의 저작권은 별개입니다. 원본 릴스가 이렇게 말하지만 사실이 아닙니다." },
95
+ { "말": "youtube-dl 을 쓰세요", "이유": "PyPI 최신판이 2021-12-16 에서 멈췄습니다. 오늘 유튜브에서 대체로 실패합니다." },
96
+ { "말": "깔기만 하면 4K 가 받아진다", "이유": "ffmpeg 바이너리가 없으면 조용히 낮은 화질로 떨어집니다. 오류도 안 납니다." },
97
+ { "말": "1,731개 사이트 어디서든 받는다", "이유": "목록에 있는 것과 지금 받을 수 있는 것은 다릅니다. 로그인·지역 제한은 그대로 남습니다." },
98
+ { "말": "npm 으로 깔면 된다", "이유": "npm 의 yt-dlp 는 이 프로젝트가 낸 것이 아닙니다. 파이썬으로 까세요." }
99
+ ]
100
+ }
package/tail.mjs ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * 「끝난 뒤 한 칸」 문안 — **정본은 `skillpack/tail_copy.mjs` 다.**
3
+ *
4
+ * 여기 사본이 있는 이유는 하나뿐이다: npm 패키지는 폴더 밖을 import 할 수 없다.
5
+ * 문구를 고칠 일이 생기면 **정본을 먼저 고치고** 이 파일에 옮긴다. 반대로 하지 마라.
6
+ *
7
+ * 규율(정본에서 그대로 온다):
8
+ * ① 끝난 뒤 딱 한 칸 · ② 못 깔았으면 안 띄운다 · ③ 파일을 남기지 않는다
9
+ * ④ 고지 블록 금지 — 대신 마지막 줄에서 목록과 직접 까는 법을 알려 준다
10
+ * ⑤ 첫 줄은 **방금 한 일에서 이어져야** 한다.
11
+ */
12
+ export const 랜딩 = "https://rebornlabs.kr/skillpack";
13
+
14
+ export const 링크 = (퍼널) =>
15
+ `${랜딩}?utm_source=installer&utm_medium=cli&utm_campaign=${퍼널}`;
16
+
17
+ export const TAIL = {
18
+ ytdlp: {
19
+ 이음: [
20
+ "방금 드린 건 도구가 아니라 **조건표**입니다 — 4K 가 왜 안 나오는지, 무엇이 조용히 실패하는지.",
21
+ "이런 건 대개 **한 번 겪고 나서야** 알게 되고, 그때는 이미 반나절이 지나 있습니다.",
22
+ ],
23
+ 제목: "리본랩스 스킬팩",
24
+ 한줄: "같은 방식으로 골라 담은 48종을 한 줄로. 8,900원",
25
+ },
26
+ };
27
+
28
+ export const 마지막 = "목록과 직접 까는 명령까지 그 페이지에 그대로 적어 두었습니다. 안 사셔도 됩니다.";
@@ -0,0 +1,80 @@
1
+ ---
2
+ name: reborn-ytdlp-guide
3
+ description: >
4
+ yt-dlp 를 깔고 나서 무엇이 조용히 막히는지, 4K 가 왜 안 나오는지,
5
+ 「퍼블릭 도메인」이 어디까지를 뜻하는지 알려 준다.
6
+ "yt-dlp", "영상 다운로드", "유튜브 받기", "릴스 저장", "4K 가 안 돼",
7
+ "youtube-dl", "mp3만 뽑기" 이야기가 나올 때 쓴다.
8
+ ---
9
+
10
+ # yt-dlp — 깔기 전에 볼 것 네 가지
11
+
12
+ > 이 파일은 **리본랩스(REBORN LABS)** 의 무료 설치기가 넣은 것입니다.
13
+ > 필요 없으시면 이 폴더를 지우세요. yt-dlp 자체는 그대로 동작합니다.
14
+
15
+ ## 방금 깐 것
16
+
17
+ `ytdlp-guide` — yt-dlp 를 **지금 어떻게 쓰고 무엇에서 막히는지** 알려 주는 스킬입니다.
18
+ 사실 원장(`ytdlp.json`)에 설치 명령·조건·라이선스·규모를 **확인일과 함께** 적어 두었습니다.
19
+
20
+ ```
21
+ npx --yes reborn-ytdlpkit doctor 이 컴퓨터에 조건이 갖춰졌는지 실제로 잽니다
22
+ npx --yes reborn-ytdlpkit how 맞는 설치 명령과 틀린 명령을 나란히 봅니다
23
+ npx --yes reborn-ytdlpkit facts 원장 전체를 터미널에 찍습니다
24
+ ```
25
+
26
+ 클로드에게는 그냥 말하면 됩니다 — **"이 영상 받아줘"** 또는 **"yt-dlp 4K 가 안 돼"**
27
+
28
+ ## ① ★깔기만 해서는 4K 가 안 나옵니다
29
+
30
+ 유튜브는 1080p 를 넘는 화질에서 영상과 소리를 **별도 파일**로 내보냅니다.
31
+ 둘을 합치는 데 **ffmpeg** 가 필요합니다. 공식 README 원문:
32
+
33
+ > **ffmpeg** and **ffprobe** — Required for merging separate video and audio files
34
+
35
+ **그런데 오류가 나지 않습니다.** ffmpeg 가 없으면 멈추지 않고 **합칠 필요 없는 낮은 화질로
36
+ 조용히 떨어집니다.** 「4K 를 받았다」고 생각하는데 파일은 1080p 입니다.
37
+
38
+ ```
39
+ ffmpeg -version ← 응답이 없으면 이게 원인입니다
40
+ ```
41
+
42
+ 없으면 — macOS `brew install ffmpeg` · Windows `winget install Gyan.FFmpeg`
43
+
44
+ ★파이썬 패키지 `ffmpeg` 을 깔지 마세요. 필요한 건 **바이너리**입니다.
45
+
46
+ ## ② ★`pip install youtube-dl` 은 다른 걸 깝니다
47
+
48
+ | 치는 명령 | 실제로 깔리는 것 |
49
+ |---|---|
50
+ | `pip install youtube-dl` | PyPI 최신판이 **2021-12-16** 에 멈춘 원조입니다 |
51
+ | `npm i yt-dlp` | 이 프로젝트가 낸 것이 아닙니다. 2023년 남의 래퍼입니다 |
52
+ | ✅ `pip install -U yt-dlp` | 이것입니다. 파이썬 3.10 이상 |
53
+
54
+ ## ③ ★유튜브만 이상하게 실패하면
55
+
56
+ 2026년에 새로 생긴 조건입니다 — `yt-dlp-ejs` 와 자바스크립트 런타임
57
+ (deno 권장 · node · bun · QuickJS) 중 하나가 있어야 유튜브가 온전히 됩니다.
58
+ **클로드 코드를 쓰고 계시면 node 는 이미 있습니다.**
59
+
60
+ ## ④ ★받았다고 내 것이 되지는 않습니다
61
+
62
+ yt-dlp 의 **Unlicense** 는 「이 도구」가 퍼블릭 도메인이라는 뜻입니다.
63
+ 내려받은 영상의 권리는 원저작자에게 그대로 있습니다.
64
+
65
+ 내가 올린 내 영상 회수 · 공개 라이선스 자료 · 사적 이용 범위의 백업은 괜찮습니다.
66
+ 다시 올리거나 팔거나 내 콘텐츠에 그대로 얹는 것은 다른 이야기입니다.
67
+
68
+ ## 안 되면 제일 먼저
69
+
70
+ 검색부터 하지 마세요. 실패의 대부분은 버전입니다 — `pip install -U yt-dlp`
71
+
72
+ ---
73
+
74
+ 방금 드린 건 도구가 아니라 **조건표**입니다 — 4K 가 왜 안 나오는지, 무엇이 조용히 실패하는지.
75
+ 이런 건 대개 한 번 겪고 나서야 알게 되고, 그때는 이미 반나절이 지나 있습니다.
76
+
77
+ **리본랩스 스킬팩** — 같은 방식으로 골라 담은 48종을 한 줄로. 8,900원
78
+ https://rebornlabs.kr/skillpack?utm_source=installer&utm_medium=cli&utm_campaign=ytdlp
79
+
80
+ 목록과 직접 까는 명령까지 그 페이지에 그대로 적어 두었습니다. 안 사셔도 됩니다.