Directory.Packages.props
+4
-0
diff --git a/Directory.Packages.props b/Directory.Packages.props
index a9d4f33..559a4c4 100644
@@ -9,6 +9,8 @@
<PrivateAssets>all</PrivateAssets>
</GlobalPackageReference>
<PackageVersion Include="FFMpegCore" Version="5.4.0" />
<PackageVersion Include="Google.Apis.Auth.AspNetCore3" Version="1.74.0" />
<PackageVersion Include="Google.Apis.YouTube.v3" Version="1.74.0.4137" />
<PackageVersion Include="Jellyfin.Database.Implementations" Version="10.11.8" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="11.0.0-preview.3.26207.106" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
@@ -37,6 +39,7 @@
Include="Microsoft.Extensions.Options.ConfigurationExtensions"
Version="11.0.0-preview.3.26207.106"
/>
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="5.7.0" />
<PackageVersion Include="OllamaSharp" Version="5.4.25" />
<PackageVersion Include="OpenTelemetry" Version="1.15.3" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
@@ -52,6 +55,7 @@
<PackageVersion Include="Quartz" Version="3.18.1" />
<PackageVersion Include="Quartz.AspNetCore" Version="3.18.1" />
<PackageVersion Include="SubtitlesParserV2" Version="2.4.0" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="5.7.0" />
<PackageVersion Include="Winton.Extensions.Configuration.Consul" Version="3.4.0" />
</ItemGroup>
</Project>
Slopper.slnx
+1
-0
diff --git a/Slopper.slnx b/Slopper.slnx
index 1fde094..eac7134 100644
@@ -8,5 +8,6 @@
<Project Path="src/Infrastructure/Ai/Ai.csproj" />
<Project Path="src/Infrastructure/Database/Database.csproj" />
<Project Path="src/Infrastructure/Ffmpeg/Ffmpeg.csproj" />
<Project Path="src/Infrastructure/YouTube/YouTube.csproj" />
</Folder>
</Solution>
src/Api/Api.csproj
+2
-0
diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj
index 7184c08..71080d5 100644
@@ -9,8 +9,10 @@
<ProjectReference Include="..\Infrastructure\Ai\Ai.csproj" />
<ProjectReference Include="..\Infrastructure\Database\Database.csproj" />
<ProjectReference Include="..\Infrastructure\Ffmpeg\Ffmpeg.csproj" />
<ProjectReference Include="..\Infrastructure\YouTube\YouTube.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google.Apis.Auth.AspNetCore3" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
src/Api/Program.cs
+54
-11
diff --git a/src/Api/Program.cs b/src/Api/Program.cs
index 0582857..82dcce8 100644
@@ -1,24 +1,33 @@
using System.Linq;
using System.Threading;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Quartz;
using Quartz.AspNetCore;
using Slopper.Api;
using Slopper.Api.YouTubeAuth;
using Slopper.Domain;
using Slopper.Infrastructure.Ai;
using Slopper.Infrastructure.Database;
using Slopper.Infrastructure.Ffmpeg;
using Winton.Extensions.Configuration.Consul;
using Slopper.Infrastructure.YouTube;
// using Winton.Extensions.Configuration.Consul;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddConsul(
"slopper",
options =>
{
options.ConsulConfigurationOptions = options => options.Address = new(builder.Configuration["Consul:Address"]!);
options.ReloadOnChange = true;
}
);
// builder.Configuration.AddConsul(
// "slopper",
// options =>
// {
// options.ConsulConfigurationOptions = options => options.Address = new(builder.Configuration["Consul:Address"]!);
// options.ReloadOnChange = true;
// }
// );
builder.ConfigureOpenTelemetry();
@@ -26,15 +35,27 @@ builder.Services.AddOpenApi();
builder.Services.AddClipSelector().AddClipGenerator().AddCleaner();
builder.Services.AddJellyfinDatabase().AddSlopperDatabase().AddFfmpegServices().AddAi();
builder.Services.AddJellyfinDatabase().AddSlopperDatabase().AddFfmpegServices().AddAi().AddYouTubeUploader();
builder
.Services.AddClipGenerationJobOptions()
.AddQuartz(quartz => quartz.AddClipGenerationJob().AddCleanupJob())
.AddQuartz() //quartz => quartz.AddClipGenerationJob().AddCleanupJob())
.AddQuartzServer();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie().AddYouTube();
builder.Services.AddAuthorizationBuilder().AddYouTubePolicy();
using var app = builder.Build();
if (builder.Environment.IsDevelopment())
{
app.UseCookiePolicy(new() { MinimumSameSitePolicy = SameSiteMode.Lax });
}
app.UseAuthentication();
app.UseAuthorization();
app.MapOpenApi();
app.MapApi();
@@ -42,4 +63,26 @@ app.MapApi();
app.UseDefaultFiles();
app.UseStaticFiles();
app.MapGet(
"/admin/youtube",
async (
[FromServices] IClipRepository clipRepository,
[FromKeyedServices("YouTube")] IUploader uploader,
HttpContext httpContext,
CancellationToken cancellationToken
) =>
{
var clip = await clipRepository
.GetLatest(limit: 1, cancellationToken: cancellationToken)
.FirstOrDefaultAsync(cancellationToken);
if (clip is null)
{
return Results.NotFound();
}
var result = await uploader.Upload(clip, cancellationToken);
return Results.Ok(result.CanonicalUrl);
}
)
.RequireAuthorization("YouTube");
app.Run();
src/Api/YouTubeAuth/YouTubeAuthExtensions.cs
+49
-0
diff --git a/src/Api/YouTubeAuth/YouTubeAuthExtensions.cs b/src/Api/YouTubeAuth/YouTubeAuthExtensions.cs
new file mode 100644
index 0000000..f7fa2c8
@@ -0,0 +1,49 @@
using Google.Apis.Auth.AspNetCore3;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Slopper.Infrastructure.YouTube;
namespace Slopper.Api.YouTubeAuth;
public static class YouTubeAuthExtensions
{
extension(AuthenticationBuilder auth)
{
public AuthenticationBuilder AddYouTube()
{
auth.AddGoogleOpenIdConnect(
GoogleOpenIdConnectDefaults.AuthenticationScheme,
options =>
{
options.CallbackPath = "/admin/redirect/youtube";
options.ResponseMode = OpenIdConnectResponseMode.Query;
}
);
auth.Services.AddOptions<OpenIdConnectOptions>(GoogleOpenIdConnectDefaults.AuthenticationScheme)
.BindConfiguration("YouTube");
auth.Services.AddTransient<IAuthorizationHandler, YouTubeScopeAuthorizationHandler>();
auth.Services.AddTransient<IYouTubeCredentialsProvider, YouTubeCredentialsProvider>();
return auth;
}
}
extension(AuthorizationBuilder auth)
{
public AuthorizationBuilder AddYouTubePolicy() =>
auth.AddPolicy(
"YouTube",
policy =>
policy
.AddAuthenticationSchemes(GoogleOpenIdConnectDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.AddRequirements(new YouTubeScopeRequirement("https://www.googleapis.com/auth/youtube.upload"))
);
}
}
src/Api/YouTubeAuth/YouTubeCredentialsProvider.cs
+13
-0
diff --git a/src/Api/YouTubeAuth/YouTubeCredentialsProvider.cs b/src/Api/YouTubeAuth/YouTubeCredentialsProvider.cs
new file mode 100644
index 0000000..65fd66e
@@ -0,0 +1,13 @@
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Auth.AspNetCore3;
using Google.Apis.Auth.OAuth2;
using Slopper.Infrastructure.YouTube;
namespace Slopper.Api.YouTubeAuth;
internal sealed class YouTubeCredentialsProvider(IGoogleAuthProvider googleAuthProvider) : IYouTubeCredentialsProvider
{
public async Task<ICredential> GetCredentials(CancellationToken cancellationToken) =>
await googleAuthProvider.GetCredentialAsync(cancellationToken: cancellationToken);
}
src/Api/YouTubeAuth/YouTubeScopeAuthorizationHandler.cs
+40
-0
diff --git a/src/Api/YouTubeAuth/YouTubeScopeAuthorizationHandler.cs b/src/Api/YouTubeAuth/YouTubeScopeAuthorizationHandler.cs
new file mode 100644
index 0000000..d36c494
@@ -0,0 +1,40 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Slopper.Api.YouTubeAuth;
internal sealed class YouTubeScopeAuthorizationHandler(IHttpContextAccessor httpContextAccessor)
: AuthorizationHandler<YouTubeScopeRequirement>
{
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
YouTubeScopeRequirement requirement
)
{
var httpContext = httpContextAccessor.HttpContext!;
var auth = await httpContext.AuthenticateAsync();
if (!auth.Succeeded || auth.None)
{
httpContext.Items["ScopeIncremental"] = string.Join(" ", requirement.Scopes);
return;
}
var existingScopes =
auth.Properties?.Items.TryGetValue("scope", out var existingScope) is true && existingScope is not null
? existingScope.Split(" ", StringSplitOptions.RemoveEmptyEntries)
: [];
var additionalScopes = requirement.Scopes.Except(existingScopes).ToArray();
if (additionalScopes is not [])
{
httpContext.Items["ScopeIncremental"] = string.Join(" ", additionalScopes);
return;
}
context.Succeed(requirement);
}
}
src/Api/YouTubeAuth/YouTubeScopeRequirement.cs
+6
-0
diff --git a/src/Api/YouTubeAuth/YouTubeScopeRequirement.cs b/src/Api/YouTubeAuth/YouTubeScopeRequirement.cs
new file mode 100644
index 0000000..bdbd1eb
@@ -0,0 +1,6 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
namespace Slopper.Api.YouTubeAuth;
internal sealed record YouTubeScopeRequirement(params IReadOnlyCollection<string> Scopes) : IAuthorizationRequirement;
src/Api/appsettings.Development.json
+5
-0
diff --git a/src/Api/appsettings.Development.json b/src/Api/appsettings.Development.json
index 7250c32..11124e7 100644
@@ -38,5 +38,10 @@
"Love means never having to say you're sorry",
"There's no place like home"
]
},
"YouTube": {
"Scopes": [
"YoutubeUpload"
]
}
}
src/Api/packages.lock.json
+180
-2
diff --git a/src/Api/packages.lock.json b/src/Api/packages.lock.json
index 940f8e5..48abddc 100644
@@ -8,6 +8,16 @@
"resolved": "1.2.6",
"contentHash": "KMSJG+jfk7vjP52QkWB99qWespXCPAzG/IaMCMRHYWumJEAGKQYm2HtyWG6eqnOwDitH96i1cqq5EVesyOtPmg=="
},
"Google.Apis.Auth.AspNetCore3": {
"type": "Direct",
"requested": "[1.74.0, )",
"resolved": "1.74.0",
"contentHash": "O1vwlUCodsvT1bt1kYgpXTScAInY7zynSm+iHRpBHDUOXWuy2sghuabWgyBMv18q7ENpZfaqWBhiLiMhSAUmxA==",
"dependencies": {
"Google.Apis.Auth": "1.74.0",
"Microsoft.AspNetCore.Authentication.OpenIdConnect": "3.0.3"
}
},
"Microsoft.AspNetCore.OpenApi": {
"type": "Direct",
"requested": "[11.0.0-preview.3.26207.106, )",
@@ -113,11 +123,45 @@
"Newtonsoft.Json": "13.0.1"
}
},
"Google.Apis": {
"type": "Transitive",
"resolved": "1.74.0",
"contentHash": "Z6hSkmRD6V8/raSBqn/EsPpNop6suaLaGE9DbSY3yRE5G47l4zYqrJgapcLARk2Z3TsQUDmWz+w/tob3wuVt3g==",
"dependencies": {
"Google.Apis.Core": "1.74.0"
}
},
"Google.Apis.Auth": {
"type": "Transitive",
"resolved": "1.74.0",
"contentHash": "43H4foh3zfWgboK8QnJjIfPQuBiS794TxqQLDfeE3erQSluKmEN0qeDuwxLTpSr9Mtwj76S4LWxPneuSbC6fNg==",
"dependencies": {
"Google.Apis": "1.74.0",
"Google.Apis.Core": "1.74.0",
"System.Management": "7.0.2"
}
},
"Google.Apis.Core": {
"type": "Transitive",
"resolved": "1.74.0",
"contentHash": "gWOIiks6yo2TRliIyy10V18BJOO2BfUSPJFiTW6Tfap4tm3hFj7nvYU3XIfwzAxwoIy6nsBoqVDt9iBDSXrvYQ==",
"dependencies": {
"Newtonsoft.Json": "13.0.4"
}
},
"Instances": {
"type": "Transitive",
"resolved": "3.0.2",
"contentHash": "LfhegDpmA8PuHW58RmgVvCDG/mfVCTU+Vhy4ppmXLJfAer33Xl0NocDy92OwSL6CnkVdx41O/I0+BjNhU1JtMQ=="
},
"Microsoft.AspNetCore.Authentication.OpenIdConnect": {
"type": "Transitive",
"resolved": "3.0.3",
"contentHash": "Ovzy0Yd3cO7xeDuams4lTHW4z+3OHU0yyBERdi9qOLcBnbtz5rQqtIdPpog20jhZWMX2AcA9HB4mCUqPHyLz2w==",
"dependencies": {
"Microsoft.IdentityModel.Protocols.OpenIdConnect": "5.5.0"
}
},
"Microsoft.Data.Sqlite.Core": {
"type": "Transitive",
"resolved": "10.0.7",
@@ -387,6 +431,39 @@
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "IBOlwyX13ax6/fXA7AoZFswKFytta9TExBv3/8qemMJGBoDXYlQEcw4WerHQCvmerJ5uP2o8bjIAvxcNdTZVLQ=="
},
"Microsoft.IdentityModel.Logging": {
"type": "Transitive",
"resolved": "5.7.0",
"contentHash": "ciNPEQSnOXlhLBjylS2B8QLQhmWFXBAzbKyxpswLd9NHheCKoyC1zFU46W36Oj/Bx+RpYuOWkq1FZgnu8DqH2g=="
},
"Microsoft.IdentityModel.Protocols": {
"type": "Transitive",
"resolved": "5.5.0",
"contentHash": "m1gwAQwZjUxzRBC+4H40vYSo9Cms9yUbMdW492rQoXHU77G/ItiKxpk2+W9bWYcdsKUDKudye7im3T3MlVxEkg==",
"dependencies": {
"Microsoft.IdentityModel.Logging": "5.5.0",
"Microsoft.IdentityModel.Tokens": "5.5.0"
}
},
"Microsoft.IdentityModel.Protocols.OpenIdConnect": {
"type": "Transitive",
"resolved": "5.5.0",
"contentHash": "21F4QlbaD5CXNs2urNRCO6vljbbrhv3gmGT8P18SKGKZ9IYBCn29extoJriHiPfhABd5b8S7RcdKU50XhERkYg==",
"dependencies": {
"Microsoft.IdentityModel.Protocols": "5.5.0",
"Newtonsoft.Json": "10.0.1",
"System.IdentityModel.Tokens.Jwt": "5.5.0"
}
},
"Microsoft.IdentityModel.Tokens": {
"type": "Transitive",
"resolved": "5.7.0",
"contentHash": "hj7eUanB5f8ap9e4I+iVY0Fbm9Uh0tG1UlZq3Z+EGq9nbDUOn8ml3N5YotNQX6XD0q/Upe16tt0jkAIVIWKrWg==",
"dependencies": {
"Microsoft.IdentityModel.Logging": "5.7.0",
"Newtonsoft.Json": "13.0.1"
}
},
"Microsoft.OpenApi": {
"type": "Transitive",
"resolved": "3.3.1",
@@ -394,8 +471,8 @@
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.1",
"contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A=="
"resolved": "13.0.4",
"contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A=="
},
"OpenTelemetry.Api": {
"type": "Transitive",
@@ -466,11 +543,24 @@
"SQLitePCLRaw.core": "2.1.11"
}
},
"System.CodeDom": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A=="
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "dcvsAEx7YxeuSfzj5QOx8B1npA+3WuI25nLb9IscNyvgGB1cDmliDE9WRlwJvGaHhCBjqQO0G4dyB+3V7BLFzA=="
},
"System.Management": {
"type": "Transitive",
"resolved": "7.0.2",
"contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==",
"dependencies": {
"System.CodeDom": "7.0.0"
}
},
"System.Numerics.Tensors": {
"type": "Transitive",
"resolved": "10.0.6",
@@ -514,6 +604,15 @@
"SubtitlesParserV2": "[2.4.0, )"
}
},
"Slopper.Infrastructure.YouTube": {
"type": "Project",
"dependencies": {
"Google.Apis.YouTube.v3": "[1.74.0.4137, )",
"Microsoft.Extensions.Options": "[11.0.0-preview.3.26207.106, )",
"Microsoft.Extensions.Options.ConfigurationExtensions": "[11.0.0-preview.3.26207.106, )",
"Slopper.Domain": "[1.0.0, )"
}
},
"FFMpegCore": {
"type": "CentralTransitive",
"requested": "[5.4.0, )",
@@ -523,6 +622,16 @@
"Instances": "3.0.2"
}
},
"Google.Apis.YouTube.v3": {
"type": "CentralTransitive",
"requested": "[1.74.0.4137, )",
"resolved": "1.74.0.4137",
"contentHash": "atVRzrrKcRDOMXGNa+fW+81o7Mxm3HGCrxAVr7t0zJjyaCDa1kE/ah9hJ8G8bDOQnhjkBbEFzC8Htje18LyjJg==",
"dependencies": {
"Google.Apis": "1.74.0",
"Google.Apis.Auth": "1.74.0"
}
},
"Jellyfin.Database.Implementations": {
"type": "CentralTransitive",
"requested": "[10.11.8, )",
@@ -663,6 +772,16 @@
"Microsoft.Extensions.Primitives": "11.0.0-preview.3.26207.106"
}
},
"Microsoft.IdentityModel.JsonWebTokens": {
"type": "CentralTransitive",
"requested": "[5.7.0, )",
"resolved": "5.7.0",
"contentHash": "zWeKWLdKdA9hbxEFBMCrImfbcHGSbwXJh4Q7dE2xIqDcaG+Uect+QUTSuXLsibtGG7/CwjhZgjZx7cl3KuRoXA==",
"dependencies": {
"Microsoft.IdentityModel.Tokens": "5.7.0",
"Newtonsoft.Json": "13.0.1"
}
},
"OllamaSharp": {
"type": "CentralTransitive",
"requested": "[5.4.25, )",
@@ -686,6 +805,17 @@
"requested": "[2.4.0, )",
"resolved": "2.4.0",
"contentHash": "/QDneMSeMa1SxLu8IDsaX6WFFDdUqxn7GVI654NGgK/X4nGqKLmlrMi2CUFROD1nphp7EURrlIPIsAgSRtEgcg=="
},
"System.IdentityModel.Tokens.Jwt": {
"type": "CentralTransitive",
"requested": "[5.7.0, )",
"resolved": "5.7.0",
"contentHash": "9whmG+NvaZaTVW27i7CWiEMWAndDC4JpHg6FhZgYzFOQidscBmwu8qeGsTi0YL/HdqxmNzWqsni591dPgMYB0g==",
"dependencies": {
"Microsoft.IdentityModel.JsonWebTokens": "5.7.0",
"Microsoft.IdentityModel.Tokens": "5.7.0",
"Newtonsoft.Json": "13.0.1"
}
}
},
"net11.0/linux-arm64": {
@@ -698,6 +828,14 @@
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "dcvsAEx7YxeuSfzj5QOx8B1npA+3WuI25nLb9IscNyvgGB1cDmliDE9WRlwJvGaHhCBjqQO0G4dyB+3V7BLFzA=="
},
"System.Management": {
"type": "Transitive",
"resolved": "7.0.2",
"contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==",
"dependencies": {
"System.CodeDom": "7.0.0"
}
}
},
"net11.0/linux-x64": {
@@ -710,6 +848,14 @@
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "dcvsAEx7YxeuSfzj5QOx8B1npA+3WuI25nLb9IscNyvgGB1cDmliDE9WRlwJvGaHhCBjqQO0G4dyB+3V7BLFzA=="
},
"System.Management": {
"type": "Transitive",
"resolved": "7.0.2",
"contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==",
"dependencies": {
"System.CodeDom": "7.0.0"
}
}
},
"net11.0/osx-arm64": {
@@ -722,6 +868,14 @@
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "dcvsAEx7YxeuSfzj5QOx8B1npA+3WuI25nLb9IscNyvgGB1cDmliDE9WRlwJvGaHhCBjqQO0G4dyB+3V7BLFzA=="
},
"System.Management": {
"type": "Transitive",
"resolved": "7.0.2",
"contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==",
"dependencies": {
"System.CodeDom": "7.0.0"
}
}
},
"net11.0/osx-x64": {
@@ -734,6 +888,14 @@
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "dcvsAEx7YxeuSfzj5QOx8B1npA+3WuI25nLb9IscNyvgGB1cDmliDE9WRlwJvGaHhCBjqQO0G4dyB+3V7BLFzA=="
},
"System.Management": {
"type": "Transitive",
"resolved": "7.0.2",
"contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==",
"dependencies": {
"System.CodeDom": "7.0.0"
}
}
},
"net11.0/win-arm64": {
@@ -746,6 +908,14 @@
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "dcvsAEx7YxeuSfzj5QOx8B1npA+3WuI25nLb9IscNyvgGB1cDmliDE9WRlwJvGaHhCBjqQO0G4dyB+3V7BLFzA=="
},
"System.Management": {
"type": "Transitive",
"resolved": "7.0.2",
"contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==",
"dependencies": {
"System.CodeDom": "7.0.0"
}
}
},
"net11.0/win-x64": {
@@ -758,6 +928,14 @@
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "dcvsAEx7YxeuSfzj5QOx8B1npA+3WuI25nLb9IscNyvgGB1cDmliDE9WRlwJvGaHhCBjqQO0G4dyB+3V7BLFzA=="
},
"System.Management": {
"type": "Transitive",
"resolved": "7.0.2",
"contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==",
"dependencies": {
"System.CodeDom": "7.0.0"
}
}
}
}
src/Cli/Cli.csproj
+1
-0
diff --git a/src/Cli/Cli.csproj b/src/Cli/Cli.csproj
index fe6f23e..3e98d99 100644
@@ -18,6 +18,7 @@
<ProjectReference Include="..\Infrastructure\Database\Database.csproj" />
<ProjectReference Include="..\Infrastructure\Ffmpeg\Ffmpeg.csproj" />
<ProjectReference Include="..\Infrastructure\Ai\Ai.csproj" />
<ProjectReference Include="..\Infrastructure\YouTube\YouTube.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" />
src/Cli/Program.cs
+27
-6
diff --git a/src/Cli/Program.cs b/src/Cli/Program.cs
index 6110f25..19b7a50 100644
@@ -1,12 +1,14 @@
using System.Threading;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Slopper.Cli;
using Slopper.Cli.YouTubeAuth;
using Slopper.Domain;
using Slopper.Infrastructure.Ai;
using Slopper.Infrastructure.Database;
using Slopper.Infrastructure.Ffmpeg;
using Slopper.Infrastructure.YouTube;
var builder = Host.CreateApplicationBuilder();
@@ -14,16 +16,35 @@ builder.ConfigureOpenTelemetry();
builder.Services.AddClipSelector().AddClipGenerator();
builder.Services.AddJellyfinDatabase().AddSlopperDatabase().AddFfmpegServices().AddAi();
builder.Services.AddJellyfinDatabase().AddSlopperDatabase().AddFfmpegServices().AddAi().AddYouTubeUploader();
builder.Services.AddYouTubeAuth();
using var app = builder.Build();
await app.StartAsync();
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
var logger = app.Services.GetRequiredService<ILogger<Program>>();
using var scope = app.Services.CreateScope();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
var mediaRepository = scope.ServiceProvider.GetRequiredService<IMediaRepository>();
var media = await mediaRepository.GetRandomMediaItem(CancellationToken.None);
logger.LogInformation("Media: {MediaId}", media.Id);
var clipRepository = scope.ServiceProvider.GetRequiredService<IClipRepository>();
var clip = await clipRepository
.GetLatest(limit: 1, cancellationToken: lifetime.ApplicationStopping)
.FirstOrDefaultAsync(lifetime.ApplicationStopping);
if (clip is null)
{
logger.LogInformation("No clips in repository");
return;
}
logger.LogInformation("Uploading {ClipId}", clip.Id);
var youTubeUploader = app.Services.GetRequiredKeyedService<IUploader>("YouTube");
var upload = await youTubeUploader.Upload(clip, lifetime.ApplicationStopping);
logger.LogInformation("Clip uploaded at {Url}", upload.CanonicalUrl);
await app.StopAsync();
src/Cli/YouTubeAuth/YouTubeCredentialsProvider.cs
+21
-0
diff --git a/src/Cli/YouTubeAuth/YouTubeCredentialsProvider.cs b/src/Cli/YouTubeAuth/YouTubeCredentialsProvider.cs
new file mode 100644
index 0000000..cc29b28
@@ -0,0 +1,21 @@
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Auth.OAuth2;
using Microsoft.Extensions.Options;
using Slopper.Cli.YouTubeAuth;
using Slopper.Infrastructure.YouTube;
internal sealed class YouTubeCredentialsProvider(IOptions<YouTubeOptions> options) : IYouTubeCredentialsProvider
{
private readonly string clientId = options.Value.ClientId;
private readonly string clientSecret = options.Value.ClientSecret;
private readonly string user = options.Value.User;
public async Task<ICredential> GetCredentials(CancellationToken cancellationToken) =>
await GoogleWebAuthorizationBroker.AuthorizeAsync(
new ClientSecrets() { ClientId = clientId, ClientSecret = clientSecret },
["https://www.googleapis.com/auth/youtube.upload"],
user,
cancellationToken
);
}
src/Cli/YouTubeAuth/YouTubeOptions.cs
+19
-0
diff --git a/src/Cli/YouTubeAuth/YouTubeOptions.cs b/src/Cli/YouTubeAuth/YouTubeOptions.cs
new file mode 100644
index 0000000..b8e158b
@@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;
namespace Slopper.Cli.YouTubeAuth;
public sealed class YouTubeOptions
{
[Required]
public required string ClientId { get; set; }
[Required]
public required string ClientSecret { get; set; }
[Required]
public required string User { get; set; }
}
[OptionsValidator]
public sealed partial class YouTubeOptionsValidator : IValidateOptions<YouTubeOptions>;
src/Cli/YouTubeAuth/YouTubeServiceCollectionExtensions.cs
+21
-0
diff --git a/src/Cli/YouTubeAuth/YouTubeServiceCollectionExtensions.cs b/src/Cli/YouTubeAuth/YouTubeServiceCollectionExtensions.cs
new file mode 100644
index 0000000..a93a6d8
@@ -0,0 +1,21 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Slopper.Infrastructure.YouTube;
namespace Slopper.Cli.YouTubeAuth;
public static class YouTubeServiceCollectionExtensions
{
extension(IServiceCollection services)
{
public IServiceCollection AddYouTubeAuth()
{
services.AddOptions<YouTubeOptions>().BindConfiguration("YouTube").ValidateOnStart();
services.AddTransient<IValidateOptions<YouTubeOptions>, YouTubeOptionsValidator>();
services.AddSingleton<IYouTubeCredentialsProvider, YouTubeCredentialsProvider>();
return services;
}
}
}
src/Cli/packages.lock.json
+108
-2
diff --git a/src/Cli/packages.lock.json b/src/Cli/packages.lock.json
index 5b0fb25..dcc7681 100644
@@ -107,6 +107,32 @@
"Newtonsoft.Json": "13.0.1"
}
},
"Google.Apis": {
"type": "Transitive",
"resolved": "1.74.0",
"contentHash": "Z6hSkmRD6V8/raSBqn/EsPpNop6suaLaGE9DbSY3yRE5G47l4zYqrJgapcLARk2Z3TsQUDmWz+w/tob3wuVt3g==",
"dependencies": {
"Google.Apis.Core": "1.74.0"
}
},
"Google.Apis.Auth": {
"type": "Transitive",
"resolved": "1.74.0",
"contentHash": "43H4foh3zfWgboK8QnJjIfPQuBiS794TxqQLDfeE3erQSluKmEN0qeDuwxLTpSr9Mtwj76S4LWxPneuSbC6fNg==",
"dependencies": {
"Google.Apis": "1.74.0",
"Google.Apis.Core": "1.74.0",
"System.Management": "7.0.2"
}
},
"Google.Apis.Core": {
"type": "Transitive",
"resolved": "1.74.0",
"contentHash": "gWOIiks6yo2TRliIyy10V18BJOO2BfUSPJFiTW6Tfap4tm3hFj7nvYU3XIfwzAxwoIy6nsBoqVDt9iBDSXrvYQ==",
"dependencies": {
"Newtonsoft.Json": "13.0.4"
}
},
"Instances": {
"type": "Transitive",
"resolved": "3.0.2",
@@ -391,8 +417,8 @@
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.1",
"contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A=="
"resolved": "13.0.4",
"contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A=="
},
"OpenTelemetry.Api": {
"type": "Transitive",
@@ -448,11 +474,24 @@
"SQLitePCLRaw.core": "2.1.11"
}
},
"System.CodeDom": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A=="
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "dcvsAEx7YxeuSfzj5QOx8B1npA+3WuI25nLb9IscNyvgGB1cDmliDE9WRlwJvGaHhCBjqQO0G4dyB+3V7BLFzA=="
},
"System.Management": {
"type": "Transitive",
"resolved": "7.0.2",
"contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==",
"dependencies": {
"System.CodeDom": "7.0.0"
}
},
"System.Numerics.Tensors": {
"type": "Transitive",
"resolved": "10.0.6",
@@ -496,6 +535,15 @@
"SubtitlesParserV2": "[2.4.0, )"
}
},
"Slopper.Infrastructure.YouTube": {
"type": "Project",
"dependencies": {
"Google.Apis.YouTube.v3": "[1.74.0.4137, )",
"Microsoft.Extensions.Options": "[11.0.0-preview.3.26207.106, )",
"Microsoft.Extensions.Options.ConfigurationExtensions": "[11.0.0-preview.3.26207.106, )",
"Slopper.Domain": "[1.0.0, )"
}
},
"FFMpegCore": {
"type": "CentralTransitive",
"requested": "[5.4.0, )",
@@ -505,6 +553,16 @@
"Instances": "3.0.2"
}
},
"Google.Apis.YouTube.v3": {
"type": "CentralTransitive",
"requested": "[1.74.0.4137, )",
"resolved": "1.74.0.4137",
"contentHash": "atVRzrrKcRDOMXGNa+fW+81o7Mxm3HGCrxAVr7t0zJjyaCDa1kE/ah9hJ8G8bDOQnhjkBbEFzC8Htje18LyjJg==",
"dependencies": {
"Google.Apis": "1.74.0",
"Google.Apis.Auth": "1.74.0"
}
},
"Jellyfin.Database.Implementations": {
"type": "CentralTransitive",
"requested": "[10.11.8, )",
@@ -661,6 +719,14 @@
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "dcvsAEx7YxeuSfzj5QOx8B1npA+3WuI25nLb9IscNyvgGB1cDmliDE9WRlwJvGaHhCBjqQO0G4dyB+3V7BLFzA=="
},
"System.Management": {
"type": "Transitive",
"resolved": "7.0.2",
"contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==",
"dependencies": {
"System.CodeDom": "7.0.0"
}
}
},
"net11.0/linux-x64": {
@@ -673,6 +739,14 @@
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "dcvsAEx7YxeuSfzj5QOx8B1npA+3WuI25nLb9IscNyvgGB1cDmliDE9WRlwJvGaHhCBjqQO0G4dyB+3V7BLFzA=="
},
"System.Management": {
"type": "Transitive",
"resolved": "7.0.2",
"contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==",
"dependencies": {
"System.CodeDom": "7.0.0"
}
}
},
"net11.0/osx-arm64": {
@@ -685,6 +759,14 @@
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "dcvsAEx7YxeuSfzj5QOx8B1npA+3WuI25nLb9IscNyvgGB1cDmliDE9WRlwJvGaHhCBjqQO0G4dyB+3V7BLFzA=="
},
"System.Management": {
"type": "Transitive",
"resolved": "7.0.2",
"contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==",
"dependencies": {
"System.CodeDom": "7.0.0"
}
}
},
"net11.0/osx-x64": {
@@ -697,6 +779,14 @@
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "dcvsAEx7YxeuSfzj5QOx8B1npA+3WuI25nLb9IscNyvgGB1cDmliDE9WRlwJvGaHhCBjqQO0G4dyB+3V7BLFzA=="
},
"System.Management": {
"type": "Transitive",
"resolved": "7.0.2",
"contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==",
"dependencies": {
"System.CodeDom": "7.0.0"
}
}
},
"net11.0/win-arm64": {
@@ -709,6 +799,14 @@
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "dcvsAEx7YxeuSfzj5QOx8B1npA+3WuI25nLb9IscNyvgGB1cDmliDE9WRlwJvGaHhCBjqQO0G4dyB+3V7BLFzA=="
},
"System.Management": {
"type": "Transitive",
"resolved": "7.0.2",
"contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==",
"dependencies": {
"System.CodeDom": "7.0.0"
}
}
},
"net11.0/win-x64": {
@@ -721,6 +819,14 @@
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "dcvsAEx7YxeuSfzj5QOx8B1npA+3WuI25nLb9IscNyvgGB1cDmliDE9WRlwJvGaHhCBjqQO0G4dyB+3V7BLFzA=="
},
"System.Management": {
"type": "Transitive",
"resolved": "7.0.2",
"contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==",
"dependencies": {
"System.CodeDom": "7.0.0"
}
}
}
}
src/Domain/IUploader.cs
+12
-0
diff --git a/src/Domain/IUploader.cs b/src/Domain/IUploader.cs
new file mode 100644
index 0000000..3f6ee3c
@@ -0,0 +1,12 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Slopper.Domain;
public interface IUploader
{
Task<UploadResult> Upload(Clip clip, CancellationToken cancellationToken);
}
public sealed record UploadResult(Uri CanonicalUrl);
src/Infrastructure/YouTube/HttpClientInitializer.cs
+20
-0
diff --git a/src/Infrastructure/YouTube/HttpClientInitializer.cs b/src/Infrastructure/YouTube/HttpClientInitializer.cs
new file mode 100644
index 0000000..5f7fb25
@@ -0,0 +1,20 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Http;
namespace Slopper.Infrastructure.YouTube;
internal sealed class HttpClientInitializer(IYouTubeCredentialsProvider youTubeCredentialsProvider)
: IConfigurableHttpClientInitializer,
IHttpExecuteInterceptor
{
public void Initialize(ConfigurableHttpClient httpClient) => httpClient.MessageHandler.Credential = this;
public async Task InterceptAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var credentials = await youTubeCredentialsProvider.GetCredentials(cancellationToken);
var token = await credentials.GetAccessTokenForRequestAsync(request.RequestUri?.ToString(), cancellationToken);
request.Headers.Authorization = new("Bearer", token);
}
}
src/Infrastructure/YouTube/IYouTubeCredentialsProvider.cs
+10
-0
diff --git a/src/Infrastructure/YouTube/IYouTubeCredentialsProvider.cs b/src/Infrastructure/YouTube/IYouTubeCredentialsProvider.cs
new file mode 100644
index 0000000..13b3704
@@ -0,0 +1,10 @@
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Auth.OAuth2;
namespace Slopper.Infrastructure.YouTube;
public interface IYouTubeCredentialsProvider
{
Task<ICredential> GetCredentials(CancellationToken cancellationToken);
}
src/Infrastructure/YouTube/ServiceCollectionExtensions.cs
+28
-0
diff --git a/src/Infrastructure/YouTube/ServiceCollectionExtensions.cs b/src/Infrastructure/YouTube/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..04ec00b
@@ -0,0 +1,28 @@
using Google.Apis.Services;
using Google.Apis.YouTube.v3;
using Microsoft.Extensions.DependencyInjection;
using Slopper.Domain;
namespace Slopper.Infrastructure.YouTube;
public static class ServiceCollectionExtensions
{
extension(IServiceCollection services)
{
public IServiceCollection AddYouTubeUploader()
{
services.AddTransient<HttpClientInitializer>();
services.AddTransient(sp => new YouTubeService(
new BaseClientService.Initializer
{
HttpClientInitializer = sp.GetRequiredService<HttpClientInitializer>(),
}
));
services.AddKeyedTransient<IUploader, YouTubeUploader>("YouTube");
return services;
}
}
}
src/Infrastructure/YouTube/YouTube.csproj
+12
-0
diff --git a/src/Infrastructure/YouTube/YouTube.csproj b/src/Infrastructure/YouTube/YouTube.csproj
new file mode 100644
index 0000000..6b776ab
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Slopper.Infrastructure.YouTube</RootNamespace>
<AssemblyName>Slopper.Infrastructure.YouTube</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain\Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google.Apis.YouTube.v3" />
</ItemGroup>
</Project>
src/Infrastructure/YouTube/YouTubeUploader.cs
+44
-0
diff --git a/src/Infrastructure/YouTube/YouTubeUploader.cs b/src/Infrastructure/YouTube/YouTubeUploader.cs
new file mode 100644
index 0000000..79a076f
@@ -0,0 +1,44 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Upload;
using Google.Apis.YouTube.v3;
using Google.Apis.YouTube.v3.Data;
using Slopper.Domain;
namespace Slopper.Infrastructure.YouTube;
internal sealed class YouTubeUploader(YouTubeService youTubeService) : IUploader
{
public async Task<UploadResult> Upload(Clip clip, CancellationToken cancellationToken)
{
var video = new Video()
{
Snippet = new()
{
Title = clip.Caption,
Tags = [.. clip.Tags.Select(t => t.Value)],
CategoryId = "1",
},
Status = new() { PrivacyStatus = "public" },
};
string? youTubeId = null;
using (var videoStream = File.OpenRead(clip.Path))
{
var request = youTubeService.Videos.Insert(video, "snippet,status", videoStream, "video/mp4");
request.ResponseReceived += v => youTubeId = v.Id;
var progress = await request.UploadAsync(cancellationToken);
progress.ThrowOnFailure();
}
if (youTubeId is null)
{
throw new Exception("Received no YouTube ID from upload");
}
return new(new($"https://www.youtube.com/shorts/{youTubeId}"));
}
}
src/Infrastructure/YouTube/packages.lock.json
+210
-0
diff --git a/src/Infrastructure/YouTube/packages.lock.json b/src/Infrastructure/YouTube/packages.lock.json
new file mode 100644
index 0000000..3a76423
@@ -0,0 +1,210 @@
{
"version": 2,
"dependencies": {
"net11.0": {
"CSharpier.MsBuild": {
"type": "Direct",
"requested": "[1.2.6, )",
"resolved": "1.2.6",
"contentHash": "KMSJG+jfk7vjP52QkWB99qWespXCPAzG/IaMCMRHYWumJEAGKQYm2HtyWG6eqnOwDitH96i1cqq5EVesyOtPmg=="
},
"Google.Apis.YouTube.v3": {
"type": "Direct",
"requested": "[1.74.0.4137, )",
"resolved": "1.74.0.4137",
"contentHash": "atVRzrrKcRDOMXGNa+fW+81o7Mxm3HGCrxAVr7t0zJjyaCDa1kE/ah9hJ8G8bDOQnhjkBbEFzC8Htje18LyjJg==",
"dependencies": {
"Google.Apis": "1.74.0",
"Google.Apis.Auth": "1.74.0"
}
},
"Google.Apis": {
"type": "Transitive",
"resolved": "1.74.0",
"contentHash": "Z6hSkmRD6V8/raSBqn/EsPpNop6suaLaGE9DbSY3yRE5G47l4zYqrJgapcLARk2Z3TsQUDmWz+w/tob3wuVt3g==",
"dependencies": {
"Google.Apis.Core": "1.74.0"
}
},
"Google.Apis.Auth": {
"type": "Transitive",
"resolved": "1.74.0",
"contentHash": "43H4foh3zfWgboK8QnJjIfPQuBiS794TxqQLDfeE3erQSluKmEN0qeDuwxLTpSr9Mtwj76S4LWxPneuSbC6fNg==",
"dependencies": {
"Google.Apis": "1.74.0",
"Google.Apis.Core": "1.74.0",
"System.Management": "7.0.2"
}
},
"Google.Apis.Core": {
"type": "Transitive",
"resolved": "1.74.0",
"contentHash": "gWOIiks6yo2TRliIyy10V18BJOO2BfUSPJFiTW6Tfap4tm3hFj7nvYU3XIfwzAxwoIy6nsBoqVDt9iBDSXrvYQ==",
"dependencies": {
"Newtonsoft.Json": "13.0.4"
}
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "Do6yieeVHdvwyIKED9oPfFHAH5PAkvwDjR+65u2ZS/ddSHvEtOd5e5rrAQyhIIflbCz13graO/XkBQQV5EJNkg==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "11.0.0-preview.3.26207.106",
"Microsoft.Extensions.Primitives": "11.0.0-preview.3.26207.106"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "DAFozg1P/fA2yh36sYLS/NMDxGCFATUFNYbgQi1wbkFT2cFYqEsK/VYbTXsiXKQfr3G/d4Rnorpe0In2WBaMIA==",
"dependencies": {
"Microsoft.Extensions.Primitives": "11.0.0-preview.3.26207.106"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "ulM+V32IqcFYIqIxxT7MZjSfpQ9T3k33chyBrnjcfSm1BQFIgtdTcXAlZJpzZmFAklh4PHG7BFuVw9PIJ1KcUg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "11.0.0-preview.3.26207.106",
"Microsoft.Extensions.Configuration.Abstractions": "11.0.0-preview.3.26207.106"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "+gJnv1/kfXLXPv21R3iluhKqfXdf2zPWUaHBiSvlJurThv2D5HRUfU5z5SpmBII4I0JSpuprX9DlHrKz/1wCXA=="
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "IBOlwyX13ax6/fXA7AoZFswKFytta9TExBv3/8qemMJGBoDXYlQEcw4WerHQCvmerJ5uP2o8bjIAvxcNdTZVLQ=="
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.4",
"contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A=="
},
"System.CodeDom": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A=="
},
"System.Management": {
"type": "Transitive",
"resolved": "7.0.2",
"contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==",
"dependencies": {
"System.CodeDom": "7.0.0"
}
},
"Slopper.Domain": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.AI.Abstractions": "[10.5.2, )",
"Microsoft.Extensions.Logging.Abstractions": "[11.0.0-preview.3.26207.106, )",
"Microsoft.Extensions.Options": "[11.0.0-preview.3.26207.106, )",
"Microsoft.Extensions.Options.ConfigurationExtensions": "[11.0.0-preview.3.26207.106, )"
}
},
"Microsoft.Extensions.AI.Abstractions": {
"type": "CentralTransitive",
"requested": "[10.5.2, )",
"resolved": "10.5.2",
"contentHash": "Ei+YWV9Ybnps7pR1dgjlG29gelXEwZkhLVAcWmKe6HvXS6LNBYgSdWiY3Hk9OZXYtK34rv/NtLWBQYQGOBQYPQ=="
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "CentralTransitive",
"requested": "[11.0.0-preview.3.26207.106, )",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "0LktkD4eySHjlglnee7jt/I3KPea+MPIxLTYBacH1P/iluOCl7VVKwpG/bciZMkyaNnfslY2E70t6nfvjq51vA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "11.0.0-preview.3.26207.106"
}
},
"Microsoft.Extensions.Options": {
"type": "CentralTransitive",
"requested": "[11.0.0-preview.3.26207.106, )",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "2kd+Lqnh8bvBun9wH+MUZ15Pb+4LAY0ErmeBhy5bsliLQyjRsoejWEOgyjkiZpLj9iLNM8tYAt6SW2vkzFbR8g==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "11.0.0-preview.3.26207.106",
"Microsoft.Extensions.Primitives": "11.0.0-preview.3.26207.106"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "CentralTransitive",
"requested": "[11.0.0-preview.3.26207.106, )",
"resolved": "11.0.0-preview.3.26207.106",
"contentHash": "DbZcRfBrCSLas0cS0iKdiez9kM/7Z3rz5xlDJKqAxhGPGzhKJu82Z3+LNANPZSTUbyYnNawb3Euvv8ACPPatjQ==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "11.0.0-preview.3.26207.106",
"Microsoft.Extensions.Configuration.Binder": "11.0.0-preview.3.26207.106",
"Microsoft.Extensions.DependencyInjection.Abstractions": "11.0.0-preview.3.26207.106",
"Microsoft.Extensions.Options": "11.0.0-preview.3.26207.106",
"Microsoft.Extensions.Primitives": "11.0.0-preview.3.26207.106"
}
}
},
"net11.0/linux-arm64": {
"System.Management": {
"type": "Transitive",
"resolved": "7.0.2",
"contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==",
"dependencies": {
"System.CodeDom": "7.0.0"
}
}
},
"net11.0/linux-x64": {
"System.Management": {
"type": "Transitive",
"resolved": "7.0.2",
"contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==",
"dependencies": {
"System.CodeDom": "7.0.0"
}
}
},
"net11.0/osx-arm64": {
"System.Management": {
"type": "Transitive",
"resolved": "7.0.2",
"contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==",
"dependencies": {
"System.CodeDom": "7.0.0"
}
}
},
"net11.0/osx-x64": {
"System.Management": {
"type": "Transitive",
"resolved": "7.0.2",
"contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==",
"dependencies": {
"System.CodeDom": "7.0.0"
}
}
},
"net11.0/win-arm64": {
"System.Management": {
"type": "Transitive",
"resolved": "7.0.2",
"contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==",
"dependencies": {
"System.CodeDom": "7.0.0"
}
}
},
"net11.0/win-x64": {
"System.Management": {
"type": "Transitive",
"resolved": "7.0.2",
"contentHash": "/qEUN91mP/MUQmJnM5y5BdT7ZoPuVrtxnFlbJ8a3kBJGhe2wCzBfnPFtK2wTtEEcf3DMGR9J00GZZfg6HRI6yA==",
"dependencies": {
"System.CodeDom": "7.0.0"
}
}
}
}
}
\ No newline at end of file