diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Actions/GetVideoInfoAction.cs b/src/modules/cmdpal/Exts/YouTubeExtension/Actions/GetVideoInfoAction.cs new file mode 100644 index 0000000000..98c4dcea2d --- /dev/null +++ b/src/modules/cmdpal/Exts/YouTubeExtension/Actions/GetVideoInfoAction.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CmdPal.Extensions.Helpers; + +namespace YouTubeExtension.Helper; + +internal sealed partial class GetVideoInfoAction : InvokableCommand +{ + private readonly YouTubeVideo _video; + + internal GetVideoInfoAction(YouTubeVideo video) + { + this._video = video; + this.Name = "See more information"; + this.Icon = new("\uE946"); + } + + public override CommandResult Invoke() + { + return CommandResult.KeepOpen(); + } +} diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Actions/OpenChannelLinkAction.cs b/src/modules/cmdpal/Exts/YouTubeExtension/Actions/OpenChannelLinkAction.cs new file mode 100644 index 0000000000..4cf3acb596 --- /dev/null +++ b/src/modules/cmdpal/Exts/YouTubeExtension/Actions/OpenChannelLinkAction.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CmdPal.Extensions.Helpers; + +namespace YouTubeExtension.Helper; + +internal sealed partial class OpenChannelLinkAction : InvokableCommand +{ + private readonly YouTubeVideo _video; + + internal OpenChannelLinkAction(YouTubeVideo video) + { + this._video = video; + this.Name = "Open channel"; + this.Icon = new("\uF131"); + } + + public override CommandResult Invoke() + { + Process.Start(new ProcessStartInfo(_video.ChannelUrl) { UseShellExecute = true }); + return CommandResult.KeepOpen(); + } +} diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Helper/LinkAction.cs b/src/modules/cmdpal/Exts/YouTubeExtension/Actions/OpenVideoLinkAction.cs similarity index 77% rename from src/modules/cmdpal/Exts/YouTubeExtension/Helper/LinkAction.cs rename to src/modules/cmdpal/Exts/YouTubeExtension/Actions/OpenVideoLinkAction.cs index 214225adcb..1b0ee962a2 100644 --- a/src/modules/cmdpal/Exts/YouTubeExtension/Helper/LinkAction.cs +++ b/src/modules/cmdpal/Exts/YouTubeExtension/Actions/OpenVideoLinkAction.cs @@ -12,15 +12,15 @@ using Microsoft.CmdPal.Extensions.Helpers; namespace YouTubeExtension.Helper; -internal sealed partial class LinkAction : InvokableCommand +internal sealed partial class OpenVideoLinkAction : InvokableCommand { private readonly YouTubeVideo _video; - internal LinkAction(YouTubeVideo video) + internal OpenVideoLinkAction(YouTubeVideo video) { this._video = video; - this.Name = "Open link"; - this.Icon = new("https://www.youtube.com/favicon.ico"); + this.Name = "Open video"; + this.Icon = new("\uE714"); } public override CommandResult Invoke() diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Helper/YouTubeVideo.cs b/src/modules/cmdpal/Exts/YouTubeExtension/Helper/YouTubeVideo.cs index 2835b184ab..97113ae107 100644 --- a/src/modules/cmdpal/Exts/YouTubeExtension/Helper/YouTubeVideo.cs +++ b/src/modules/cmdpal/Exts/YouTubeExtension/Helper/YouTubeVideo.cs @@ -21,6 +21,12 @@ public sealed class YouTubeVideo // The author or channel name of the video public string Author { get; init; } = string.Empty; + // The channel id (needed for the channel URL) + public string ChannelId { get; set; } + + // The URL link to the channel + public string ChannelUrl { get; set; } + // The URL to the thumbnail image of the video public string ThumbnailUrl { get; init; } = string.Empty; diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeAPIForm.cs b/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeAPIForm.cs index 5dc092b2de..cb781712e5 100644 --- a/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeAPIForm.cs +++ b/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeAPIForm.cs @@ -27,7 +27,7 @@ internal sealed partial class YouTubeAPIForm : Form "body": [ { "type": "Input.Text", - "style": "text", + "style": "password", "id": "apiKey", "label": "API Key", "isRequired": true, diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeAPIPage.cs b/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeAPIPage.cs index 1a4f4a557b..65c41da43a 100644 --- a/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeAPIPage.cs +++ b/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeAPIPage.cs @@ -20,7 +20,7 @@ internal sealed partial class YouTubeAPIPage : FormPage public YouTubeAPIPage() { - Name = "YouTube API"; + Name = "Edit YouTube API Key"; Icon = new("https://www.youtube.com/favicon.ico"); } } diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeChannelsPage.cs b/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeChannelsPage.cs new file mode 100644 index 0000000000..b42ff28237 --- /dev/null +++ b/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeChannelsPage.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; +using YouTubeExtension.Helper; + +namespace YouTubeExtension.Pages; + +internal sealed partial class YouTubeChannelsPage : DynamicListPage +{ + public YouTubeChannelsPage() + { + Icon = new("https://www.youtube.com/favicon.ico"); + Name = "YouTube"; + this.ShowDetails = true; + } + + public override ISection[] GetItems(string query) + { + return DoGetItems(query).GetAwaiter().GetResult(); // Fetch and await the task synchronously + } + + private async Task DoGetItems(string query) + { + // Fetch YouTube videos based on the query + List items = await GetYouTubeVideos(query); + + // Create a section and populate it with the video results + var section = new ListSection() + { + Title = "Search Results", + Items = items.Select(video => new ListItem(new OpenVideoLinkAction(video)) + { + Title = video.Title, + Subtitle = $"{video.Author}", + Details = new Details() + { + Title = video.Title, + HeroImage = new(video.ThumbnailUrl), + Body = $"{video.Author}", + }, + Tags = [new Tag() + { + Text = video.PublishedAt.ToString("MMMM dd, yyyy", CultureInfo.InvariantCulture), // Show the date of the video post + } + ], + }).ToArray(), + }; + + return new[] { section }; // Properly return an array of sections + } + + // Method to fetch videos from YouTube API + private static async Task> GetYouTubeVideos(string query) + { + var state = File.ReadAllText(YouTubeHelper.StateJsonPath()); + var jsonState = JsonNode.Parse(state); + var apiKey = jsonState["apiKey"]?.ToString() ?? string.Empty; + + var videos = new List(); + + using HttpClient client = new HttpClient(); + { + try + { + // Send the request to the YouTube API with the provided query + var response = await client.GetStringAsync($"https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&q={query}&key={apiKey}&maxResults=2"); + var json = JsonNode.Parse(response); + + // Parse the response + if (json?["items"] is JsonArray itemsArray) + { + foreach (var item in itemsArray) + { + // Add each video to the list with title, link, author, thumbnail, and captions (if available) + videos.Add(new YouTubeVideo + { + Title = item["snippet"]?["title"]?.ToString() ?? string.Empty, + Link = $"https://www.youtube.com/watch?v={item["id"]?["videoId"]?.ToString()}", + Author = item["snippet"]?["channelTitle"]?.ToString() ?? string.Empty, + ThumbnailUrl = item["snippet"]?["thumbnails"]?["default"]?["url"]?.ToString() ?? string.Empty, // Get the default thumbnail URL + Captions = "Captions not available", // Placeholder for captions; You can integrate with another API if needed + PublishedAt = DateTime.Parse(item["snippet"]?["publishedAt"]?.ToString(), CultureInfo.InvariantCulture), // Use CultureInfo.InvariantCulture + }); + } + } + } + catch (Exception ex) + { + // Handle any errors from the API call or parsing + videos.Add(new YouTubeVideo + { + Title = "Error fetching data", + Link = string.Empty, + Author = $"Error: {ex.Message}", + ThumbnailUrl = string.Empty, + Captions = string.Empty, + PublishedAt = DateTime.MinValue, + }); + } + } + + return videos; + } +} diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeVideoInfoMarkdownPage.cs b/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeVideoInfoMarkdownPage.cs new file mode 100644 index 0000000000..e21afe880b --- /dev/null +++ b/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeVideoInfoMarkdownPage.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.WindowsRuntime; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.CmdPal.Extensions; +using Microsoft.CmdPal.Extensions.Helpers; +using Microsoft.UI.Windowing; + +namespace SamplePagesExtension; + +internal sealed partial class YouTubeVideoInfoMarkdownPage : MarkdownPage +{ + private readonly string _markdown = @" +# Markdown Guide + +Markdown is a lightweight markup language with plain text formatting syntax. It's often used to format readme files, for writing messages in online forums, and to create rich text using a simple, plain text editor. + +--- + +## Headings + +You can create headings using the `#` symbol, with the number of `#` determining the heading level. + +```markdown +# H1 Heading +## H2 Heading +### H3 Heading +#### H4 Heading +"; + + public YouTubeVideoInfoMarkdownPage() + { + Icon = new("\uE946"); + Name = "See more information"; + } + + public override string[] Bodies() + { + return [_markdown]; + } +} diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeVideosPage.cs b/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeVideosPage.cs index 1fd6408b6d..545f573fca 100644 --- a/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeVideosPage.cs +++ b/src/modules/cmdpal/Exts/YouTubeExtension/Pages/YouTubeVideosPage.cs @@ -9,10 +9,13 @@ using System.IO; using System.Linq; using System.Net.Http; using System.Text.Json.Nodes; +using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.CmdPal.Extensions; using Microsoft.CmdPal.Extensions.Helpers; +using SamplePagesExtension; using YouTubeExtension.Helper; +using YouTubeExtension.Pages; namespace YouTubeExtension; @@ -39,7 +42,7 @@ internal sealed partial class YouTubeVideosPage : DynamicListPage var section = new ListSection() { Title = "Search Results", - Items = items.Select(video => new ListItem(new LinkAction(video)) + Items = items.Select(video => new ListItem(new OpenVideoLinkAction(video)) { Title = video.Title, Subtitle = $"{video.Author}", @@ -54,6 +57,11 @@ internal sealed partial class YouTubeVideosPage : DynamicListPage Text = video.PublishedAt.ToString("MMMM dd, yyyy", CultureInfo.InvariantCulture), // Show the date of the video post } ], + MoreCommands = [ + new CommandContextItem(new OpenChannelLinkAction(video)), + new CommandContextItem(new YouTubeVideoInfoMarkdownPage()), + new CommandContextItem(new YouTubeAPIPage()), + ], }).ToArray(), }; @@ -74,7 +82,7 @@ internal sealed partial class YouTubeVideosPage : DynamicListPage try { // Send the request to the YouTube API with the provided query - var response = await client.GetStringAsync($"https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&q={query}&key={apiKey}&maxResults=20"); + var response = await client.GetStringAsync($"https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&q={query}&key={apiKey}&maxResults=2"); var json = JsonNode.Parse(response); // Parse the response @@ -88,6 +96,8 @@ internal sealed partial class YouTubeVideosPage : DynamicListPage Title = item["snippet"]?["title"]?.ToString() ?? string.Empty, Link = $"https://www.youtube.com/watch?v={item["id"]?["videoId"]?.ToString()}", Author = item["snippet"]?["channelTitle"]?.ToString() ?? string.Empty, + ChannelId = item["snippet"]?["channelId"]?.ToString() ?? string.Empty, + ChannelUrl = $"https://www.youtube.com/channel/{item["snippet"]?["channelId"]?.ToString()}" ?? string.Empty, ThumbnailUrl = item["snippet"]?["thumbnails"]?["default"]?["url"]?.ToString() ?? string.Empty, // Get the default thumbnail URL Captions = "Captions not available", // Placeholder for captions; You can integrate with another API if needed PublishedAt = DateTime.Parse(item["snippet"]?["publishedAt"]?.ToString(), CultureInfo.InvariantCulture), // Use CultureInfo.InvariantCulture diff --git a/src/modules/cmdpal/Exts/YouTubeExtension/YouTubeExtensionCommandsProvider.cs b/src/modules/cmdpal/Exts/YouTubeExtension/YouTubeExtensionCommandsProvider.cs index 8cc2c00fc2..d11ee2f045 100644 --- a/src/modules/cmdpal/Exts/YouTubeExtension/YouTubeExtensionCommandsProvider.cs +++ b/src/modules/cmdpal/Exts/YouTubeExtension/YouTubeExtensionCommandsProvider.cs @@ -25,6 +25,7 @@ public partial class YouTubeExtensionActionsProvider : ICommandProvider private readonly IListItem[] _commands = [ new ListItem(new YouTubeVideosPage()) { Title = "Search Videos", Subtitle = "YouTube" }, + new ListItem(new YouTubeChannelsPage()) { Title = "Search Channels", Subtitle = "YouTube" }, ]; private readonly YouTubeAPIPage apiPage = new();