Add project files.
This commit is contained in:
parent
7b5e677c96
commit
7b13d0ba7b
25
YTSearch.sln
Normal file
25
YTSearch.sln
Normal file
@ -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
|
||||||
90
YTSearch/AppServices/QuotaService.cs
Normal file
90
YTSearch/AppServices/QuotaService.cs
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
using MongoDB.Driver;
|
||||||
|
using YTSearch.Contracts;
|
||||||
|
using YTSearch.Models;
|
||||||
|
|
||||||
|
namespace YTSearch.AppServices
|
||||||
|
{
|
||||||
|
public class QuotaService : IQuotaService
|
||||||
|
{
|
||||||
|
private readonly IMongoCollection<QuotaLimit> _quotaCollection;
|
||||||
|
private const int SEARCH_COST = 100;
|
||||||
|
private const int VIDEO_DETAILS_COST = 1;
|
||||||
|
|
||||||
|
public QuotaService(IMongoDatabase database)
|
||||||
|
{
|
||||||
|
_quotaCollection = database.GetCollection<QuotaLimit>("quota_limits");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanUseQuotaAsync(int cost)
|
||||||
|
{
|
||||||
|
await ResetQuotaIfNeededAsync();
|
||||||
|
var quota = await GetCurrentQuotaAsync();
|
||||||
|
return (quota.CurrentUsage + cost) <= quota.DailyLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UseQuotaAsync(int cost)
|
||||||
|
{
|
||||||
|
var filter = Builders<QuotaLimit>.Filter.Eq(x => x.Id, "youtube_quota");
|
||||||
|
var update = Builders<QuotaLimit>.Update
|
||||||
|
.Inc(x => x.CurrentUsage, cost)
|
||||||
|
.Set(x => x.LastUpdated, DateTime.UtcNow);
|
||||||
|
|
||||||
|
await _quotaCollection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<QuotaStatus> 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<QuotaLimit>.Filter.Eq(x => x.Id, "youtube_quota");
|
||||||
|
var update = Builders<QuotaLimit>.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<QuotaLimit>.Filter.Eq(x => x.Id, "youtube_quota");
|
||||||
|
var update = Builders<QuotaLimit>.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<QuotaLimit> GetCurrentQuotaAsync()
|
||||||
|
{
|
||||||
|
var quota = await _quotaCollection.Find(x => x.Id == "youtube_quota").FirstOrDefaultAsync();
|
||||||
|
return quota ?? new QuotaLimit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
166
YTSearch/AppServices/YouTubeSearchService.cs
Normal file
166
YTSearch/AppServices/YouTubeSearchService.cs
Normal file
@ -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<SearchResponse> 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<SearchResponse> 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<SearchResponse> 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<VideoResult>();
|
||||||
|
|
||||||
|
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<VideoResult>(),
|
||||||
|
SearchMethod = "YoutubeExplode",
|
||||||
|
Message = $"YoutubeExplode search failed: {ex.Message}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
YTSearch/Contracts/IQuotaService.cs
Normal file
13
YTSearch/Contracts/IQuotaService.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using YTSearch.Models;
|
||||||
|
|
||||||
|
namespace YTSearch.Contracts
|
||||||
|
{
|
||||||
|
public interface IQuotaService
|
||||||
|
{
|
||||||
|
Task<bool> CanUseQuotaAsync(int cost);
|
||||||
|
Task UseQuotaAsync(int cost);
|
||||||
|
Task<QuotaStatus> GetQuotaStatusAsync();
|
||||||
|
Task UpdateDailyLimitAsync(long newLimit);
|
||||||
|
Task ResetQuotaIfNeededAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
9
YTSearch/Contracts/IYouTubeSearchService.cs
Normal file
9
YTSearch/Contracts/IYouTubeSearchService.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using YTSearch.Models;
|
||||||
|
|
||||||
|
namespace YTSearch.Contracts
|
||||||
|
{
|
||||||
|
public interface IYouTubeSearchService
|
||||||
|
{
|
||||||
|
Task<SearchResponse> SearchVideosAsync(SearchRequest request);
|
||||||
|
}
|
||||||
|
}
|
||||||
65
YTSearch/Controllers/QuotaController.cs
Normal file
65
YTSearch/Controllers/QuotaController.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get current quota status
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("status")]
|
||||||
|
public async Task<ActionResult<QuotaStatus>> GetQuotaStatus()
|
||||||
|
{
|
||||||
|
var status = await _quotaService.GetQuotaStatusAsync();
|
||||||
|
return Ok(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update daily quota limit
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("update-limit")]
|
||||||
|
public async Task<ActionResult> 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Force reset quota (for testing purposes)
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("reset")]
|
||||||
|
public async Task<ActionResult> ResetQuota()
|
||||||
|
{
|
||||||
|
await _quotaService.ResetQuotaIfNeededAsync();
|
||||||
|
var status = await _quotaService.GetQuotaStatusAsync();
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
message = "Quota reset completed",
|
||||||
|
status = status
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
YTSearch/Controllers/YouTubeController.cs
Normal file
73
YTSearch/Controllers/YouTubeController.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Search for YouTube videos by keywords
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keywords">Search keywords</param>
|
||||||
|
/// <param name="pageToken">Token for pagination (optional)</param>
|
||||||
|
/// <param name="maxResults">Maximum number of results (default: 20, max: 50)</param>
|
||||||
|
/// <returns>List of videos with details</returns>
|
||||||
|
[HttpGet("search")]
|
||||||
|
public async Task<ActionResult<SearchResponse>> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Search for YouTube videos using POST method
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("search")]
|
||||||
|
public async Task<ActionResult<SearchResponse>> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
YTSearch/Models/QuotaLimit.cs
Normal file
11
YTSearch/Models/QuotaLimit.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
YTSearch/Models/QuotaStatus.cs
Normal file
12
YTSearch/Models/QuotaStatus.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
YTSearch/Models/QuotaUpdateRequest.cs
Normal file
8
YTSearch/Models/QuotaUpdateRequest.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace YTSearch.Models
|
||||||
|
{
|
||||||
|
public class QuotaUpdateRequest
|
||||||
|
{
|
||||||
|
public long NewDailyLimit { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
9
YTSearch/Models/SearchRequest.cs
Normal file
9
YTSearch/Models/SearchRequest.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
YTSearch/Models/SearchResponse.cs
Normal file
11
YTSearch/Models/SearchResponse.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace YTSearch.Models
|
||||||
|
{
|
||||||
|
public class SearchResponse
|
||||||
|
{
|
||||||
|
public List<VideoResult> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
YTSearch/Models/VideoResult.cs
Normal file
17
YTSearch/Models/VideoResult.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
44
YTSearch/Program.cs
Normal file
44
YTSearch/Program.cs
Normal file
@ -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<IMongoClient>(serviceProvider =>
|
||||||
|
{
|
||||||
|
var connectionString = builder.Configuration.GetConnectionString("MongoDB") ?? "mongodb://localhost:27017";
|
||||||
|
return new MongoClient(connectionString);
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddScoped(serviceProvider =>
|
||||||
|
{
|
||||||
|
var client = serviceProvider.GetService<IMongoClient>();
|
||||||
|
return client.GetDatabase("YouTubeSearchDB");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register Services
|
||||||
|
builder.Services.AddScoped<IQuotaService, QuotaService>();
|
||||||
|
builder.Services.AddScoped<IYouTubeSearchService, YouTubeSearchService>();
|
||||||
|
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();
|
||||||
41
YTSearch/Properties/launchSettings.json
Normal file
41
YTSearch/Properties/launchSettings.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
YTSearch/YTSearch.csproj
Normal file
17
YTSearch/YTSearch.csproj
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.69.0.3764" />
|
||||||
|
<PackageReference Include="MongoDB.Driver" Version="3.4.0" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||||
|
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||||
|
<PackageReference Include="YoutubeExplode" Version="6.5.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
6
YTSearch/YTSearch.http
Normal file
6
YTSearch/YTSearch.http
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
@YTSearch_HostAddress = http://localhost:5044
|
||||||
|
|
||||||
|
GET {{YTSearch_HostAddress}}/weatherforecast/
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
8
YTSearch/appsettings.Development.json
Normal file
8
YTSearch/appsettings.Development.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
YTSearch/appsettings.json
Normal file
12
YTSearch/appsettings.json
Normal file
@ -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": "*"
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user