From 7b13d0ba7b379f4cdcad28c7bd597cd36fde8374 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Tue, 20 May 2025 21:45:49 -0300 Subject: [PATCH] Add project files. --- YTSearch.sln | 25 +++ YTSearch/AppServices/QuotaService.cs | 90 ++++++++++ YTSearch/AppServices/YouTubeSearchService.cs | 166 +++++++++++++++++++ YTSearch/Contracts/IQuotaService.cs | 13 ++ YTSearch/Contracts/IYouTubeSearchService.cs | 9 + YTSearch/Controllers/QuotaController.cs | 65 ++++++++ YTSearch/Controllers/YouTubeController.cs | 73 ++++++++ YTSearch/Models/QuotaLimit.cs | 11 ++ YTSearch/Models/QuotaStatus.cs | 12 ++ YTSearch/Models/QuotaUpdateRequest.cs | 8 + YTSearch/Models/SearchRequest.cs | 9 + YTSearch/Models/SearchResponse.cs | 11 ++ YTSearch/Models/VideoResult.cs | 17 ++ YTSearch/Program.cs | 44 +++++ YTSearch/Properties/launchSettings.json | 41 +++++ YTSearch/YTSearch.csproj | 17 ++ YTSearch/YTSearch.http | 6 + YTSearch/appsettings.Development.json | 8 + YTSearch/appsettings.json | 12 ++ 19 files changed, 637 insertions(+) create mode 100644 YTSearch.sln create mode 100644 YTSearch/AppServices/QuotaService.cs create mode 100644 YTSearch/AppServices/YouTubeSearchService.cs create mode 100644 YTSearch/Contracts/IQuotaService.cs create mode 100644 YTSearch/Contracts/IYouTubeSearchService.cs create mode 100644 YTSearch/Controllers/QuotaController.cs create mode 100644 YTSearch/Controllers/YouTubeController.cs create mode 100644 YTSearch/Models/QuotaLimit.cs create mode 100644 YTSearch/Models/QuotaStatus.cs create mode 100644 YTSearch/Models/QuotaUpdateRequest.cs create mode 100644 YTSearch/Models/SearchRequest.cs create mode 100644 YTSearch/Models/SearchResponse.cs create mode 100644 YTSearch/Models/VideoResult.cs create mode 100644 YTSearch/Program.cs create mode 100644 YTSearch/Properties/launchSettings.json create mode 100644 YTSearch/YTSearch.csproj create mode 100644 YTSearch/YTSearch.http create mode 100644 YTSearch/appsettings.Development.json create mode 100644 YTSearch/appsettings.json diff --git a/YTSearch.sln b/YTSearch.sln new file mode 100644 index 0000000..0da7388 --- /dev/null +++ b/YTSearch.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35818.85 d17.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YTSearch", "YTSearch\YTSearch.csproj", "{35BF7008-0A54-4797-B94A-14CB360C63E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {35BF7008-0A54-4797-B94A-14CB360C63E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35BF7008-0A54-4797-B94A-14CB360C63E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35BF7008-0A54-4797-B94A-14CB360C63E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35BF7008-0A54-4797-B94A-14CB360C63E1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FC9E4038-AEC5-4DE9-995B-6DC9F570B4F7} + EndGlobalSection +EndGlobal diff --git a/YTSearch/AppServices/QuotaService.cs b/YTSearch/AppServices/QuotaService.cs new file mode 100644 index 0000000..188d26e --- /dev/null +++ b/YTSearch/AppServices/QuotaService.cs @@ -0,0 +1,90 @@ +using MongoDB.Driver; +using YTSearch.Contracts; +using YTSearch.Models; + +namespace YTSearch.AppServices +{ + public class QuotaService : IQuotaService + { + private readonly IMongoCollection _quotaCollection; + private const int SEARCH_COST = 100; + private const int VIDEO_DETAILS_COST = 1; + + public QuotaService(IMongoDatabase database) + { + _quotaCollection = database.GetCollection("quota_limits"); + } + + public async Task CanUseQuotaAsync(int cost) + { + await ResetQuotaIfNeededAsync(); + var quota = await GetCurrentQuotaAsync(); + return (quota.CurrentUsage + cost) <= quota.DailyLimit; + } + + public async Task UseQuotaAsync(int cost) + { + var filter = Builders.Filter.Eq(x => x.Id, "youtube_quota"); + var update = Builders.Update + .Inc(x => x.CurrentUsage, cost) + .Set(x => x.LastUpdated, DateTime.UtcNow); + + await _quotaCollection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + } + + public async Task GetQuotaStatusAsync() + { + await ResetQuotaIfNeededAsync(); + var quota = await GetCurrentQuotaAsync(); + + var pacificTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, + TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time")); + var nextReset = pacificTime.Date.AddDays(1); + + return new QuotaStatus + { + DailyLimit = quota.DailyLimit, + CurrentUsage = quota.CurrentUsage, + Remaining = Math.Max(0, quota.DailyLimit - quota.CurrentUsage), + LastReset = quota.LastReset, + IsExceeded = quota.CurrentUsage >= quota.DailyLimit, + ResetTime = nextReset.ToString("yyyy-MM-dd HH:mm:ss") + " PT" + }; + } + + public async Task UpdateDailyLimitAsync(long newLimit) + { + var filter = Builders.Filter.Eq(x => x.Id, "youtube_quota"); + var update = Builders.Update + .Set(x => x.DailyLimit, newLimit) + .Set(x => x.LastUpdated, DateTime.UtcNow); + + await _quotaCollection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + } + + public async Task ResetQuotaIfNeededAsync() + { + var quota = await GetCurrentQuotaAsync(); + var pacificTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, + TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time")); + + // Reset at midnight Pacific Time + if (quota.LastReset.Date < pacificTime.Date) + { + var filter = Builders.Filter.Eq(x => x.Id, "youtube_quota"); + var update = Builders.Update + .Set(x => x.CurrentUsage, 0) + .Set(x => x.LastReset, pacificTime.Date) + .Set(x => x.LastUpdated, DateTime.UtcNow); + + await _quotaCollection.UpdateOneAsync(filter, update); + } + } + + private async Task GetCurrentQuotaAsync() + { + var quota = await _quotaCollection.Find(x => x.Id == "youtube_quota").FirstOrDefaultAsync(); + return quota ?? new QuotaLimit(); + } + } +} diff --git a/YTSearch/AppServices/YouTubeSearchService.cs b/YTSearch/AppServices/YouTubeSearchService.cs new file mode 100644 index 0000000..7af4b50 --- /dev/null +++ b/YTSearch/AppServices/YouTubeSearchService.cs @@ -0,0 +1,166 @@ +using Google.Apis.Services; +using Google.Apis.YouTube.v3; +using YoutubeExplode; +using YoutubeExplode.Common; +using YTSearch.Contracts; +using YTSearch.Models; + +namespace YTSearch.AppServices +{ + public class YouTubeSearchService : IYouTubeSearchService + { + private readonly IQuotaService _quotaService; + private readonly IConfiguration _configuration; + private readonly YouTubeService _youtubeService; + private readonly YoutubeClient _youtubeClient; + + public YouTubeSearchService(IQuotaService quotaService, IConfiguration configuration) + { + _quotaService = quotaService; + _configuration = configuration; + + var apiKey = _configuration["GoogleAPiKey"]; + _youtubeService = new YouTubeService(new BaseClientService.Initializer() + { + ApiKey = apiKey, + ApplicationName = "YouTube Search API" + }); + + _youtubeClient = new YoutubeClient(); + } + + public async Task SearchVideosAsync(SearchRequest request) + { + // Calculate quota cost: Search (100) + Video details (1 per video) + var quotaCost = 100 + request.MaxResults; + + // Try YouTube Data API first if quota allows + if (await _quotaService.CanUseQuotaAsync(quotaCost)) + { + try + { + var result = await SearchWithYouTubeApiAsync(request); + await _quotaService.UseQuotaAsync(quotaCost); + result.SearchMethod = "YouTube Data API"; + result.Message = $"Used {quotaCost} quota units. Remaining: {(await _quotaService.GetQuotaStatusAsync()).Remaining}"; + return result; + } + catch (Exception ex) + { + return await SearchWithYoutubeExplodeAsync(request, $"YouTube API failed: {ex.Message}. Using YoutubeExplode fallback."); + } + } + else + { + var quotaStatus = await _quotaService.GetQuotaStatusAsync(); + return await SearchWithYoutubeExplodeAsync(request, + $"Quota exceeded ({quotaStatus.CurrentUsage}/{quotaStatus.DailyLimit}). Using YoutubeExplode fallback."); + } + } + + private async Task SearchWithYouTubeApiAsync(SearchRequest request) + { + var searchRequest = _youtubeService.Search.List("snippet"); + searchRequest.Q = request.Keywords; + searchRequest.MaxResults = request.MaxResults; + searchRequest.PageToken = request.PageToken; + searchRequest.Type = "video"; + searchRequest.Order = SearchResource.ListRequest.OrderEnum.ViewCount; + searchRequest.VideoDuration = SearchResource.ListRequest.VideoDurationEnum.Medium | + SearchResource.ListRequest.VideoDurationEnum.Long__; + + var searchResponse = await searchRequest.ExecuteAsync(); + + var videoIds = searchResponse.Items.Select(item => item.Id.VideoId).ToList(); + + var videosRequest = _youtubeService.Videos.List("snippet,statistics"); + videosRequest.Id = string.Join(",", videoIds); + + var videosResponse = await videosRequest.ExecuteAsync(); + + var videos = videosResponse.Items.Select(video => new VideoResult + { + Id = video.Id, + Title = video.Snippet.Title, + Description = video.Snippet.Description, + ThumbnailUrl = video.Snippet.Thumbnails.High?.Url ?? video.Snippet.Thumbnails.Default__.Url, + ChannelName = video.Snippet.ChannelTitle, + ChannelId = video.Snippet.ChannelId, + ViewCount = (long?) video.Statistics.ViewCount ?? 0, + LikeCount = (long?)video.Statistics.LikeCount ?? 0, + DislikeCount = (long?)video.Statistics.DislikeCount ?? 0, + PublishedAt = video.Snippet.PublishedAt ?? DateTime.MinValue, + VideoUrl = $"https://www.youtube.com/watch?v={video.Id}" + }) + .OrderByDescending(v => v.ViewCount) + .ToList(); + + return new SearchResponse + { + Videos = videos, + NextPageToken = searchResponse.NextPageToken ?? string.Empty, + TotalResults = (int)(searchResponse.PageInfo.TotalResults ?? 0) + }; + } + + private async Task SearchWithYoutubeExplodeAsync(SearchRequest request, string message) + { + try + { + var skip = !string.IsNullOrEmpty(request.PageToken) ? int.Parse(request.PageToken) * request.MaxResults : 0; + + var searchResults = _youtubeClient.Search.GetVideosAsync(request.Keywords); + var videos = new List(); + + await foreach (var video in searchResults.Skip(skip).Take(request.MaxResults)) + { + var videoDetails = await _youtubeClient.Videos.GetAsync(video.Id); + + if (videoDetails.Duration?.TotalSeconds <= 60) + { + continue; + } + + videos.Add(new VideoResult + { + Id = video.Id, + Title = video.Title, + Description = videoDetails.Description, + ThumbnailUrl = video.Thumbnails.GetWithHighestResolution().Url, + ChannelName = video.Author.ChannelTitle, + ChannelId = video.Author.ChannelId, + ViewCount = videoDetails.Engagement.ViewCount, + LikeCount = videoDetails.Engagement.LikeCount, + DislikeCount = videoDetails.Engagement.DislikeCount, + PublishedAt = videoDetails.UploadDate.DateTime, + VideoUrl = video.Url + }); + } + + // Sort by view count + videos = videos.OrderByDescending(v => v.ViewCount).ToList(); + + // Generate next page token + var nextPageToken = videos.Count == request.MaxResults ? (skip / request.MaxResults + 1).ToString() : string.Empty; + + return new SearchResponse + { + Videos = videos, + NextPageToken = nextPageToken, + TotalResults = videos.Count, + SearchMethod = "YoutubeExplode", + Message = message + }; + } + catch (Exception ex) + { + return new SearchResponse + { + Videos = new List(), + SearchMethod = "YoutubeExplode", + Message = $"YoutubeExplode search failed: {ex.Message}" + }; + } + } + } +} diff --git a/YTSearch/Contracts/IQuotaService.cs b/YTSearch/Contracts/IQuotaService.cs new file mode 100644 index 0000000..b724c00 --- /dev/null +++ b/YTSearch/Contracts/IQuotaService.cs @@ -0,0 +1,13 @@ +using YTSearch.Models; + +namespace YTSearch.Contracts +{ + public interface IQuotaService + { + Task CanUseQuotaAsync(int cost); + Task UseQuotaAsync(int cost); + Task GetQuotaStatusAsync(); + Task UpdateDailyLimitAsync(long newLimit); + Task ResetQuotaIfNeededAsync(); + } +} diff --git a/YTSearch/Contracts/IYouTubeSearchService.cs b/YTSearch/Contracts/IYouTubeSearchService.cs new file mode 100644 index 0000000..8fee4f8 --- /dev/null +++ b/YTSearch/Contracts/IYouTubeSearchService.cs @@ -0,0 +1,9 @@ +using YTSearch.Models; + +namespace YTSearch.Contracts +{ + public interface IYouTubeSearchService + { + Task SearchVideosAsync(SearchRequest request); + } +} diff --git a/YTSearch/Controllers/QuotaController.cs b/YTSearch/Controllers/QuotaController.cs new file mode 100644 index 0000000..6dd6389 --- /dev/null +++ b/YTSearch/Controllers/QuotaController.cs @@ -0,0 +1,65 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using YTSearch.Contracts; +using YTSearch.Models; + +namespace YTSearch.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class QuotaController : ControllerBase + { + private readonly IQuotaService _quotaService; + + public QuotaController(IQuotaService quotaService) + { + _quotaService = quotaService; + } + + /// + /// Get current quota status + /// + [HttpGet("status")] + public async Task> GetQuotaStatus() + { + var status = await _quotaService.GetQuotaStatusAsync(); + return Ok(status); + } + + /// + /// Update daily quota limit + /// + [HttpPost("update-limit")] + public async Task UpdateQuotaLimit([FromBody] QuotaUpdateRequest request) + { + if (request.NewDailyLimit <= 0) + { + return BadRequest(new { message = "Daily limit must be greater than 0" }); + } + + await _quotaService.UpdateDailyLimitAsync(request.NewDailyLimit); + + var updatedStatus = await _quotaService.GetQuotaStatusAsync(); + return Ok(new + { + message = "Quota limit updated successfully", + newStatus = updatedStatus + }); + } + + /// + /// Force reset quota (for testing purposes) + /// + [HttpPost("reset")] + public async Task ResetQuota() + { + await _quotaService.ResetQuotaIfNeededAsync(); + var status = await _quotaService.GetQuotaStatusAsync(); + return Ok(new + { + message = "Quota reset completed", + status = status + }); + } + } +} diff --git a/YTSearch/Controllers/YouTubeController.cs b/YTSearch/Controllers/YouTubeController.cs new file mode 100644 index 0000000..d4d96e6 --- /dev/null +++ b/YTSearch/Controllers/YouTubeController.cs @@ -0,0 +1,73 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using YTSearch.Contracts; +using YTSearch.Models; + +namespace YTSearch.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class YouTubeController : ControllerBase + { + private readonly IYouTubeSearchService _youtubeService; + + public YouTubeController(IYouTubeSearchService youtubeService) + { + _youtubeService = youtubeService; + } + + /// + /// Search for YouTube videos by keywords + /// + /// Search keywords + /// Token for pagination (optional) + /// Maximum number of results (default: 20, max: 50) + /// List of videos with details + [HttpGet("search")] + public async Task> SearchVideos( + [FromQuery] string keywords, + [FromQuery] string pageToken = "", + [FromQuery] int maxResults = 20) + { + if (string.IsNullOrWhiteSpace(keywords)) + { + return BadRequest(new { message = "Keywords are required" }); + } + + if (maxResults > 50) + { + return BadRequest(new { message = "Maximum results cannot exceed 50" }); + } + + var request = new SearchRequest + { + Keywords = keywords, + PageToken = pageToken, + MaxResults = maxResults + }; + + var result = await _youtubeService.SearchVideosAsync(request); + return Ok(result); + } + + /// + /// Search for YouTube videos using POST method + /// + [HttpPost("search")] + public async Task> SearchVideosPost([FromBody] SearchRequest request) + { + if (string.IsNullOrWhiteSpace(request.Keywords)) + { + return BadRequest(new { message = "Keywords are required" }); + } + + if (request.MaxResults > 50) + { + return BadRequest(new { message = "Maximum results cannot exceed 50" }); + } + + var result = await _youtubeService.SearchVideosAsync(request); + return Ok(result); + } + } +} diff --git a/YTSearch/Models/QuotaLimit.cs b/YTSearch/Models/QuotaLimit.cs new file mode 100644 index 0000000..ebf37ce --- /dev/null +++ b/YTSearch/Models/QuotaLimit.cs @@ -0,0 +1,11 @@ +namespace YTSearch.Models +{ + public class QuotaLimit + { + public string Id { get; set; } = "youtube_quota"; + public long DailyLimit { get; set; } = 10000; + public long CurrentUsage { get; set; } = 0; + public DateTime LastReset { get; set; } = DateTime.UtcNow; + public DateTime LastUpdated { get; set; } = DateTime.UtcNow; + } +} diff --git a/YTSearch/Models/QuotaStatus.cs b/YTSearch/Models/QuotaStatus.cs new file mode 100644 index 0000000..906bafa --- /dev/null +++ b/YTSearch/Models/QuotaStatus.cs @@ -0,0 +1,12 @@ +namespace YTSearch.Models +{ + public class QuotaStatus + { + public long DailyLimit { get; set; } + public long CurrentUsage { get; set; } + public long Remaining { get; set; } + public DateTime LastReset { get; set; } + public bool IsExceeded { get; set; } + public string ResetTime { get; set; } = string.Empty; + } +} diff --git a/YTSearch/Models/QuotaUpdateRequest.cs b/YTSearch/Models/QuotaUpdateRequest.cs new file mode 100644 index 0000000..34be160 --- /dev/null +++ b/YTSearch/Models/QuotaUpdateRequest.cs @@ -0,0 +1,8 @@ +namespace YTSearch.Models +{ + public class QuotaUpdateRequest + { + public long NewDailyLimit { get; set; } + + } +} diff --git a/YTSearch/Models/SearchRequest.cs b/YTSearch/Models/SearchRequest.cs new file mode 100644 index 0000000..bb0062b --- /dev/null +++ b/YTSearch/Models/SearchRequest.cs @@ -0,0 +1,9 @@ +namespace YTSearch.Models +{ + public class SearchRequest + { + public string Keywords { get; set; } = string.Empty; + public string PageToken { get; set; } = string.Empty; + public int MaxResults { get; set; } = 20; + } +} diff --git a/YTSearch/Models/SearchResponse.cs b/YTSearch/Models/SearchResponse.cs new file mode 100644 index 0000000..b60143d --- /dev/null +++ b/YTSearch/Models/SearchResponse.cs @@ -0,0 +1,11 @@ +namespace YTSearch.Models +{ + public class SearchResponse + { + public List Videos { get; set; } = new(); + public string NextPageToken { get; set; } = string.Empty; + public int TotalResults { get; set; } + public string SearchMethod { get; set; } = string.Empty; // "YouTubeAPI" or "YoutubeExplode" + public string Message { get; set; } = string.Empty; + } +} diff --git a/YTSearch/Models/VideoResult.cs b/YTSearch/Models/VideoResult.cs new file mode 100644 index 0000000..c47a1b4 --- /dev/null +++ b/YTSearch/Models/VideoResult.cs @@ -0,0 +1,17 @@ +namespace YTSearch.Models +{ + public class VideoResult + { + public string Id { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string ThumbnailUrl { get; set; } = string.Empty; + public string ChannelName { get; set; } = string.Empty; + public string ChannelId { get; set; } = string.Empty; + public long ViewCount { get; set; } + public long LikeCount { get; set; } + public long DislikeCount { get; set; } + public DateTime PublishedAt { get; set; } + public string VideoUrl { get; set; } = string.Empty; + } +} diff --git a/YTSearch/Program.cs b/YTSearch/Program.cs new file mode 100644 index 0000000..2423843 --- /dev/null +++ b/YTSearch/Program.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Mvc; +using MongoDB.Driver; +using YTSearch.AppServices; +using YTSearch.Contracts; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +// MongoDB Configuration +builder.Services.AddSingleton(serviceProvider => +{ + var connectionString = builder.Configuration.GetConnectionString("MongoDB") ?? "mongodb://localhost:27017"; + return new MongoClient(connectionString); +}); + +builder.Services.AddScoped(serviceProvider => +{ + var client = serviceProvider.GetService(); + return client.GetDatabase("YouTubeSearchDB"); +}); + +// Register Services +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddHttpClient(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.UseAuthorization(); +app.MapControllers(); + +app.Run(); diff --git a/YTSearch/Properties/launchSettings.json b/YTSearch/Properties/launchSettings.json new file mode 100644 index 0000000..7e5525d --- /dev/null +++ b/YTSearch/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:38465", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5044", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7183;http://localhost:5044", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/YTSearch/YTSearch.csproj b/YTSearch/YTSearch.csproj new file mode 100644 index 0000000..e8481f1 --- /dev/null +++ b/YTSearch/YTSearch.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/YTSearch/YTSearch.http b/YTSearch/YTSearch.http new file mode 100644 index 0000000..149d88a --- /dev/null +++ b/YTSearch/YTSearch.http @@ -0,0 +1,6 @@ +@YTSearch_HostAddress = http://localhost:5044 + +GET {{YTSearch_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/YTSearch/appsettings.Development.json b/YTSearch/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/YTSearch/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/YTSearch/appsettings.json b/YTSearch/appsettings.json new file mode 100644 index 0000000..702e44c --- /dev/null +++ b/YTSearch/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "GoogleClientId": "656198616765-06t0blmd1tlpcb9h81vgrshoqs4gd434.apps.googleusercontent.com", + "GoogleAPiKey": "AIzaSyB9YTCRcTzPHQkHW7zdwkYbpo0KON39ll8", + "GoogleClientSecret": "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", + "AllowedHosts": "*" +}