ffmpeg-skill 1.13.0 → 1.14.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/README.md +68 -29
- package/SKILL.md +44 -39
- package/bin/install.js +1 -1
- package/docs/contract.md +51 -9
- package/package.json +4 -2
- package/references/gotchas.md +15 -0
- package/references/scripts.md +98 -9
- package/scripts/_common.py +6 -3
- package/scripts/_contract.py +8 -5
- package/scripts/_platforms.py +251 -0
- package/scripts/caption.py +17 -1
- package/scripts/check.py +13 -16
- package/scripts/export.py +87 -11
- package/scripts/fit.py +21 -2
- package/scripts/graphics.py +93 -8
- package/scripts/look.py +35 -0
- package/scripts/overlay.py +37 -13
- package/scripts/render.py +313 -29
- package/scripts/report.py +73 -1
- package/templates/facebook.json +47 -0
- package/templates/linkedin.json +47 -0
- package/templates/podcast.json +22 -0
- package/templates/reels.json +47 -0
- package/templates/shorts.json +47 -0
- package/templates/tiktok.json +47 -0
- package/templates/x.json +47 -0
- package/templates/youtube-shorts.json +47 -0
- package/templates/youtube.json +47 -0
package/scripts/report.py
CHANGED
|
@@ -7,6 +7,7 @@ Examples:
|
|
|
7
7
|
python3 report.py --before raw.mov --after final.mp4 -o report.html
|
|
8
8
|
python3 report.py --after final.mp4 --platform reels --title "Episode 12 — Reels cut" -o report.html
|
|
9
9
|
python3 report.py --before raw.mov --after final.mp4 --commands commands.txt --notes notes.md
|
|
10
|
+
python3 report.py --pack talk_pack.md -o pack.html # the social pack table as HTML
|
|
10
11
|
"""
|
|
11
12
|
import argparse
|
|
12
13
|
import base64
|
|
@@ -79,9 +80,76 @@ def media_rows(meta: Dict[str, Any], ld: Dict[str, Any]) -> List[List[str]]:
|
|
|
79
80
|
return rows
|
|
80
81
|
|
|
81
82
|
|
|
83
|
+
PACK_CSS = """
|
|
84
|
+
:root{--bg:#F4F6F8;--paper:#fff;--ink:#161B21;--ink2:#4B5661;--line:#D8DEE4;--ok:#2C8A5B;--bad:#B4362F}
|
|
85
|
+
@media (prefers-color-scheme:dark){:root{--bg:#111518;--paper:#191E23;--ink:#E8ECEF;--ink2:#AEB6BE;--line:#2A3138;--ok:#5CC38C;--bad:#F07A73}}
|
|
86
|
+
body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.6 system-ui,-apple-system,"Segoe UI",Roboto,"Noto Sans JP",sans-serif}
|
|
87
|
+
.wrap{max-width:900px;margin:0 auto;padding:32px 20px 60px}
|
|
88
|
+
h1{font-size:24px;margin:0 0 16px}
|
|
89
|
+
table{border-collapse:collapse;width:100%;font-size:14px;background:var(--paper);border:1px solid var(--line);border-radius:6px;overflow:hidden}
|
|
90
|
+
th,td{text-align:left;padding:7px 10px;border-bottom:1px solid var(--line)}th{color:var(--ink2);font-weight:500}
|
|
91
|
+
td.pass{color:var(--ok);font-weight:600}td.bad{color:var(--bad);font-weight:600}
|
|
92
|
+
p.note{color:var(--ink2);font-size:13px}
|
|
93
|
+
.foot{color:var(--ink2);font-size:12px;margin-top:30px;border-top:1px solid var(--line);padding-top:10px}
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def pack_report(args) -> int:
|
|
98
|
+
"""render.py --template all writes <stem>_pack.md: one row per destination. This renders the
|
|
99
|
+
same rows as HTML, so a pack can be handed over the way a single delivery report is -- no
|
|
100
|
+
re-measurement, because the pack table is what the renders actually produced."""
|
|
101
|
+
text = read_text_or_die(args.pack, "--pack")
|
|
102
|
+
rows: List[List[str]] = []
|
|
103
|
+
header: List[str] = []
|
|
104
|
+
for line in text.splitlines():
|
|
105
|
+
line = line.strip()
|
|
106
|
+
if not line.startswith("|"):
|
|
107
|
+
continue
|
|
108
|
+
cells = [c.strip() for c in line.strip("|").split("|")]
|
|
109
|
+
if all(set(c) <= set("-: ") for c in cells):
|
|
110
|
+
continue
|
|
111
|
+
if not header:
|
|
112
|
+
header = cells
|
|
113
|
+
else:
|
|
114
|
+
rows.append(cells)
|
|
115
|
+
if not rows:
|
|
116
|
+
die(f"{args.pack}: no pack table found (expected the Markdown table render.py --template all writes)")
|
|
117
|
+
title = args.title or f"Social pack — {Path(args.pack).stem.replace('_pack', '')}"
|
|
118
|
+
output = args.output or str(Path(args.pack).with_suffix(".html"))
|
|
119
|
+
head = "".join(f"<th>{html.escape(h)}</th>" for h in header)
|
|
120
|
+
body = ""
|
|
121
|
+
for r in rows:
|
|
122
|
+
cells = ""
|
|
123
|
+
for i, c in enumerate(r):
|
|
124
|
+
cls = ""
|
|
125
|
+
if header[i:i + 1] == ["check"]:
|
|
126
|
+
cls = " class='pass'" if c.lower() == "pass" else " class='bad'"
|
|
127
|
+
cells += f"<td{cls}>{html.escape(c)}</td>"
|
|
128
|
+
body += f"<tr>{cells}</tr>"
|
|
129
|
+
doc = ("<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>"
|
|
130
|
+
f"<title>{html.escape(title)}</title><style>{PACK_CSS}</style></head><body><div class='wrap'>"
|
|
131
|
+
f"<h1>{html.escape(title)}</h1><table><tr>{head}</tr>{body}</table>"
|
|
132
|
+
f"<p class='note'>{len(rows)} destination(s) from one edit.</p>"
|
|
133
|
+
"<p class='foot'>Generated by ffmpeg-skill · local FFmpeg, no cloud.</p></div></body></html>")
|
|
134
|
+
if STATE.dry_run:
|
|
135
|
+
info(f"wrote {output}")
|
|
136
|
+
else:
|
|
137
|
+
try:
|
|
138
|
+
Path(output).write_text(doc, encoding="utf-8")
|
|
139
|
+
except OSError as e:
|
|
140
|
+
die(f"cannot write {output}: {e}", kind="output")
|
|
141
|
+
info(f"wrote {output} ({os.path.getsize(output) / 1024:.0f} KB)")
|
|
142
|
+
emit(None, report=output, pack=[dict(zip(header, r)) for r in rows], check=None,
|
|
143
|
+
verification=([{"step": "exists", "ok": True}] if not STATE.dry_run else []))
|
|
144
|
+
if not args.json:
|
|
145
|
+
print(output)
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
|
|
82
149
|
def main() -> int:
|
|
83
150
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
84
|
-
ap.add_argument("--after",
|
|
151
|
+
ap.add_argument("--after", help="the deliverable (required unless --pack is given)")
|
|
152
|
+
ap.add_argument("--pack", metavar="FILE", help="a social pack table written by render.py --template all (<stem>_pack.md): render it as HTML")
|
|
85
153
|
ap.add_argument("--before", help="the source (optional)")
|
|
86
154
|
ap.add_argument("-o", "--output", help="report path (default: <after>_report.html)")
|
|
87
155
|
ap.add_argument("--title", help="report title")
|
|
@@ -93,6 +161,10 @@ def main() -> int:
|
|
|
93
161
|
args = ap.parse_args()
|
|
94
162
|
apply_common(args)
|
|
95
163
|
|
|
164
|
+
if args.pack:
|
|
165
|
+
return pack_report(args)
|
|
166
|
+
if not args.after:
|
|
167
|
+
die("--after is required (or --pack FILE for a social pack table)")
|
|
96
168
|
after = probe(args.after)
|
|
97
169
|
before = probe(args.before) if args.before else None
|
|
98
170
|
ld_after = loudness(args.after) if after.get("audio") else {}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"template": "facebook",
|
|
3
|
+
"output": "$OUTPUT",
|
|
4
|
+
"clips": [
|
|
5
|
+
{
|
|
6
|
+
"src": "$INPUT"
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"frame": {
|
|
10
|
+
"aspect": "16:9",
|
|
11
|
+
"fit": "pad"
|
|
12
|
+
},
|
|
13
|
+
"brand": "$BRAND",
|
|
14
|
+
"captions": {
|
|
15
|
+
"text": "$CUES",
|
|
16
|
+
"animate": "none",
|
|
17
|
+
"karaoke": false,
|
|
18
|
+
"size": 20,
|
|
19
|
+
"position": "bottom",
|
|
20
|
+
"margin": 14
|
|
21
|
+
},
|
|
22
|
+
"graphics": [
|
|
23
|
+
{
|
|
24
|
+
"template": "lower-third",
|
|
25
|
+
"name": "$TITLE",
|
|
26
|
+
"start": 1,
|
|
27
|
+
"end": 6
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"overlays": [
|
|
31
|
+
{
|
|
32
|
+
"image": "$LOGO",
|
|
33
|
+
"position": "top-left"
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"loudness": {
|
|
37
|
+
"lufs": -14,
|
|
38
|
+
"tp": -1.0
|
|
39
|
+
},
|
|
40
|
+
"export": {
|
|
41
|
+
"preset": "facebook",
|
|
42
|
+
"normalize": true
|
|
43
|
+
},
|
|
44
|
+
"check": {
|
|
45
|
+
"platform": "facebook"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"template": "linkedin",
|
|
3
|
+
"output": "$OUTPUT",
|
|
4
|
+
"clips": [
|
|
5
|
+
{
|
|
6
|
+
"src": "$INPUT"
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"frame": {
|
|
10
|
+
"aspect": "1:1",
|
|
11
|
+
"fit": "crop"
|
|
12
|
+
},
|
|
13
|
+
"brand": "$BRAND",
|
|
14
|
+
"captions": {
|
|
15
|
+
"text": "$CUES",
|
|
16
|
+
"animate": "none",
|
|
17
|
+
"karaoke": false,
|
|
18
|
+
"size": 20,
|
|
19
|
+
"position": "bottom",
|
|
20
|
+
"margin": 14
|
|
21
|
+
},
|
|
22
|
+
"graphics": [
|
|
23
|
+
{
|
|
24
|
+
"template": "lower-third",
|
|
25
|
+
"name": "$TITLE",
|
|
26
|
+
"start": 1,
|
|
27
|
+
"end": 6
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"overlays": [
|
|
31
|
+
{
|
|
32
|
+
"image": "$LOGO",
|
|
33
|
+
"position": "top-left"
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"loudness": {
|
|
37
|
+
"lufs": -14,
|
|
38
|
+
"tp": -1.0
|
|
39
|
+
},
|
|
40
|
+
"export": {
|
|
41
|
+
"preset": "linkedin",
|
|
42
|
+
"normalize": true
|
|
43
|
+
},
|
|
44
|
+
"check": {
|
|
45
|
+
"platform": "linkedin"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"template": "podcast",
|
|
3
|
+
"output": "$OUTPUT",
|
|
4
|
+
"clips": [
|
|
5
|
+
{
|
|
6
|
+
"src": "$INPUT"
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"silence": {
|
|
10
|
+
"threshold": -40,
|
|
11
|
+
"min_silence": 1.0,
|
|
12
|
+
"margin": 0.15
|
|
13
|
+
},
|
|
14
|
+
"loudness": {
|
|
15
|
+
"lufs": -16,
|
|
16
|
+
"tp": -1
|
|
17
|
+
},
|
|
18
|
+
"chapters": "$CHAPTERS",
|
|
19
|
+
"check": {
|
|
20
|
+
"platform": "podcast"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"template": "reels",
|
|
3
|
+
"output": "$OUTPUT",
|
|
4
|
+
"clips": [
|
|
5
|
+
{
|
|
6
|
+
"src": "$INPUT"
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"frame": {
|
|
10
|
+
"aspect": "9:16",
|
|
11
|
+
"fit": "crop"
|
|
12
|
+
},
|
|
13
|
+
"brand": "$BRAND",
|
|
14
|
+
"captions": {
|
|
15
|
+
"text": "$CUES",
|
|
16
|
+
"animate": "pop",
|
|
17
|
+
"karaoke": true,
|
|
18
|
+
"size": 24,
|
|
19
|
+
"position": "bottom",
|
|
20
|
+
"margin": 58
|
|
21
|
+
},
|
|
22
|
+
"graphics": [
|
|
23
|
+
{
|
|
24
|
+
"template": "hook",
|
|
25
|
+
"title": "$TITLE",
|
|
26
|
+
"start": 0,
|
|
27
|
+
"end": 3
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"overlays": [
|
|
31
|
+
{
|
|
32
|
+
"image": "$LOGO",
|
|
33
|
+
"position": "top-left"
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"loudness": {
|
|
37
|
+
"lufs": -14,
|
|
38
|
+
"tp": -1.0
|
|
39
|
+
},
|
|
40
|
+
"export": {
|
|
41
|
+
"preset": "reels",
|
|
42
|
+
"normalize": true
|
|
43
|
+
},
|
|
44
|
+
"check": {
|
|
45
|
+
"platform": "reels"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"template": "shorts",
|
|
3
|
+
"output": "$OUTPUT",
|
|
4
|
+
"clips": [
|
|
5
|
+
{
|
|
6
|
+
"src": "$INPUT"
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"frame": {
|
|
10
|
+
"aspect": "9:16",
|
|
11
|
+
"fit": "crop"
|
|
12
|
+
},
|
|
13
|
+
"brand": "$BRAND",
|
|
14
|
+
"captions": {
|
|
15
|
+
"text": "$CUES",
|
|
16
|
+
"animate": "pop",
|
|
17
|
+
"karaoke": true,
|
|
18
|
+
"size": 24,
|
|
19
|
+
"position": "bottom",
|
|
20
|
+
"margin": 52
|
|
21
|
+
},
|
|
22
|
+
"graphics": [
|
|
23
|
+
{
|
|
24
|
+
"template": "hook",
|
|
25
|
+
"title": "$TITLE",
|
|
26
|
+
"start": 0,
|
|
27
|
+
"end": 3
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"overlays": [
|
|
31
|
+
{
|
|
32
|
+
"image": "$LOGO",
|
|
33
|
+
"position": "top-left"
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"loudness": {
|
|
37
|
+
"lufs": -14,
|
|
38
|
+
"tp": -1.0
|
|
39
|
+
},
|
|
40
|
+
"export": {
|
|
41
|
+
"preset": "shorts",
|
|
42
|
+
"normalize": true
|
|
43
|
+
},
|
|
44
|
+
"check": {
|
|
45
|
+
"platform": "shorts"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"template": "tiktok",
|
|
3
|
+
"output": "$OUTPUT",
|
|
4
|
+
"clips": [
|
|
5
|
+
{
|
|
6
|
+
"src": "$INPUT"
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"frame": {
|
|
10
|
+
"aspect": "9:16",
|
|
11
|
+
"fit": "crop"
|
|
12
|
+
},
|
|
13
|
+
"brand": "$BRAND",
|
|
14
|
+
"captions": {
|
|
15
|
+
"text": "$CUES",
|
|
16
|
+
"animate": "pop",
|
|
17
|
+
"karaoke": true,
|
|
18
|
+
"size": 24,
|
|
19
|
+
"position": "bottom",
|
|
20
|
+
"margin": 63
|
|
21
|
+
},
|
|
22
|
+
"graphics": [
|
|
23
|
+
{
|
|
24
|
+
"template": "hook",
|
|
25
|
+
"title": "$TITLE",
|
|
26
|
+
"start": 0,
|
|
27
|
+
"end": 3
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"overlays": [
|
|
31
|
+
{
|
|
32
|
+
"image": "$LOGO",
|
|
33
|
+
"position": "top-left"
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"loudness": {
|
|
37
|
+
"lufs": -14,
|
|
38
|
+
"tp": -1.0
|
|
39
|
+
},
|
|
40
|
+
"export": {
|
|
41
|
+
"preset": "tiktok",
|
|
42
|
+
"normalize": true
|
|
43
|
+
},
|
|
44
|
+
"check": {
|
|
45
|
+
"platform": "tiktok"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/templates/x.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"template": "x",
|
|
3
|
+
"output": "$OUTPUT",
|
|
4
|
+
"clips": [
|
|
5
|
+
{
|
|
6
|
+
"src": "$INPUT"
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"frame": {
|
|
10
|
+
"aspect": "16:9",
|
|
11
|
+
"fit": "pad"
|
|
12
|
+
},
|
|
13
|
+
"brand": "$BRAND",
|
|
14
|
+
"captions": {
|
|
15
|
+
"text": "$CUES",
|
|
16
|
+
"animate": "none",
|
|
17
|
+
"karaoke": false,
|
|
18
|
+
"size": 20,
|
|
19
|
+
"position": "bottom",
|
|
20
|
+
"margin": 14
|
|
21
|
+
},
|
|
22
|
+
"graphics": [
|
|
23
|
+
{
|
|
24
|
+
"template": "lower-third",
|
|
25
|
+
"name": "$TITLE",
|
|
26
|
+
"start": 1,
|
|
27
|
+
"end": 6
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"overlays": [
|
|
31
|
+
{
|
|
32
|
+
"image": "$LOGO",
|
|
33
|
+
"position": "top-left"
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"loudness": {
|
|
37
|
+
"lufs": -14,
|
|
38
|
+
"tp": -1.0
|
|
39
|
+
},
|
|
40
|
+
"export": {
|
|
41
|
+
"preset": "x",
|
|
42
|
+
"normalize": true
|
|
43
|
+
},
|
|
44
|
+
"check": {
|
|
45
|
+
"platform": "x"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"template": "youtube-shorts",
|
|
3
|
+
"output": "$OUTPUT",
|
|
4
|
+
"clips": [
|
|
5
|
+
{
|
|
6
|
+
"src": "$INPUT"
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"frame": {
|
|
10
|
+
"aspect": "9:16",
|
|
11
|
+
"fit": "crop"
|
|
12
|
+
},
|
|
13
|
+
"brand": "$BRAND",
|
|
14
|
+
"captions": {
|
|
15
|
+
"text": "$CUES",
|
|
16
|
+
"animate": "pop",
|
|
17
|
+
"karaoke": true,
|
|
18
|
+
"size": 24,
|
|
19
|
+
"position": "bottom",
|
|
20
|
+
"margin": 52
|
|
21
|
+
},
|
|
22
|
+
"graphics": [
|
|
23
|
+
{
|
|
24
|
+
"template": "hook",
|
|
25
|
+
"title": "$TITLE",
|
|
26
|
+
"start": 0,
|
|
27
|
+
"end": 3
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"overlays": [
|
|
31
|
+
{
|
|
32
|
+
"image": "$LOGO",
|
|
33
|
+
"position": "top-left"
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"loudness": {
|
|
37
|
+
"lufs": -14,
|
|
38
|
+
"tp": -1.0
|
|
39
|
+
},
|
|
40
|
+
"export": {
|
|
41
|
+
"preset": "shorts",
|
|
42
|
+
"normalize": true
|
|
43
|
+
},
|
|
44
|
+
"check": {
|
|
45
|
+
"platform": "shorts"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"template": "youtube",
|
|
3
|
+
"output": "$OUTPUT",
|
|
4
|
+
"clips": [
|
|
5
|
+
{
|
|
6
|
+
"src": "$INPUT"
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"frame": {
|
|
10
|
+
"aspect": "16:9",
|
|
11
|
+
"fit": "pad"
|
|
12
|
+
},
|
|
13
|
+
"brand": "$BRAND",
|
|
14
|
+
"captions": {
|
|
15
|
+
"text": "$CUES",
|
|
16
|
+
"animate": "none",
|
|
17
|
+
"karaoke": false,
|
|
18
|
+
"size": 20,
|
|
19
|
+
"position": "bottom",
|
|
20
|
+
"margin": 14
|
|
21
|
+
},
|
|
22
|
+
"graphics": [
|
|
23
|
+
{
|
|
24
|
+
"template": "lower-third",
|
|
25
|
+
"name": "$TITLE",
|
|
26
|
+
"start": 1,
|
|
27
|
+
"end": 6
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"overlays": [
|
|
31
|
+
{
|
|
32
|
+
"image": "$LOGO",
|
|
33
|
+
"position": "top-left"
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"loudness": {
|
|
37
|
+
"lufs": -14,
|
|
38
|
+
"tp": -1.0
|
|
39
|
+
},
|
|
40
|
+
"export": {
|
|
41
|
+
"preset": "youtube",
|
|
42
|
+
"normalize": true
|
|
43
|
+
},
|
|
44
|
+
"check": {
|
|
45
|
+
"platform": "youtube"
|
|
46
|
+
}
|
|
47
|
+
}
|