#!/usr/bin/env node // List videos in any YouTube playlist sorted in descending order of views. // // Usage: // ytpsort.mjs https://www.youtube.com/playlist?list=listid // ytpsort.mjs listid const ytBaseUrl = "https://www.youtube.com"; const args = process.argv.slice(2); if (!args[0]) { console.error("Playlist URL or ID must be passed"); process.exit(1); } let playlistId = null; try { const url = new URL(args[0]); playlistId = url.searchParams.get("list"); if (!playlistId) { console.error("Invalid playlist URL passed"); process.exit(1); } } catch { playlistId = args[0]; } const body = await fetch("https://www.youtube.com/youtubei/v1/browse?prettyPrint=false", { method: "POST", headers: { "Origin": "https://www.youtube.com" }, body: JSON.stringify({ "context": { "client": { "remoteHost": "0.0.0.0", "clientName": "WEB", "clientVersion": "2.20240702.09.00", "originalUrl": `https://www.youtube.com/playlist?list=${playlistId}`, "mainAppWebInfo": { "graftUrl": `/playlist?list=${playlistId}`, } } }, "browseId": `VL${playlistId}` }) }).then(res => res.json()) let rawPlaylistItems = null; try { rawPlaylistItems = body.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents[0].itemSectionRenderer.contents[0].playlistVideoListRenderer.contents; } catch { console.error("Error accessing JSON response fields: YT response possibly changed?"); process.exit(1); } const viewsRegex = /by .* ((?:\d+,?)+) views .*ago/; function parseViews(text) { return Number.parseInt(text.match(viewsRegex)[1].replaceAll(",", "")); } const videos = rawPlaylistItems.map(root => { const item = root.playlistVideoRenderer; return { title: item.title.runs[0].text.slice(0, 70), url: `${ytBaseUrl}/watch?v=${item.videoId}`, views: parseViews(item.title.accessibility.accessibilityData.label), } }); videos.sort((a, b) => b.views - a.views); console.table(videos);