mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-11 23:06:45 +01:00
YouTube Extension V0 Complete
This commit is contained in:
@@ -10,7 +10,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
namespace YouTubeExtension.Helper;
|
||||
namespace YouTubeExtension.Actions;
|
||||
|
||||
internal sealed partial class GetVideoInfoAction : InvokableCommand
|
||||
{
|
||||
|
||||
@@ -10,22 +10,22 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
namespace YouTubeExtension.Helper;
|
||||
namespace YouTubeExtension.Actions;
|
||||
|
||||
internal sealed partial class OpenChannelLinkAction : InvokableCommand
|
||||
{
|
||||
private readonly YouTubeVideo _video;
|
||||
private readonly string _channelurl;
|
||||
|
||||
internal OpenChannelLinkAction(YouTubeVideo video)
|
||||
internal OpenChannelLinkAction(string url)
|
||||
{
|
||||
this._video = video;
|
||||
this._channelurl = url;
|
||||
this.Name = "Open channel";
|
||||
this.Icon = new("\uF131");
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(_video.ChannelUrl) { UseShellExecute = true });
|
||||
Process.Start(new ProcessStartInfo(_channelurl) { UseShellExecute = true });
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,22 +10,22 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
namespace YouTubeExtension.Helper;
|
||||
namespace YouTubeExtension.Actions;
|
||||
|
||||
internal sealed partial class OpenVideoLinkAction : InvokableCommand
|
||||
{
|
||||
private readonly YouTubeVideo _video;
|
||||
private readonly string _videourl;
|
||||
|
||||
internal OpenVideoLinkAction(YouTubeVideo video)
|
||||
internal OpenVideoLinkAction(string url)
|
||||
{
|
||||
this._video = video;
|
||||
this._videourl = url;
|
||||
this.Name = "Open video";
|
||||
this.Icon = new("\uE714");
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(_video.Link) { UseShellExecute = true });
|
||||
Process.Start(new ProcessStartInfo(_videourl) { UseShellExecute = true });
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YouTubeExtension.Actions;
|
||||
|
||||
public sealed class YouTubeChannel
|
||||
{
|
||||
// The name of the channel
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
// The unique id of the channel
|
||||
public string ChannelId { get; set; } = string.Empty;
|
||||
|
||||
// The URL link to the channel
|
||||
public string ChannelUrl { get; set; } = string.Empty;
|
||||
|
||||
// The URL to the profile picture of the channel
|
||||
public string ProfilePicUrl { get; set; } = string.Empty;
|
||||
|
||||
// The description of the channel
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
// Number of subscribers the channel has
|
||||
public long SubscriberCount { get; set; }
|
||||
|
||||
// Number of views the channel has
|
||||
public long ViewCount { get; set; }
|
||||
|
||||
// Number of videos the channel has
|
||||
public long VideoCount { get; set; }
|
||||
}
|
||||
@@ -9,7 +9,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YouTubeExtension.Helper;
|
||||
namespace YouTubeExtension.Actions;
|
||||
|
||||
internal sealed class YouTubeHelper
|
||||
{
|
||||
|
||||
@@ -8,31 +8,46 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YouTubeExtension.Helper;
|
||||
namespace YouTubeExtension.Actions;
|
||||
|
||||
public sealed class YouTubeVideo
|
||||
{
|
||||
// The title of the video
|
||||
public string Title { get; init; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
// The unique id of the video
|
||||
public string VideoId { get; set; } = string.Empty;
|
||||
|
||||
// The URL link to the video
|
||||
public string Link { get; init; } = string.Empty;
|
||||
public string Link { get; set; } = string.Empty;
|
||||
|
||||
// The author or channel name of the video
|
||||
public string Author { get; init; } = string.Empty;
|
||||
public string Channel { get; set; } = string.Empty;
|
||||
|
||||
// The channel id (needed for the channel URL)
|
||||
public string ChannelId { get; set; }
|
||||
public string ChannelId { get; set; } = string.Empty;
|
||||
|
||||
// The URL link to the channel
|
||||
public string ChannelUrl { get; set; }
|
||||
public string ChannelUrl { get; set; } = string.Empty;
|
||||
|
||||
// The URL to the profile picture of the channel
|
||||
public string ChannelProfilePicUrl { get; set; }
|
||||
|
||||
// Number of subscribers the channel has
|
||||
public long SubscriberCount { get; set; }
|
||||
|
||||
// The URL to the thumbnail image of the video
|
||||
public string ThumbnailUrl { get; init; } = string.Empty;
|
||||
public string ThumbnailUrl { get; set; } = string.Empty;
|
||||
|
||||
// Captions or subtitles associated with the video
|
||||
public string Captions { get; init; } = string.Empty;
|
||||
public string Caption { get; set; } = string.Empty;
|
||||
|
||||
// The date and time the video was published
|
||||
public DateTime PublishedAt { get; set; }
|
||||
public DateTime PublishedAt { get; set; } = DateTime.MinValue;
|
||||
|
||||
// Number of views the video has
|
||||
public long ViewCount { get; set; }
|
||||
|
||||
// Number of likes the video has
|
||||
public long LikeCount { get; set; }
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using YouTubeExtension.Helper;
|
||||
using YouTubeExtension.Actions;
|
||||
|
||||
namespace YouTubeExtension.Pages;
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// 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.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using YouTubeExtension.Actions;
|
||||
|
||||
namespace YouTubeExtension.Pages;
|
||||
|
||||
internal sealed partial class YouTubeChannelInfoMarkdownPage : MarkdownPage
|
||||
{
|
||||
private readonly YouTubeChannel _channel;
|
||||
private string _markdown = string.Empty;
|
||||
|
||||
public YouTubeChannelInfoMarkdownPage(YouTubeChannel channel)
|
||||
{
|
||||
Icon = new("\uE946");
|
||||
Name = "See more information about this channel";
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public override string[] Bodies()
|
||||
{
|
||||
var state = File.ReadAllText(YouTubeHelper.StateJsonPath());
|
||||
var jsonState = JsonNode.Parse(state);
|
||||
var apiKey = jsonState["apiKey"]?.ToString() ?? string.Empty;
|
||||
|
||||
FillInChannelDetailsAsync(_channel, apiKey).GetAwaiter().GetResult();
|
||||
|
||||
// Define the markdown content using the channel information
|
||||
_markdown = $@"
|
||||
# {_channel.Name}
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**Channel Description**
|
||||
|
||||
{_channel.Description}
|
||||
|
||||
---
|
||||
|
||||
**Key Stats**
|
||||
|
||||
- **Subscribers:** {_channel.SubscriberCount:N0}
|
||||
- **Total Views:** {_channel.ViewCount:N0}
|
||||
- **Total Videos:** {_channel.VideoCount:N0}
|
||||
|
||||
[Visit Channel]({_channel.ChannelUrl})
|
||||
|
||||
---
|
||||
|
||||
_Last updated: {DateTime.Now:MMMM dd, yyyy}_
|
||||
_Data sourced via YouTube API_
|
||||
";
|
||||
|
||||
return new string[] { _markdown };
|
||||
}
|
||||
|
||||
private async Task<YouTubeChannel> FillInChannelDetailsAsync(YouTubeChannel channel, string apiKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
|
||||
// Fetch channel details from YouTube API
|
||||
var channelUrl = $"https://www.googleapis.com/youtube/v3/channels?part=snippet,statistics&id={channel.ChannelId}&key={apiKey}";
|
||||
var channelResponse = await httpClient.GetStringAsync(channelUrl);
|
||||
var channelData = JsonNode.Parse(channelResponse);
|
||||
|
||||
if (channelData?["items"]?.AsArray().Count > 0)
|
||||
{
|
||||
var channelSnippet = channelData?["items"]?[0]?["snippet"];
|
||||
var channelStatistics = channelData?["items"]?[0]?["statistics"];
|
||||
|
||||
// Update statistics
|
||||
channel.ViewCount = long.TryParse(channelStatistics?["viewCount"]?.ToString(), out var views) ? views : 0;
|
||||
channel.VideoCount = long.TryParse(channelStatistics?["videoCount"]?.ToString(), out var videos) ? videos : 0;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle exceptions (e.g., log the error)
|
||||
Console.WriteLine($"An error occurred while fetching channel details: {ex.Message}");
|
||||
}
|
||||
|
||||
return channel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// 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.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using YouTubeExtension.Actions;
|
||||
|
||||
namespace YouTubeExtension.Pages;
|
||||
|
||||
internal sealed partial class YouTubeChannelVideosPage : DynamicListPage
|
||||
{
|
||||
private readonly string _channelId;
|
||||
private readonly string _channelName;
|
||||
|
||||
public YouTubeChannelVideosPage(string channelId = null, string channelName = null)
|
||||
{
|
||||
Icon = new("https://www.youtube.com/favicon.ico");
|
||||
Name = $"YouTube ({channelName ?? "Channel Video Search"})";
|
||||
this.ShowDetails = true;
|
||||
|
||||
// Ensure either a ChannelId or ChannelName is provided
|
||||
if (string.IsNullOrEmpty(channelId) && string.IsNullOrEmpty(channelName))
|
||||
{
|
||||
throw new ArgumentException("Either channelId or channelName must be provided.");
|
||||
}
|
||||
|
||||
_channelId = channelId;
|
||||
_channelName = channelName;
|
||||
}
|
||||
|
||||
public override ISection[] GetItems(string query)
|
||||
{
|
||||
return DoGetItems(query).GetAwaiter().GetResult(); // Fetch and await the task synchronously
|
||||
}
|
||||
|
||||
private async Task<ISection[]> DoGetItems(string query)
|
||||
{
|
||||
// Fetch YouTube videos scoped to the channel
|
||||
List<YouTubeVideo> items = await GetYouTubeChannelVideos(query, _channelId, _channelName);
|
||||
|
||||
// Create a section and populate it with the video results
|
||||
var section = new ListSection()
|
||||
{
|
||||
Title = $"Videos from {_channelName ?? _channelId}",
|
||||
Items = items.Select(video => new ListItem(new OpenVideoLinkAction(video.Link))
|
||||
{
|
||||
Title = video.Title,
|
||||
Subtitle = $"{video.Channel}",
|
||||
Details = new Details()
|
||||
{
|
||||
Title = video.Title,
|
||||
HeroImage = new(video.ThumbnailUrl),
|
||||
Body = $"{video.Channel}",
|
||||
},
|
||||
Tags = [
|
||||
new Tag()
|
||||
{
|
||||
Text = video.PublishedAt.ToString("MMMM dd, yyyy", CultureInfo.InvariantCulture), // Show the date of the video post
|
||||
}
|
||||
],
|
||||
MoreCommands = [
|
||||
new CommandContextItem(new OpenChannelLinkAction(video.ChannelUrl)),
|
||||
new CommandContextItem(new YouTubeVideoInfoMarkdownPage(video)),
|
||||
new CommandContextItem(new YouTubeAPIPage()),
|
||||
],
|
||||
}).ToArray(),
|
||||
};
|
||||
|
||||
return new[] { section }; // Properly return an array of sections
|
||||
}
|
||||
|
||||
// Method to fetch videos from a specific channel
|
||||
private static async Task<List<YouTubeVideo>> GetYouTubeChannelVideos(string query, string channelId, string channelName)
|
||||
{
|
||||
var state = File.ReadAllText(YouTubeHelper.StateJsonPath());
|
||||
var jsonState = JsonNode.Parse(state);
|
||||
var apiKey = jsonState["apiKey"]?.ToString() ?? string.Empty;
|
||||
|
||||
var videos = new List<YouTubeVideo>();
|
||||
|
||||
using HttpClient client = new HttpClient();
|
||||
{
|
||||
try
|
||||
{
|
||||
// Build the YouTube API URL for fetching channel-specific videos
|
||||
string requestUrl;
|
||||
|
||||
if (!string.IsNullOrEmpty(channelId))
|
||||
{
|
||||
// If ChannelId is provided, filter by channelId
|
||||
requestUrl = $"https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&channelId={channelId}&q={query}&key={apiKey}&maxResults=10";
|
||||
}
|
||||
else
|
||||
{
|
||||
// If ChannelName is provided, search by the channel name
|
||||
requestUrl = $"https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&q={query}+{channelName}&key={apiKey}&maxResults=10";
|
||||
}
|
||||
|
||||
// Send the request to the YouTube API
|
||||
var response = await client.GetStringAsync(requestUrl);
|
||||
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,
|
||||
VideoId = item["id"]?["videoId"]?.ToString() ?? string.Empty,
|
||||
Link = $"https://www.youtube.com/watch?v={item["id"]?["videoId"]?.ToString()}",
|
||||
Channel = 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
|
||||
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",
|
||||
Channel = $"Error: {ex.Message}",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return videos;
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,11 @@ 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;
|
||||
using YouTubeExtension.Actions;
|
||||
|
||||
namespace YouTubeExtension.Pages;
|
||||
|
||||
@@ -23,7 +21,7 @@ internal sealed partial class YouTubeChannelsPage : DynamicListPage
|
||||
public YouTubeChannelsPage()
|
||||
{
|
||||
Icon = new("https://www.youtube.com/favicon.ico");
|
||||
Name = "YouTube";
|
||||
Name = "YouTube (Channel Search)";
|
||||
this.ShowDetails = true;
|
||||
}
|
||||
|
||||
@@ -34,49 +32,49 @@ internal sealed partial class YouTubeChannelsPage : DynamicListPage
|
||||
|
||||
private async Task<ISection[]> DoGetItems(string query)
|
||||
{
|
||||
// Fetch YouTube videos based on the query
|
||||
List<YouTubeVideo> items = await GetYouTubeVideos(query);
|
||||
// Fetch YouTube channels based on the query
|
||||
List<YouTubeChannel> items = await GetYouTubeChannels(query);
|
||||
|
||||
// Create a section and populate it with the video results
|
||||
// Create a section and populate it with the channel results
|
||||
var section = new ListSection()
|
||||
{
|
||||
Title = "Search Results",
|
||||
Items = items.Select(video => new ListItem(new OpenVideoLinkAction(video))
|
||||
Items = items.Select(channel => new ListItem(new OpenChannelLinkAction(channel.ChannelUrl))
|
||||
{
|
||||
Title = video.Title,
|
||||
Subtitle = $"{video.Author}",
|
||||
Title = channel.Name,
|
||||
Subtitle = $"{channel.SubscriberCount} subscribers",
|
||||
Details = new Details()
|
||||
{
|
||||
Title = video.Title,
|
||||
HeroImage = new(video.ThumbnailUrl),
|
||||
Body = $"{video.Author}",
|
||||
Title = channel.Name,
|
||||
HeroImage = new(channel.ProfilePicUrl),
|
||||
Body = $"Subscribers: {channel.SubscriberCount}\nChannel Description: {channel.Description}",
|
||||
},
|
||||
Tags = [new Tag()
|
||||
{
|
||||
Text = video.PublishedAt.ToString("MMMM dd, yyyy", CultureInfo.InvariantCulture), // Show the date of the video post
|
||||
}
|
||||
],
|
||||
MoreCommands = [
|
||||
new CommandContextItem(new YouTubeChannelInfoMarkdownPage(channel)),
|
||||
new CommandContextItem(new YouTubeChannelVideosPage(channel.ChannelId, channel.Name)),
|
||||
new CommandContextItem(new YouTubeAPIPage()),
|
||||
],
|
||||
}).ToArray(),
|
||||
};
|
||||
|
||||
return new[] { section }; // Properly return an array of sections
|
||||
}
|
||||
|
||||
// Method to fetch videos from YouTube API
|
||||
private static async Task<List<YouTubeVideo>> GetYouTubeVideos(string query)
|
||||
// Method to fetch channels from YouTube API
|
||||
private static async Task<List<YouTubeChannel>> GetYouTubeChannels(string query)
|
||||
{
|
||||
var state = File.ReadAllText(YouTubeHelper.StateJsonPath());
|
||||
var jsonState = JsonNode.Parse(state);
|
||||
var apiKey = jsonState["apiKey"]?.ToString() ?? string.Empty;
|
||||
|
||||
var videos = new List<YouTubeVideo>();
|
||||
var channels = new List<YouTubeChannel>();
|
||||
|
||||
using HttpClient client = new HttpClient();
|
||||
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");
|
||||
// Send the request to the YouTube API with the provided query to search for channels
|
||||
var response = await client.GetStringAsync($"https://www.googleapis.com/youtube/v3/search?part=snippet&type=channel&q={query}&key={apiKey}&maxResults=5");
|
||||
var json = JsonNode.Parse(response);
|
||||
|
||||
// Parse the response
|
||||
@@ -84,15 +82,15 @@ internal sealed partial class YouTubeChannelsPage : DynamicListPage
|
||||
{
|
||||
foreach (var item in itemsArray)
|
||||
{
|
||||
// Add each video to the list with title, link, author, thumbnail, and captions (if available)
|
||||
videos.Add(new YouTubeVideo
|
||||
// Add each channel to the list with channel details
|
||||
channels.Add(new YouTubeChannel
|
||||
{
|
||||
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
|
||||
Name = 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,
|
||||
ProfilePicUrl = item["snippet"]?["thumbnails"]?["default"]?["url"]?.ToString() ?? string.Empty, // Get the default profile picture
|
||||
Description = item["snippet"]?["description"]?.ToString() ?? string.Empty,
|
||||
SubscriberCount = await GetChannelSubscriberCount(apiKey, item["snippet"]?["channelId"]?.ToString()), // Fetch the subscriber count
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -100,18 +98,43 @@ internal sealed partial class YouTubeChannelsPage : DynamicListPage
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle any errors from the API call or parsing
|
||||
videos.Add(new YouTubeVideo
|
||||
channels.Add(new YouTubeChannel
|
||||
{
|
||||
Title = "Error fetching data",
|
||||
Link = string.Empty,
|
||||
Author = $"Error: {ex.Message}",
|
||||
ThumbnailUrl = string.Empty,
|
||||
Captions = string.Empty,
|
||||
PublishedAt = DateTime.MinValue,
|
||||
Name = "Error fetching data",
|
||||
Description = $"Error: {ex.Message}",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return videos;
|
||||
return channels;
|
||||
}
|
||||
|
||||
// Fetch subscriber count for each channel using a separate API call
|
||||
private static async Task<long> GetChannelSubscriberCount(string apiKey, string channelId)
|
||||
{
|
||||
using HttpClient client = new HttpClient();
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await client.GetStringAsync($"https://www.googleapis.com/youtube/v3/channels?part=statistics&id={channelId}&key={apiKey}");
|
||||
var json = JsonNode.Parse(response);
|
||||
|
||||
if (json?["items"] is JsonArray itemsArray && itemsArray.Count > 0)
|
||||
{
|
||||
var statistics = itemsArray[0]?["statistics"];
|
||||
if (long.TryParse(statistics?["subscriberCount"]?.ToString(), out var subscriberCount))
|
||||
{
|
||||
return subscriberCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// In case of any error, return 0 subscribers
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,49 +3,125 @@
|
||||
// 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.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using Microsoft.UI.Windowing;
|
||||
using YouTubeExtension.Actions;
|
||||
|
||||
namespace SamplePagesExtension;
|
||||
namespace YouTubeExtension.Pages;
|
||||
|
||||
internal sealed partial class YouTubeVideoInfoMarkdownPage : MarkdownPage
|
||||
{
|
||||
private readonly string _markdown = @"
|
||||
# Markdown Guide
|
||||
private readonly YouTubeVideo _video;
|
||||
private string _markdown = string.Empty;
|
||||
|
||||
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()
|
||||
public YouTubeVideoInfoMarkdownPage(YouTubeVideo video)
|
||||
{
|
||||
Icon = new("\uE946");
|
||||
Name = "See more information";
|
||||
Name = "See more information about this video";
|
||||
_video = video;
|
||||
}
|
||||
|
||||
public override string[] Bodies()
|
||||
{
|
||||
return [_markdown];
|
||||
var state = File.ReadAllText(YouTubeHelper.StateJsonPath());
|
||||
var jsonState = JsonNode.Parse(state);
|
||||
var apiKey = jsonState["apiKey"]?.ToString() ?? string.Empty;
|
||||
|
||||
FillInVideoDetailsAsync(_video, apiKey).GetAwaiter().GetResult();
|
||||
|
||||
// Refined markdown content for user-focused display
|
||||
_markdown = $@"
|
||||
# {_video.Title}
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**Video Description**
|
||||
|
||||
{_video.Caption}
|
||||
|
||||
---
|
||||
|
||||
**Key Stats**
|
||||
|
||||
- **Views:** {_video.ViewCount:N0}
|
||||
- **Likes:** {_video.LikeCount:N0}
|
||||
- **Published on:** {_video.PublishedAt:MMMM dd, yyyy}
|
||||
|
||||
---
|
||||
|
||||
**Channel Info**
|
||||
|
||||
- **Channel Name:** {_video.Channel}
|
||||
- **Subscribers:** {_video.SubscriberCount:N0}
|
||||
|
||||

|
||||
|
||||
[Visit Channel]({_video.ChannelUrl})
|
||||
|
||||
---
|
||||
|
||||
[Watch Video]({_video.Link})
|
||||
|
||||
---
|
||||
|
||||
_Last updated: {DateTime.Now:MMMM dd, yyyy}_
|
||||
_Data sourced via YouTube API_
|
||||
";
|
||||
|
||||
return new string[] { _markdown };
|
||||
}
|
||||
|
||||
private async Task<YouTubeVideo> FillInVideoDetailsAsync(YouTubeVideo video, string apiKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
|
||||
// Fetch channel details to get ChannelProfilePicUrl and SubscriberCount
|
||||
var channelUrl = $"https://www.googleapis.com/youtube/v3/channels?part=snippet,statistics&id={video.ChannelId}&key={apiKey}";
|
||||
var channelResponse = await httpClient.GetStringAsync(channelUrl);
|
||||
var channelData = JsonNode.Parse(channelResponse);
|
||||
|
||||
if (channelData?["items"]?.AsArray().Count > 0)
|
||||
{
|
||||
var channelSnippet = channelData?["items"]?[0]?["snippet"];
|
||||
var channelStatistics = channelData?["items"]?[0]?["statistics"];
|
||||
|
||||
// Update ChannelProfilePicUrl and SubscriberCount
|
||||
video.ChannelProfilePicUrl = channelSnippet?["thumbnails"]?["default"]?["url"]?.ToString() ?? string.Empty;
|
||||
video.SubscriberCount = long.TryParse(channelStatistics?["subscriberCount"]?.ToString(), out var subscribers) ? subscribers : 0;
|
||||
}
|
||||
|
||||
// Fetch video details to get ViewCount, LikeCount, and Captions (description)
|
||||
var videoUrl = $"https://www.googleapis.com/youtube/v3/videos?part=statistics,snippet&id={video.VideoId}&key={apiKey}";
|
||||
var videoResponse = await httpClient.GetStringAsync(videoUrl);
|
||||
var videoData = JsonNode.Parse(videoResponse);
|
||||
|
||||
if (videoData?["items"]?.AsArray().Count > 0)
|
||||
{
|
||||
var videoSnippet = videoData?["items"]?[0]?["snippet"];
|
||||
var videoStatistics = videoData?["items"]?[0]?["statistics"];
|
||||
|
||||
// Update ViewCount and LikeCount
|
||||
video.ViewCount = long.TryParse(videoStatistics?["viewCount"]?.ToString(), out var views) ? views : 0;
|
||||
video.LikeCount = long.TryParse(videoStatistics?["likeCount"]?.ToString(), out var likes) ? likes : 0;
|
||||
|
||||
// Update Captions (description)
|
||||
video.Caption = videoSnippet?["description"]?.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle errors
|
||||
Console.WriteLine($"An error occurred while fetching video details: {ex.Message}");
|
||||
}
|
||||
|
||||
return video;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,18 +13,16 @@ 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;
|
||||
using YouTubeExtension.Actions;
|
||||
|
||||
namespace YouTubeExtension;
|
||||
namespace YouTubeExtension.Pages;
|
||||
|
||||
internal sealed partial class YouTubeVideosPage : DynamicListPage
|
||||
{
|
||||
public YouTubeVideosPage()
|
||||
{
|
||||
Icon = new("https://www.youtube.com/favicon.ico");
|
||||
Name = "YouTube";
|
||||
Name = "YouTube (Video Search)";
|
||||
this.ShowDetails = true;
|
||||
}
|
||||
|
||||
@@ -42,15 +40,15 @@ internal sealed partial class YouTubeVideosPage : DynamicListPage
|
||||
var section = new ListSection()
|
||||
{
|
||||
Title = "Search Results",
|
||||
Items = items.Select(video => new ListItem(new OpenVideoLinkAction(video))
|
||||
Items = items.Select(video => new ListItem(new OpenVideoLinkAction(video.Link))
|
||||
{
|
||||
Title = video.Title,
|
||||
Subtitle = $"{video.Author}",
|
||||
Subtitle = $"{video.Channel}",
|
||||
Details = new Details()
|
||||
{
|
||||
Title = video.Title,
|
||||
HeroImage = new(video.ThumbnailUrl),
|
||||
Body = $"{video.Author}",
|
||||
Body = $"{video.Channel}",
|
||||
},
|
||||
Tags = [new Tag()
|
||||
{
|
||||
@@ -58,8 +56,8 @@ internal sealed partial class YouTubeVideosPage : DynamicListPage
|
||||
}
|
||||
],
|
||||
MoreCommands = [
|
||||
new CommandContextItem(new OpenChannelLinkAction(video)),
|
||||
new CommandContextItem(new YouTubeVideoInfoMarkdownPage()),
|
||||
new CommandContextItem(new OpenChannelLinkAction(video.ChannelUrl)),
|
||||
new CommandContextItem(new YouTubeVideoInfoMarkdownPage(video)),
|
||||
new CommandContextItem(new YouTubeAPIPage()),
|
||||
],
|
||||
}).ToArray(),
|
||||
@@ -77,7 +75,7 @@ internal sealed partial class YouTubeVideosPage : DynamicListPage
|
||||
|
||||
var videos = new List<YouTubeVideo>();
|
||||
|
||||
using (HttpClient client = new HttpClient())
|
||||
using HttpClient client = new HttpClient();
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -94,12 +92,12 @@ internal sealed partial class YouTubeVideosPage : DynamicListPage
|
||||
videos.Add(new YouTubeVideo
|
||||
{
|
||||
Title = item["snippet"]?["title"]?.ToString() ?? string.Empty,
|
||||
VideoId = item["id"]?["videoId"]?.ToString() ?? string.Empty,
|
||||
Link = $"https://www.youtube.com/watch?v={item["id"]?["videoId"]?.ToString()}",
|
||||
Author = item["snippet"]?["channelTitle"]?.ToString() ?? string.Empty,
|
||||
Channel = 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
|
||||
});
|
||||
}
|
||||
@@ -111,11 +109,7 @@ internal sealed partial class YouTubeVideosPage : DynamicListPage
|
||||
videos.Add(new YouTubeVideo
|
||||
{
|
||||
Title = "Error fetching data",
|
||||
Link = string.Empty,
|
||||
Author = $"Error: {ex.Message}",
|
||||
ThumbnailUrl = string.Empty,
|
||||
Captions = string.Empty,
|
||||
PublishedAt = DateTime.MinValue,
|
||||
Channel = $"Error: {ex.Message}",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using Windows.UI.ApplicationSettings;
|
||||
using YouTubeExtension.Helper;
|
||||
using YouTubeExtension.Actions;
|
||||
using YouTubeExtension.Pages;
|
||||
|
||||
namespace YouTubeExtension;
|
||||
@@ -24,8 +24,8 @@ public partial class YouTubeExtensionActionsProvider : ICommandProvider
|
||||
public IconDataType Icon => new(string.Empty);
|
||||
|
||||
private readonly IListItem[] _commands = [
|
||||
new ListItem(new YouTubeVideosPage()) { Title = "Search Videos", Subtitle = "YouTube" },
|
||||
new ListItem(new YouTubeChannelsPage()) { Title = "Search Channels", Subtitle = "YouTube" },
|
||||
new ListItem(new YouTubeVideosPage()) { Title = "Search Videos on YouTube", Subtitle = "YouTube" },
|
||||
new ListItem(new YouTubeChannelsPage()) { Title = "Search Channels on YouTube", Subtitle = "YouTube" },
|
||||
];
|
||||
|
||||
private readonly YouTubeAPIPage apiPage = new();
|
||||
|
||||
Reference in New Issue
Block a user