mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-11 23:06:45 +01:00
Merge pull request #69 from zadjii-msft/users/ethanfang/youtube-ext
YouTube Extension for Command Palette
This commit is contained in:
@@ -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.Actions;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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.Actions;
|
||||
|
||||
internal sealed partial class OpenChannelLinkAction : InvokableCommand
|
||||
{
|
||||
private readonly string _channelurl;
|
||||
|
||||
internal OpenChannelLinkAction(string url)
|
||||
{
|
||||
this._channelurl = url;
|
||||
this.Name = "Open channel";
|
||||
this.Icon = new("\uF131");
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(_channelurl) { UseShellExecute = true });
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
@@ -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.Actions;
|
||||
|
||||
internal sealed partial class OpenVideoLinkAction : InvokableCommand
|
||||
{
|
||||
private readonly string _videourl;
|
||||
|
||||
internal OpenVideoLinkAction(string url)
|
||||
{
|
||||
this._videourl = url;
|
||||
this.Name = "Open video";
|
||||
this.Icon = new("\uE714");
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(_videourl) { UseShellExecute = true });
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
src/modules/cmdpal/Exts/YouTubeExtension/Assets/StoreLogo.png
Normal file
BIN
src/modules/cmdpal/Exts/YouTubeExtension/Assets/StoreLogo.png
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YouTubeExtension.Actions;
|
||||
|
||||
internal sealed class YouTubeHelper
|
||||
{
|
||||
internal static string StateJsonPath()
|
||||
{
|
||||
// Get the path to our exe
|
||||
var path = System.Reflection.Assembly.GetExecutingAssembly().Location;
|
||||
|
||||
// Get the directory of the exe
|
||||
var directory = Path.GetDirectoryName(path) ?? string.Empty;
|
||||
|
||||
// now, the state is just next to the exe
|
||||
return Path.Combine(directory, "state.json");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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 YouTubeVideo
|
||||
{
|
||||
// The title of the video
|
||||
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; set; } = string.Empty;
|
||||
|
||||
// The author or channel name of the video
|
||||
public string Channel { get; set; } = string.Empty;
|
||||
|
||||
// The channel id (needed for the channel URL)
|
||||
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 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; set; } = string.Empty;
|
||||
|
||||
// Captions or subtitles associated with the video
|
||||
public string Caption { get; set; } = string.Empty;
|
||||
|
||||
// The date and time the video was published
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
|
||||
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
IgnorableNamespaces="uap uap3 rescap">
|
||||
|
||||
<Identity
|
||||
Name="YouTubeExtension"
|
||||
Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
|
||||
Version="0.0.1.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>YouTube extension for cmdpal</DisplayName>
|
||||
<PublisherDisplayName>A Lone Developer</PublisherDisplayName>
|
||||
<Logo>Assets\StoreLogo.png</Logo>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="x-generate"/>
|
||||
</Resources>
|
||||
|
||||
<Applications>
|
||||
<Application Id="App"
|
||||
Executable="$targetnametoken$.exe"
|
||||
EntryPoint="$targetentrypoint$">
|
||||
<uap:VisualElements
|
||||
DisplayName="YouTube extension for cmdpal"
|
||||
Description="YouTube extension for cmdpal"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||
Square44x44Logo="Assets\Square44x44Logo.png">
|
||||
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
|
||||
<uap:SplashScreen Image="Assets\SplashScreen.png" />
|
||||
</uap:VisualElements>
|
||||
<Extensions>
|
||||
<com:Extension Category="windows.comServer">
|
||||
<com:ComServer>
|
||||
<com:ExeServer Executable="YouTubeExtension.exe" Arguments="-RegisterProcessAsComServer" DisplayName="ClementineExtensionApp">
|
||||
<com:Class Id="95696eff-5c44-41ad-8f64-c2182ec9e3fd" DisplayName="YouTube extension for cmdpal" />
|
||||
</com:ExeServer>
|
||||
</com:ComServer>
|
||||
</com:Extension>
|
||||
<uap3:Extension Category="windows.appExtension">
|
||||
<uap3:AppExtension Name="com.microsoft.windows.commandpalette"
|
||||
Id="PG-SP-ID"
|
||||
PublicFolder="Public"
|
||||
DisplayName="YouTube extension for cmdpal"
|
||||
Description="YouTube extension for cmdpal">
|
||||
<uap3:Properties>
|
||||
<CmdPalProvider>
|
||||
<Activation>
|
||||
<CreateInstance ClassId="95696eff-5c44-41ad-8f64-c2182ec9e3fd" />
|
||||
</Activation>
|
||||
<SupportedInterfaces>
|
||||
<Commands/>
|
||||
</SupportedInterfaces>
|
||||
</CmdPalProvider>
|
||||
</uap3:Properties>
|
||||
</uap3:AppExtension>
|
||||
</uap3:Extension>
|
||||
</Extensions>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
<Capabilities>
|
||||
<Capability Name="internetClient" />
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
</Capabilities>
|
||||
</Package>
|
||||
@@ -0,0 +1,76 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using YouTubeExtension.Actions;
|
||||
|
||||
namespace YouTubeExtension.Pages;
|
||||
|
||||
internal sealed partial class YouTubeAPIForm : Form
|
||||
{
|
||||
public override string TemplateJson()
|
||||
{
|
||||
var json = $$"""
|
||||
{
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"type": "AdaptiveCard",
|
||||
"version": "1.5",
|
||||
"body": [
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"style": "password",
|
||||
"id": "apiKey",
|
||||
"label": "API Key",
|
||||
"isRequired": true,
|
||||
"errorMessage": "API Key required"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "Save",
|
||||
"data": {
|
||||
"apiKey": "apiKey"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
return json;
|
||||
}
|
||||
|
||||
public override string DataJson() => throw new NotImplementedException();
|
||||
|
||||
public override string StateJson() => throw new NotImplementedException();
|
||||
|
||||
public override CommandResult SubmitForm(string payload)
|
||||
{
|
||||
var formInput = JsonNode.Parse(payload);
|
||||
if (formInput == null)
|
||||
{
|
||||
return CommandResult.GoHome();
|
||||
}
|
||||
|
||||
// get the name and url out of the values
|
||||
var formApiKey = formInput["apiKey"] ?? string.Empty;
|
||||
|
||||
// Construct a new json blob with the name and url
|
||||
var json = $$"""
|
||||
{
|
||||
"apiKey": "{{formApiKey}}"
|
||||
}
|
||||
""";
|
||||
|
||||
File.WriteAllText(YouTubeHelper.StateJsonPath(), json);
|
||||
return CommandResult.GoHome();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
|
||||
namespace YouTubeExtension.Pages;
|
||||
|
||||
internal sealed partial class YouTubeAPIPage : FormPage
|
||||
{
|
||||
private readonly YouTubeAPIForm apiForm = new();
|
||||
|
||||
public override IForm[] Forms() => [apiForm];
|
||||
|
||||
public YouTubeAPIPage()
|
||||
{
|
||||
Name = "Edit YouTube API Key";
|
||||
Icon = new("https://www.youtube.com/favicon.ico");
|
||||
}
|
||||
}
|
||||
@@ -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 = $"Search for Videos by {channelName ?? "Channel"}";
|
||||
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=20";
|
||||
}
|
||||
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=20";
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// 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 YouTubeChannelsPage : DynamicListPage
|
||||
{
|
||||
public YouTubeChannelsPage()
|
||||
{
|
||||
Icon = new("https://www.youtube.com/favicon.ico");
|
||||
Name = "YouTube (Channel Search)";
|
||||
this.ShowDetails = true;
|
||||
}
|
||||
|
||||
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 channels based on the query
|
||||
List<YouTubeChannel> items = await GetYouTubeChannels(query);
|
||||
|
||||
// Create a section and populate it with the channel results
|
||||
var section = new ListSection()
|
||||
{
|
||||
Title = "Search Results",
|
||||
Items = items.Select(channel => new ListItem(new OpenChannelLinkAction(channel.ChannelUrl))
|
||||
{
|
||||
Title = channel.Name,
|
||||
Subtitle = $"{channel.SubscriberCount} subscribers",
|
||||
Details = new Details()
|
||||
{
|
||||
Title = channel.Name,
|
||||
HeroImage = new(channel.ProfilePicUrl),
|
||||
Body = $"Subscribers: {channel.SubscriberCount}\nChannel Description: {channel.Description}",
|
||||
},
|
||||
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 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 channels = new List<YouTubeChannel>();
|
||||
|
||||
using (HttpClient client = new HttpClient())
|
||||
{
|
||||
try
|
||||
{
|
||||
// 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=20");
|
||||
var json = JsonNode.Parse(response);
|
||||
|
||||
// Parse the response
|
||||
if (json?["items"] is JsonArray itemsArray)
|
||||
{
|
||||
foreach (var item in itemsArray)
|
||||
{
|
||||
// Add each channel to the list with channel details
|
||||
channels.Add(new YouTubeChannel
|
||||
{
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle any errors from the API call or parsing
|
||||
channels.Add(new YouTubeChannel
|
||||
{
|
||||
Name = "Error fetching data",
|
||||
Description = $"Error: {ex.Message}",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// 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 Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using YouTubeExtension.Actions;
|
||||
|
||||
namespace YouTubeExtension.Pages;
|
||||
|
||||
internal sealed partial class YouTubeVideoInfoMarkdownPage : MarkdownPage
|
||||
{
|
||||
private readonly YouTubeVideo _video;
|
||||
private string _markdown = string.Empty;
|
||||
|
||||
public YouTubeVideoInfoMarkdownPage(YouTubeVideo video)
|
||||
{
|
||||
Icon = new("\uE946");
|
||||
Name = "See more information about this video";
|
||||
_video = video;
|
||||
}
|
||||
|
||||
public override string[] Bodies()
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// 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.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using YouTubeExtension.Actions;
|
||||
|
||||
namespace YouTubeExtension.Pages;
|
||||
|
||||
internal sealed partial class YouTubeVideosPage : DynamicListPage
|
||||
{
|
||||
public YouTubeVideosPage()
|
||||
{
|
||||
Icon = new("https://www.youtube.com/favicon.ico");
|
||||
Name = "YouTube (Video Search)";
|
||||
this.ShowDetails = true;
|
||||
}
|
||||
|
||||
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 based on the query
|
||||
List<YouTubeVideo> 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.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 YouTube API
|
||||
private static async Task<List<YouTubeVideo>> 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<YouTubeVideo>();
|
||||
|
||||
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=20");
|
||||
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;
|
||||
}
|
||||
}
|
||||
36
src/modules/cmdpal/Exts/YouTubeExtension/Program.cs
Normal file
36
src/modules/cmdpal/Exts/YouTubeExtension/Program.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
// 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.Threading;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
|
||||
namespace YouTubeExtension;
|
||||
|
||||
public class Program
|
||||
{
|
||||
[MTAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer")
|
||||
{
|
||||
using ExtensionServer server = new();
|
||||
var extensionDisposedEvent = new ManualResetEvent(false);
|
||||
var extensionInstance = new SampleExtension(extensionDisposedEvent);
|
||||
|
||||
// We are instantiating an extension instance once above, and returning it every time the callback in RegisterExtension below is called.
|
||||
// This makes sure that only one instance of SampleExtension is alive, which is returned every time the host asks for the IExtension object.
|
||||
// If you want to instantiate a new instance each time the host asks, create the new instance inside the delegate.
|
||||
server.RegisterExtension(() => extensionInstance);
|
||||
|
||||
// This will make the main thread wait until the event is signalled by the extension class.
|
||||
// Since we have single instance of the extension object, we exit as sooon as it is disposed.
|
||||
extensionDisposedEvent.WaitOne();
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Not being launched as a Extension... exiting.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"profiles": {
|
||||
"YouTubeExtension (Package)": {
|
||||
"commandName": "MsixPackage",
|
||||
"commandLineArgs": "-RegisterProcessAsComServer"
|
||||
},
|
||||
"YouTubeExtension (Unpackaged)": {
|
||||
"commandName": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/modules/cmdpal/Exts/YouTubeExtension/SampleExtension.cs
Normal file
39
src/modules/cmdpal/Exts/YouTubeExtension/SampleExtension.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
// 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.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
|
||||
namespace YouTubeExtension;
|
||||
|
||||
[ComVisible(true)]
|
||||
[Guid("95696eff-5c44-41ad-8f64-c2182ec9e3fd")]
|
||||
[ComDefaultInterface(typeof(IExtension))]
|
||||
public sealed partial class SampleExtension : IExtension
|
||||
{
|
||||
private readonly ManualResetEvent _extensionDisposedEvent;
|
||||
|
||||
public SampleExtension(ManualResetEvent extensionDisposedEvent)
|
||||
{
|
||||
this._extensionDisposedEvent = extensionDisposedEvent;
|
||||
}
|
||||
|
||||
public object GetProvider(ProviderType providerType)
|
||||
{
|
||||
switch (providerType)
|
||||
{
|
||||
case ProviderType.Commands:
|
||||
return new YouTubeExtensionActionsProvider();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._extensionDisposedEvent.Set();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>YouTubeExtension</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
|
||||
<UseWinUI>false</UseWinUI>
|
||||
<EnableMsixTooling>true</EnableMsixTooling>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Assets\SplashScreen.scale-200.png" />
|
||||
<Content Include="Assets\LockScreenLogo.scale-200.png" />
|
||||
<Content Include="Assets\Square150x150Logo.scale-200.png" />
|
||||
<Content Include="Assets\Square44x44Logo.scale-200.png" />
|
||||
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
|
||||
<Content Include="Assets\StoreLogo.png" />
|
||||
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Manifest Include="$(ApplicationManifest)" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\extensionsdk\Microsoft.CmdPal.Extensions.Helpers\Microsoft.CmdPal.Extensions.Helpers.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
|
||||
Tools extension to be activated for this project even if the Windows App SDK Nuget
|
||||
package has not yet been restored.
|
||||
-->
|
||||
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<ProjectCapability Include="Msix" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Data\" />
|
||||
<Folder Include="Forms\" />
|
||||
<Folder Include="Commands\" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
|
||||
Explorer "Package and Publish" context menu entry to be enabled for this project even if
|
||||
the Windows App SDK Nuget package has not yet been restored.
|
||||
-->
|
||||
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,135 @@
|
||||
// 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.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Extensions;
|
||||
using Microsoft.CmdPal.Extensions.Helpers;
|
||||
using Windows.UI.ApplicationSettings;
|
||||
using YouTubeExtension.Actions;
|
||||
using YouTubeExtension.Pages;
|
||||
|
||||
namespace YouTubeExtension;
|
||||
|
||||
public partial class YouTubeExtensionActionsProvider : ICommandProvider
|
||||
{
|
||||
public string DisplayName => $"YouTube";
|
||||
|
||||
public IconDataType Icon => new(string.Empty);
|
||||
|
||||
private readonly IListItem[] _commands = [
|
||||
new ListItem(new YouTubeVideosPage())
|
||||
{
|
||||
Title = "Search Videos on YouTube",
|
||||
Subtitle = "YouTube",
|
||||
Tags = [new Tag()
|
||||
{
|
||||
Text = "Extension",
|
||||
}
|
||||
],
|
||||
},
|
||||
new ListItem(new YouTubeChannelsPage())
|
||||
{
|
||||
Title = "Search Channels on YouTube",
|
||||
Subtitle = "YouTube",
|
||||
Tags = [new Tag()
|
||||
{
|
||||
Text = "Extension",
|
||||
}
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
private readonly YouTubeAPIPage apiPage = new();
|
||||
|
||||
#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize
|
||||
public void Dispose() => throw new NotImplementedException();
|
||||
#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize
|
||||
|
||||
public IListItem[] TopLevelCommands()
|
||||
{
|
||||
return TopLevelCommandsAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<IListItem[]> TopLevelCommandsAsync()
|
||||
{
|
||||
var settingsPath = YouTubeHelper.StateJsonPath();
|
||||
|
||||
// Check if the settings file exists
|
||||
if (!File.Exists(settingsPath))
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new ListItem(apiPage)
|
||||
{
|
||||
Title = "YouTube Extension",
|
||||
Subtitle = "Enter your API key.",
|
||||
Tags = [new Tag()
|
||||
{
|
||||
Text = "Extension",
|
||||
}
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Read the file and parse the API key
|
||||
var state = File.ReadAllText(settingsPath);
|
||||
var jsonState = System.Text.Json.Nodes.JsonNode.Parse(state);
|
||||
var apiKey = jsonState?["apiKey"]?.ToString() ?? string.Empty;
|
||||
|
||||
// Validate the API key using YouTube API
|
||||
if (string.IsNullOrWhiteSpace(apiKey) || !await IsApiKeyValid(apiKey))
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new ListItem(apiPage)
|
||||
{
|
||||
Title = "YouTube Extension",
|
||||
Subtitle = "Enter your API key.",
|
||||
Tags = [new Tag()
|
||||
{
|
||||
Text = "Extension",
|
||||
}
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// If file exists and API key is valid, return commands
|
||||
return _commands;
|
||||
}
|
||||
|
||||
// Method to check if the API key is valid by making a simple request to the YouTube API
|
||||
private static async Task<bool> IsApiKeyValid(string apiKey)
|
||||
{
|
||||
using HttpClient client = new HttpClient();
|
||||
{
|
||||
try
|
||||
{
|
||||
// Make a simple request to verify the API key, such as fetching a video
|
||||
var response = await client.GetAsync($"https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&q=test&key={apiKey}");
|
||||
|
||||
// If the response status code is 200, the API key is valid
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Optionally, handle other status codes and log errors
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If any exception occurs (e.g., network error), consider the API key invalid
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/modules/cmdpal/Exts/YouTubeExtension/app.manifest
Normal file
19
src/modules/cmdpal/Exts/YouTubeExtension/app.manifest
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="HackerNewsExtension.app"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
|
||||
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
|
||||
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -42,6 +42,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SSHKeychainExtension", "ext
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SamplePagesExtension", "exts\SamplePagesExtension\SamplePagesExtension.csproj", "{399B53F1-5AA6-4FE0-8C3C-66E07B84E6F1}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YouTubeExtension", "Exts\YouTubeExtension\YouTubeExtension.csproj", "{276243F6-4B25-411C-B110-1E7DB575F13D}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.UI", "Microsoft.CmdPal.UI\Microsoft.CmdPal.UI.csproj", "{1DF70F56-ABB2-4798-BBA5-0B9568715BA1}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.UI.ViewModels", "Microsoft.CmdPal.UI.ViewModels\Microsoft.CmdPal.UI.ViewModels.csproj", "{D2FC419D-0ABC-425F-9D43-A7782AC4A0AE}"
|
||||
@@ -206,6 +207,18 @@ Global
|
||||
{399B53F1-5AA6-4FE0-8C3C-66E07B84E6F1}.Release|x64.ActiveCfg = Release|x64
|
||||
{399B53F1-5AA6-4FE0-8C3C-66E07B84E6F1}.Release|x64.Build.0 = Release|x64
|
||||
{399B53F1-5AA6-4FE0-8C3C-66E07B84E6F1}.Release|x64.Deploy.0 = Release|x64
|
||||
{276243F6-4B25-411C-B110-1E7DB575F13D}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{276243F6-4B25-411C-B110-1E7DB575F13D}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{276243F6-4B25-411C-B110-1E7DB575F13D}.Debug|ARM64.Deploy.0 = Debug|ARM64
|
||||
{276243F6-4B25-411C-B110-1E7DB575F13D}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{276243F6-4B25-411C-B110-1E7DB575F13D}.Debug|x64.Build.0 = Debug|x64
|
||||
{276243F6-4B25-411C-B110-1E7DB575F13D}.Debug|x64.Deploy.0 = Debug|x64
|
||||
{276243F6-4B25-411C-B110-1E7DB575F13D}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{276243F6-4B25-411C-B110-1E7DB575F13D}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{276243F6-4B25-411C-B110-1E7DB575F13D}.Release|ARM64.Deploy.0 = Release|ARM64
|
||||
{276243F6-4B25-411C-B110-1E7DB575F13D}.Release|x64.ActiveCfg = Release|x64
|
||||
{276243F6-4B25-411C-B110-1E7DB575F13D}.Release|x64.Build.0 = Release|x64
|
||||
{276243F6-4B25-411C-B110-1E7DB575F13D}.Release|x64.Deploy.0 = Release|x64
|
||||
{1DF70F56-ABB2-4798-BBA5-0B9568715BA1}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{1DF70F56-ABB2-4798-BBA5-0B9568715BA1}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{1DF70F56-ABB2-4798-BBA5-0B9568715BA1}.Debug|ARM64.Deploy.0 = Debug|ARM64
|
||||
@@ -255,6 +268,7 @@ Global
|
||||
{7F6796A4-4233-4CEC-914F-95EC7A5283A0} = {272D0E9A-8FC3-49F5-8FAD-79ABAE8AB1E4}
|
||||
{77D99BE0-F69C-4F27-8153-951CEC5110FE} = {B7FF739F-7716-4FC3-B622-705486187B87}
|
||||
{399B53F1-5AA6-4FE0-8C3C-66E07B84E6F1} = {B7FF739F-7716-4FC3-B622-705486187B87}
|
||||
{276243F6-4B25-411C-B110-1E7DB575F13D} = {B7FF739F-7716-4FC3-B622-705486187B87}
|
||||
{1DF70F56-ABB2-4798-BBA5-0B9568715BA1} = {865E9369-C53A-40B8-829C-F7843F66D3A1}
|
||||
{D2FC419D-0ABC-425F-9D43-A7782AC4A0AE} = {865E9369-C53A-40B8-829C-F7843F66D3A1}
|
||||
{2012F5FE-BF53-4E51-868D-0E5A74C11878} = {272D0E9A-8FC3-49F5-8FAD-79ABAE8AB1E4}
|
||||
|
||||
Reference in New Issue
Block a user