Scenario
Should your brand invest in Shorts or long-form? This job pulls your channel’s recent Shorts (under 60 seconds) and long-form videos separately viavideos.list, runs Video Analysis on a sample from each format, and compares behavioral scores head-to-head. The result is a format strategy recommendation backed by both YouTube analytics and Mavera’s creative intelligence — showing not just which format gets more views, but which format produces better creative quality for your brand.
Architecture
Code
import os, requests, time, re
YT = os.environ["YOUTUBE_API_KEY"]
MV = os.environ["MAVERA_API_KEY"]
YT_BASE = "https://www.googleapis.com/youtube/v3"
MV_BASE = "https://app.mavera.io/api/v1"
MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
CHANNEL_ID = "UC_YOUR_CHANNEL_ID"
# 1. Get channel uploads playlist
channel = requests.get(f"{YT_BASE}/channels", params={
"key": YT, "id": CHANNEL_ID,
"part": "contentDetails",
}).json()
uploads_playlist = channel["items"][0]["contentDetails"]["relatedPlaylists"]["uploads"]
# 2. Get recent uploads (1 quota unit per page)
playlist_items = requests.get(f"{YT_BASE}/playlistItems", params={
"key": YT, "playlistId": uploads_playlist,
"part": "snippet", "maxResults": 50,
}).json()
video_ids = [item["snippet"]["resourceId"]["videoId"]
for item in playlist_items.get("items", [])]
# 3. Get full details to classify Shorts vs long-form (1 quota unit)
details = requests.get(f"{YT_BASE}/videos", params={
"key": YT, "id": ",".join(video_ids),
"part": "snippet,contentDetails,statistics",
}).json()
def parse_duration(iso):
match = re.match(r"PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?", iso)
if not match:
return 0
h, m, s = (int(g) if g else 0 for g in match.groups())
return h * 3600 + m * 60 + s
shorts = []
long_form = []
for video in details.get("items", []):
duration = parse_duration(video["contentDetails"]["duration"])
stats = video.get("statistics", {})
entry = {
"id": video["id"],
"title": video["snippet"]["title"],
"duration": duration,
"views": int(stats.get("viewCount", 0)),
"likes": int(stats.get("likeCount", 0)),
"comments": int(stats.get("commentCount", 0)),
}
if duration <= 60:
shorts.append(entry)
else:
long_form.append(entry)
print(f"Found {len(shorts)} Shorts, {len(long_form)} long-form videos")
# 4. Analyze top 3 of each format
def analyze_videos(video_list, label):
scored = []
for video in sorted(video_list, key=lambda v: -v["views"])[:3]:
upload = requests.post(f"{MV_BASE}/assets", headers=MV_H, json={
"url": f"https://www.youtube.com/watch?v={video['id']}",
"name": f"[{label}] {video['title'][:40]}", "type": "video",
}).json()
analysis = requests.post(f"{MV_BASE}/video-analysis", headers=MV_H, json={
"asset_id": upload["id"],
"analysis_types": ["hook_score", "emotional_arc", "pacing", "cognitive_load", "visual_complexity"],
}).json()
for _ in range(30):
time.sleep(3)
status = requests.get(
f"{MV_BASE}/video-analysis/{analysis['id']}", headers=MV_H
).json()
if status.get("status") == "completed":
break
r = status.get("results", {})
scored.append({
**video,
"hook": r.get("hook_score", {}).get("score", 0),
"emotion": r.get("emotional_arc", {}).get("intensity_avg", 0),
"pacing": r.get("pacing", {}).get("score", 0),
"cog_load": r.get("cognitive_load", {}).get("average", 0),
})
time.sleep(1)
return scored
shorts_scored = analyze_videos(shorts, "Short")
long_scored = analyze_videos(long_form, "Long")
# 5. Comparison report
def avg_metric(lst, key):
vals = [v[key] for v in lst]
return sum(vals) / len(vals) if vals else 0
print("\nYOUTUBE SHORTS vs. LONG-FORM COMPARISON")
print("=" * 65)
print(f"{'Metric':<25} {'Shorts (avg)':<18} {'Long-Form (avg)':<18} {'Winner'}")
print("-" * 65)
metrics = [
("Views", "views", False),
("Hook Score (/100)", "hook", False),
("Emotional Intensity (/10)", "emotion", False),
("Pacing (/10)", "pacing", False),
("Cognitive Load (/10)", "cog_load", True),
]
for label, key, lower_is_better in metrics:
s_avg = avg_metric(shorts_scored, key)
l_avg = avg_metric(long_scored, key)
winner = "Shorts" if (s_avg < l_avg if lower_is_better else s_avg > l_avg) else "Long-Form"
fmt = ",.0f" if key == "views" else ".1f"
print(f" {label:<25} {s_avg:{fmt}:<18} {l_avg:{fmt}:<18} {winner}")
engagement_short = avg_metric(shorts_scored, "likes") / max(avg_metric(shorts_scored, "views"), 1) * 100
engagement_long = avg_metric(long_scored, "likes") / max(avg_metric(long_scored, "views"), 1) * 100
print(f" {'Engagement Rate':<25} {engagement_short:.2f}%{'':<13} {engagement_long:.2f}%{'':<13} {'Shorts' if engagement_short > engagement_long else 'Long-Form'}")
const YT = process.env.YOUTUBE_API_KEY;
const MV = process.env.MAVERA_API_KEY;
const YT_BASE = "https://www.googleapis.com/youtube/v3";
const MV_BASE = "https://app.mavera.io/api/v1";
const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
const CHANNEL_ID = "UC_YOUR_CHANNEL_ID";
// 1. Uploads playlist
const channel = await fetch(
`${YT_BASE}/channels?key=${YT}&id=${CHANNEL_ID}&part=contentDetails`
).then(r => r.json());
const uploadsPlaylist = channel.items[0].contentDetails.relatedPlaylists.uploads;
// 2. Recent uploads
const playlistItems = await fetch(
`${YT_BASE}/playlistItems?key=${YT}&playlistId=${uploadsPlaylist}&part=snippet&maxResults=50`
).then(r => r.json());
const videoIds = (playlistItems.items || []).map(i => i.snippet.resourceId.videoId);
// 3. Classify
const details = await fetch(
`${YT_BASE}/videos?key=${YT}&id=${videoIds.join(",")}&part=snippet,contentDetails,statistics`
).then(r => r.json());
function parseDuration(iso) {
const m = iso.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/);
if (!m) return 0;
return (parseInt(m[1] || "0") * 3600) + (parseInt(m[2] || "0") * 60) + parseInt(m[3] || "0");
}
const shorts = [], longForm = [];
for (const v of details.items || []) {
const dur = parseDuration(v.contentDetails.duration);
const entry = {
id: v.id, title: v.snippet.title, duration: dur,
views: parseInt(v.statistics?.viewCount || "0", 10),
likes: parseInt(v.statistics?.likeCount || "0", 10),
comments: parseInt(v.statistics?.commentCount || "0", 10),
};
(dur <= 60 ? shorts : longForm).push(entry);
}
console.log(`Found ${shorts.length} Shorts, ${longForm.length} long-form`);
// 4. Analyze top 3 each
async function analyzeVideos(list, label) {
const scored = [];
for (const video of list.sort((a, b) => b.views - a.views).slice(0, 3)) {
const upload = await fetch(`${MV_BASE}/assets`, {
method: "POST", headers: MV_H,
body: JSON.stringify({ url: `https://www.youtube.com/watch?v=${video.id}`, name: `[${label}] ${video.title.slice(0, 40)}`, type: "video" }),
}).then(r => r.json());
const analysis = await fetch(`${MV_BASE}/video-analysis`, {
method: "POST", headers: MV_H,
body: JSON.stringify({ asset_id: upload.id, analysis_types: ["hook_score", "emotional_arc", "pacing", "cognitive_load", "visual_complexity"] }),
}).then(r => r.json());
let status;
for (let i = 0; i < 30; i++) {
await new Promise(r => setTimeout(r, 3000));
status = await fetch(`${MV_BASE}/video-analysis/${analysis.id}`, { headers: MV_H }).then(r => r.json());
if (status.status === "completed") break;
}
const res = status.results || {};
scored.push({ ...video,
hook: res.hook_score?.score || 0, emotion: res.emotional_arc?.intensity_avg || 0,
pacing: res.pacing?.score || 0, cogLoad: res.cognitive_load?.average || 0,
});
await new Promise(r => setTimeout(r, 1000));
}
return scored;
}
const shortsScored = await analyzeVideos(shorts, "Short");
const longScored = await analyzeVideos(longForm, "Long");
// 5. Report
const avg = (arr, key) => arr.length ? arr.reduce((s, v) => s + v[key], 0) / arr.length : 0;
console.log("\nYOUTUBE SHORTS vs. LONG-FORM COMPARISON");
console.log("=".repeat(65));
console.log("Metric Shorts (avg) Long-Form (avg) Winner");
console.log("-".repeat(65));
for (const [label, key, lower] of [
["Views", "views", false], ["Hook Score (/100)", "hook", false],
["Emotional Intensity (/10)", "emotion", false], ["Pacing (/10)", "pacing", false],
["Cognitive Load (/10)", "cogLoad", true],
]) {
const sAvg = avg(shortsScored, key);
const lAvg = avg(longScored, key);
const winner = (lower ? sAvg < lAvg : sAvg > lAvg) ? "Shorts" : "Long-Form";
const fmt = key === "views" ? v => v.toLocaleString() : v => v.toFixed(1);
console.log(` ${label.padEnd(25)} ${fmt(sAvg).padEnd(18)} ${fmt(lAvg).padEnd(18)} ${winner}`);
}
Example Output
Found 18 Shorts, 32 long-form videos
YOUTUBE SHORTS vs. LONG-FORM COMPARISON
=================================================================
Metric Shorts (avg) Long-Form (avg) Winner
-----------------------------------------------------------------
Views 124,000 45,200 Shorts
Hook Score (/100) 82 61 Shorts
Emotional Intensity (/10) 7.2 6.8 Shorts
Pacing (/10) 9.1 6.4 Shorts
Cognitive Load (/10) 3.2 5.8 Shorts
Engagement Rate 4.80% 2.10% Shorts
INSIGHT: Shorts dominate on reach, hook, and pacing — but engagement
rate (likes/views) is 2.3x higher. Long-form drives deeper engagement
per viewer but reaches fewer people. Recommendation: Use Shorts for
top-of-funnel awareness and long-form for mid-funnel education.
Repurpose long-form highlights as Shorts for maximum coverage.
Error Handling
Shorts classification heuristic
Shorts classification heuristic
YouTube’s API doesn’t have a dedicated Shorts flag. Videos under 60 seconds are classified as Shorts. This may include non-Shorts short clips — filter by aspect ratio (9:16) if precision matters.
Channel uploads pagination
Channel uploads pagination
The
playlistItems endpoint returns 50 items per page. For channels with 100+ videos, paginate with nextPageToken. Each page costs 1 quota unit.Engagement rate calculation
Engagement rate calculation
Engagement rate = likes / views. YouTube hides dislike counts (since 2021), so this metric is positivity-biased. Comment count is an alternative engagement signal.