Creating a Command Line Tool steps
The new output uses recent C# features that simplify the code you need to write for a program. For .NET 5 and earlier versions, the console app template generates the following code:// See https://aka.ms/new-console-template for more informationConsole.WriteLine("Hello, World!");
using System;namespace MyApp // Note: actual namespace depends on the project name.{internal class Program{static void Main(string[] args){Console.WriteLine("Hello World!");}}}
These two forms represent the same program. Both are valid with C# 10.0. When you use the newer version, you only need to write the body of the Main method. The compiler synthesizes a Program class with a Main method and places all your top level statements in that Main method. You don't need to include the other program elements, the compiler generates them for you. You can learn more about the code the compiler generates when you use top level statements in the article on top level statements in the C# Guide's fundamentals section.
You have two options to work with tutorials that haven't been updated to use .NET 6+ templates:
- Use the new program style, adding new top-level statements as you add features.
- Convert the new program style to the older style, with a Program class and a Main method.
Use the new program style
Implicit using directives
using System;using System.IO;using System.Collections.Generic;using System.Linq;using System.Net.Http;using System.Threading;using System.Threading.Tasks;
Disable implicit using directives
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup>...<ImplicitUsings>disable</ImplicitUsings></PropertyGroup></Project>
Global using directives
<ItemGroup><Using Remove="System.Net.Http" /></ItemGroup>
Command Line Tool Steps Continued
using Microsoft.Extensions.CommandLineUtils;
public class CreateCommand : CommandLineApplication
Create Constructor:
this.Name = "Create";this.Description = "Create Name";
Parameters of CLI
parameters will be created using the CommandOption like below:
CommandOption apiKey = this.Option("--api-key <apiKey>", "Api key", CommandOptionType.SingleValue);
Now the overall Constructor code looks like below:
public CreateCommand(){try{this.Name = "Create";this.Description = "Create Name";CommandOption apiKey = this.Option("--api-key <apiKey>", "Api key", CommandOptionType.SingleValue);CommandOption outputfilepath = this.Option("--output-file-path <outputfilepath>", "outputfilepath", CommandOptionType.SingleValue);this.OnExecute(async () =>{string apiKeysStr = apiKey?.Value();string outputfilepathStr = outputfilepath?.Value();File.WriteAllText(outputfilepathStr, "test output");return 0;});}catch (Exception ex){throw ex;}}
Call the CreateCommand in Program.cs
var app = new CommandLineApplication(){Name = "YoutubeCLI",FullName = "YoutubeCLI",Description = "YoutubeCLI"};
app.Commands.Add(new CreateCommand());
app.OnExecute(() => {ColoredConsole.Error.WriteLine("No commands specified, please specify a command");app.ShowHelp();return 1;});
Below code will execute the command when called with arguments:
return app.Execute(args);
Now overall Program.cs code looks like this:
// See https://aka.ms/new-console-template for more information
using Colors.Net;
using Microsoft.Extensions.CommandLineUtils;
Console.WriteLine("Hello, World!");
try
{
var app = new CommandLineApplication()
{
Name = "YoutubeCLI",
FullName = "YoutubeCLI",
Description = "YoutubeCLI"
};
app.Commands.Add(new CreateCommand());
app.OnExecute(() => {
ColoredConsole.Error.WriteLine("No commands specified, please specify a command");
app.ShowHelp();
return 1;
});
return app.Execute(args);
}
catch (Exception e)
{
ColoredConsole.Error.WriteLine(e.Message);
return 1;
}
Building YouTube Channel Videos fetcher CLI tool:
We will customize the CLI tool we built above to create a realtime YouTube Channel Videos Fetcher CLI tool by using YouTube apis:
Our Final CreateCommand class will look like this:
using Colors.Net;
using Microsoft.Extensions.CommandLineUtils;
using Newtonsoft.Json;
namespace YoutubeVideosCLI
{
public class CreateCommand : CommandLineApplication
{
private string youtubeApiUrl = string.Empty;
private string youtubeChannelsApiUrl = string.Empty;
private string youtubeSearchApiUrl = string.Empty;
private string requestParametersChannelId = string.Empty;
private string requestChannelVideosInfo = string.Empty;
private string videoDetailsUrl = string.Empty;
private string playlistsUrl = string.Empty;
private string playlistItemsUrl = string.Empty;
public CreateCommand()
{
try
{
this.Name = "Create";
this.Description = "Create Name";
CommandOption apiKey = this.Option("--api-key <apiKey>", "Api key", CommandOptionType.SingleValue);
CommandOption channelName = this.Option("--channelName <channelName>", "channel Name", CommandOptionType.SingleValue);
CommandOption channel = this.Option("--channel <channel>", "channel Id", CommandOptionType.SingleValue);
CommandOption inputfilepath = this.Option("--input-file-path <inputfilepath>", "inputfilepath", CommandOptionType.SingleValue);
CommandOption outputfilepath = this.Option("--output-file-path <outputfilepath>", "outputfilepath", CommandOptionType.SingleValue);
CommandOption videoNodeInPortal = this.Option("--videoNodeInPortal <videoNodeInPortal>", "videoNodeInPortal", CommandOptionType.SingleValue);
CommandOption title = this.Option("--title <title>", "title Name", CommandOptionType.SingleValue);
CommandOption datefrom = this.Option("--date-from <datefrom>", "datefrom", CommandOptionType.SingleValue);
CommandOption dateto = this.Option("--date-to <dateto>", "dateto Name", CommandOptionType.SingleValue);
CommandOption interval = this.Option("--interval <interval>", "interval Name", CommandOptionType.SingleValue);
this.OnExecute(async () =>
{
string apiKeysStr = apiKey?.Value();
string channelStr = channel?.Value();
string outputfilepathStr = outputfilepath?.Value();
string inputfilepathStr = inputfilepath?.Value();
string titleStr = title?.Value();
string channelNameStr = channelName?.Value();
string datefromStr = datefrom?.Value();
string datetoStr = dateto?.Value();
string intervalStr = interval?.Value();
int videoNodeInPortalInt = videoNodeInPortal.Value() == null ? 1 : int.Parse(videoNodeInPortal.Value());
youtubeApiUrl = "https://youtube.googleapis.com/youtube/v3/";
playlistsUrl = youtubeApiUrl + "playlists?part=snippet%2CcontentDetails&key={0}";
playlistsUrl = string.Format(playlistsUrl, apiKeysStr) + "&channelId={0}&maxResults=25&pageToken={1}";
playlistItemsUrl = youtubeApiUrl + "playlistItems?part=snippet%2CcontentDetails&key={0}";
playlistItemsUrl = string.Format(playlistItemsUrl, apiKeysStr) + "&playlistId={0}&maxResults=25&pageToken={1}";
youtubeChannelsApiUrl = youtubeApiUrl + "channels?key={0}&";
youtubeChannelsApiUrl = string.Format(youtubeChannelsApiUrl, apiKeysStr);
youtubeSearchApiUrl = youtubeApiUrl + "search?key={0}&";
youtubeSearchApiUrl = string.Format(youtubeSearchApiUrl, apiKeysStr);
requestParametersChannelId = youtubeChannelsApiUrl + "id={0}&part=id";
requestChannelVideosInfo = youtubeSearchApiUrl + "channelId={0}&part=id&order=date&type=video&publishedBefore={1}&publishedAfter={2}&pageToken={3}&maxResults=50";
videoDetailsUrl = youtubeApiUrl + "videos?part=snippet%2CcontentDetails%2Cstatistics&id={0}&key=";
videoDetailsUrl = string.Format(videoDetailsUrl, apiKeysStr);
Paperbits paperbitObj = new Paperbits();
string channelId = this.getChannelId(channelStr);
List<PlayListItem> playListItems = getChannelPlaylists(channelId);
using (StreamReader r = new StreamReader(inputfilepathStr))
{
string json = r.ReadToEnd();
// update channel name.
if (!string.IsNullOrEmpty(channelNameStr))
{
json = json.Replace("#ChannelName", channelNameStr);
}
paperbitObj = JsonConvert.DeserializeObject<Paperbits>(json);
Page pageVid = paperbitObj.pages.Where(p => p.Value.locales.EnUs.title.ToLower() == titleStr.ToLower())?.Select(pg => pg.Value)?.FirstOrDefault();
string fileContentKey = pageVid.locales.EnUs.contentKey.Replace("files/", "");
Page file = paperbitObj.files[fileContentKey];
string nodeStr = JsonConvert.SerializeObject(file.nodes[videoNodeInPortalInt]);
string nodeFileBackup = JsonConvert.SerializeObject(file.nodes[videoNodeInPortalInt]);
string fileStr = JsonConvert.SerializeObject(paperbitObj.files[fileContentKey]);
string fileStrBackup = JsonConvert.SerializeObject(paperbitObj.files[fileContentKey]);
foreach (var item in playListItems)
{
if (!string.IsNullOrEmpty(item.snippet.title))
{
fileStr = fileStr.Replace("#PlayListTitle", item.snippet.title);
}
fileStr = fileStr.Replace("#PlaylistDesc", item.snippet.description);
Page fileval = JsonConvert.DeserializeObject<Page>(fileStr);
List<VideoItem> videoItems = getVideosFromPlaylists(item.id);
int count = 0;
int pageCount = 0;
foreach (var videoItem in videoItems)
{
Node node = new Node();
count++;
if (!string.IsNullOrEmpty(videoItem.snippet.title))
{
nodeStr = nodeStr.Replace("#VidoeTitle1", videoItem.snippet.title);
}
if (!string.IsNullOrEmpty(videoItem.snippet.description))
{
nodeStr = nodeStr.Replace("#VideoDescription1", videoItem.snippet.description);
}
if (!string.IsNullOrEmpty(videoItem.id))
{
nodeStr = nodeStr.Replace("VideoId1", videoItem.snippet.resourceId.videoId);
}
node = JsonConvert.DeserializeObject<Node>(nodeStr);
nodeStr = nodeFileBackup;
if (count == 1)
{
fileval.nodes[videoNodeInPortalInt] = node;
}
else
{
fileval.nodes.Add(node);
}
if (count == 5)
{
count = 0;
pageCount++;
AddPageToModel(paperbitObj, item, item.id + pageCount, fileval, pageCount.ToString());
fileStr = fileStrBackup;
if (!string.IsNullOrEmpty(item.snippet.title))
{
fileStr = fileStr.Replace("#PlayListTitle", item.snippet.title);
}
fileStr = fileStr.Replace("#PlaylistDesc", item.snippet.description);
}
}
if (paperbitObj.files.ContainsKey(item.id))
{
paperbitObj.files.Remove(item.id);
}
AddPageToModel(paperbitObj, item, item.id, fileval);
fileStr = fileStrBackup;
fileval = JsonConvert.DeserializeObject<Page>(fileStr);
}
}
string outJson = JsonConvert.SerializeObject(paperbitObj, Formatting.Indented, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
});
File.WriteAllText(outputfilepathStr, outJson);
return 0;
});
}
catch (Exception ex)
{
throw ex;
}
}
private static void AddPageToModel(Paperbits paperbitObj, PlayListItem item, string pageName, Page fileval, string pageNum = "")
{
if (paperbitObj.files.ContainsKey(pageName))
{
paperbitObj.files[pageName] = fileval;
}
else
{
paperbitObj.files.Add(pageName, fileval);
}
Page newPage = new Page();
string pageid = pageName + "_page";
newPage.key = "pages/" + pageid;
newPage.locales = new Locales();
newPage.locales.EnUs = new EnUs();
newPage.locales.EnUs.contentKey = "files/" + item.id;
newPage.locales.EnUs.title = item.snippet.title + "-" + pageNum;
newPage.locales.EnUs.description = item.snippet.description;
newPage.locales.EnUs.permalink = "/videos-" + pageid;
if (paperbitObj.pages.ContainsKey(pageid))
{
paperbitObj.pages.Remove(pageid);
}
paperbitObj.pages.Add(pageid, newPage);
NavigationItem navigationItem = paperbitObj.navigationItems.Find(n => n.key == "navigationItemPlaylist");
if (navigationItem == null)
{
navigationItem = new NavigationItem();
}
if (navigationItem.navigationItems == null)
{
navigationItem.navigationItems = new List<NavigationItem>();
}
//add menu navigations items based on pages created.
navigationItem.navigationItems.Add(new NavigationItem
{
key = pageid + "_nav",
label = newPage.locales.EnUs.title,
targetWindow = "_self",
anchor = "5FbTd",
targetKey = newPage.key
});
int index = paperbitObj.navigationItems.FindIndex(n => n.key == "navigationItemPlaylist");
paperbitObj.navigationItems[index] = navigationItem;
}
private string getChannelId(string channelName)
{
ColoredConsole.Out.WriteLine("Searching channel id for channel: " + channelName);
try
{
string url = string.Format(requestParametersChannelId, channelName);
HttpClient client = new HttpClient();
string response = client.GetStringAsync(url).Result;
if (response != string.Empty)
{
Playlist playlist = JsonConvert.DeserializeObject<Playlist>(response);
return playlist.items[0].id;
}
else
{
throw new Exception("channel id not found.");
}
}
catch (Exception ex)
{
throw ex;
}
}
private dynamic getVideoDetailsById(string videoId)
{
try
{
string url = string.Format(videoDetailsUrl, videoId);
ColoredConsole.Out.WriteLine("Request: " + url);
HttpClient client = new HttpClient();
dynamic response = client.GetAsync(url).Result;
if (response.items?.Count > 0)
{
return response.items[0];
}
else
{
throw new Exception("channel id not found.");
}
}
catch (Exception ex)
{
throw ex;
}
}
private List<PlayListItem> getChannelPlaylists(string channelId)
{
try
{
bool foundAll = false;
List<PlayListItem> playlists = new List<PlayListItem>();
string nextPageToken = string.Empty;
while (!foundAll)
{
string url = string.Format(playlistsUrl, channelId, nextPageToken);
ColoredConsole.Out.WriteLine("Request: " + url);
HttpClient client = new HttpClient();
string response = client.GetStringAsync(url).Result;
if (response != string.Empty)
{
Playlist playlist = JsonConvert.DeserializeObject<Playlist>(response);
playlist.items.ForEach(item => playlists.Add(item));
nextPageToken = playlist.nextPageToken;
if(nextPageToken == null || nextPageToken == string.Empty)
{
foundAll = true;
}
}
else
{
foundAll = true;
throw new Exception("channel id not found.");
}
}
return playlists;
}
catch (Exception ex)
{
throw ex;
}
}
private List<VideoItem> getVideosFromPlaylists(string playlistId)
{
try
{
bool foundAll = false;
List<VideoItem> playlists = new List<VideoItem>();
string nextPageToken = string.Empty;
while (!foundAll)
{
string url = string.Format(playlistItemsUrl, playlistId, nextPageToken);
ColoredConsole.Out.WriteLine("Request: " + url);
HttpClient client = new HttpClient();
string response = client.GetStringAsync(url).Result;
if (response != string.Empty)
{
PlaylistItemVal playlist = JsonConvert.DeserializeObject<PlaylistItemVal>(response);
playlist.items.ForEach(item => playlists.Add(item));
nextPageToken = playlist.nextPageToken;
if (nextPageToken == null || nextPageToken == string.Empty)
{
foundAll = true;
}
}
else
{
foundAll = true;
throw new Exception("channel id not found.");
}
}
return playlists;
}
catch (Exception ex)
{
throw ex;
}
}
}
}
Running this CLI tool locally:
dotnet run create --api-key secrets.GoogleApiKey --channel youTubeChannelId --output-file-path "outputfilepath.json" --input-file-path "inputfilepath.json" --title "videos" --channelName "Sanjeevi Channel"
This command line tool writes to json file which can be used by the Paperbits generated static website to show videos from provided YouTube channel.
We can customize this CLI tool to write to any html or other files and which can be used to build a static website which will update itself using this CLI tool.
You can refer to the Repo with full code here:
SSanjeevi/YouTubePlaylistSite: YouTube Playlist Site (github.com)
Will write an article on how to use this CLI tool in GitHub Actions.
Good one Sanjeevi!👍
ReplyDelete