From 26133bf873905be1553967be806072862d056add Mon Sep 17 00:00:00 2001 From: Sangeeth Sudheer Date: Mon, 8 Jul 2024 17:17:50 +0530 Subject: [PATCH] Add ytpsort --- .gitignore | 192 ++++++++++++++++++++++++++++++++++++++++++++++++++++ install.sh | 3 + ytpsort.mjs | 72 ++++++++++++++++++++ 3 files changed, 267 insertions(+) create mode 100644 .gitignore create mode 100755 install.sh create mode 100755 ytpsort.mjs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c0a574 --- /dev/null +++ b/.gitignore @@ -0,0 +1,192 @@ +# Created by https://www.toptal.com/developers/gitignore/api/macos,linux,node +# Edit at https://www.toptal.com/developers/gitignore?templates=macos,linux,node + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +# End of https://www.toptal.com/developers/gitignore/api/macos,linux,node +n \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..b25b11b --- /dev/null +++ b/install.sh @@ -0,0 +1,3 @@ +#/bin/sh + +ln -s "$PWD/ytpsort.mjs" ~/.local/bin/ytpsort diff --git a/ytpsort.mjs b/ytpsort.mjs new file mode 100755 index 0000000..0e3e9c9 --- /dev/null +++ b/ytpsort.mjs @@ -0,0 +1,72 @@ +#!/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()) + +const rawPlaylistItems = body.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents[0].itemSectionRenderer.contents[0].playlistVideoListRenderer.contents; + +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);