Integrated news headline functions.
This commit is contained in:
parent
7f8d6a8131
commit
f85779fde8
@ -15,6 +15,7 @@ builder.Services.AddSingleton<IUserInterestStore, PostgresUserInterestStore>();
|
|||||||
builder.Services.AddSingleton<FinBertScoringService>();
|
builder.Services.AddSingleton<FinBertScoringService>();
|
||||||
builder.Services.AddSingleton<INewsSentimentStore, PostgresNewsSentimentStore>();
|
builder.Services.AddSingleton<INewsSentimentStore, PostgresNewsSentimentStore>();
|
||||||
builder.Services.AddSingleton<NewsSentimentService>();
|
builder.Services.AddSingleton<NewsSentimentService>();
|
||||||
|
builder.Services.AddHostedService<NewsSentimentRefreshService>();
|
||||||
|
|
||||||
builder.Services.AddCors(options =>
|
builder.Services.AddCors(options =>
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
using Backend.Interface;
|
||||||
using Backend.ServiceHander;
|
using Backend.ServiceHander;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -7,11 +8,20 @@ namespace Backend.Controller
|
|||||||
[Route("api/news-sentiment")]
|
[Route("api/news-sentiment")]
|
||||||
public sealed class NewsSentimentController : ControllerBase
|
public sealed class NewsSentimentController : ControllerBase
|
||||||
{
|
{
|
||||||
|
private static DateTime _lastRefresh = DateTime.MinValue;
|
||||||
|
private static readonly Lock _refreshLock = new();
|
||||||
private readonly NewsSentimentService _service;
|
private readonly NewsSentimentService _service;
|
||||||
|
private readonly IUserInterestStore _interests;
|
||||||
|
private readonly INewsSentimentStore _store;
|
||||||
|
|
||||||
public NewsSentimentController(NewsSentimentService service)
|
public NewsSentimentController(
|
||||||
|
NewsSentimentService service,
|
||||||
|
IUserInterestStore interests,
|
||||||
|
INewsSentimentStore store)
|
||||||
{
|
{
|
||||||
_service = service;
|
_service = service;
|
||||||
|
_interests = interests;
|
||||||
|
_store = store;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@ -30,6 +40,7 @@ namespace Backend.Controller
|
|||||||
title = r.Title,
|
title = r.Title,
|
||||||
score = r.Score,
|
score = r.Score,
|
||||||
sentimentLabel = r.SentimentLabel,
|
sentimentLabel = r.SentimentLabel,
|
||||||
|
publisher = r.Publisher,
|
||||||
url = r.Url
|
url = r.Url
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -42,5 +53,91 @@ namespace Backend.Controller
|
|||||||
return StatusCode(500, new { error = ex.ToString() });
|
return StatusCode(500, new { error = ex.ToString() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("feed")]
|
||||||
|
public async Task<IActionResult> Feed(
|
||||||
|
[FromQuery] string user,
|
||||||
|
[FromQuery] string? keyword = null,
|
||||||
|
[FromQuery(Name = "request-amount")] int requestAmount = 20,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(user))
|
||||||
|
return BadRequest("User cannot be empty.");
|
||||||
|
|
||||||
|
if (requestAmount < 1)
|
||||||
|
requestAmount = 1;
|
||||||
|
|
||||||
|
List<string> keywords;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(keyword))
|
||||||
|
{
|
||||||
|
keywords = [keyword.Trim()];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var symbols = await _interests.FetchAllAsync(user, "symbol");
|
||||||
|
var topics = await _interests.FetchAllAsync(user, "topic");
|
||||||
|
keywords = symbols.Concat(topics).Distinct().ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keywords.Count == 0)
|
||||||
|
return Ok(Array.Empty<object>());
|
||||||
|
|
||||||
|
var results = await _store.GetFeedAsync(keywords, requestAmount, cancellationToken);
|
||||||
|
|
||||||
|
return Ok(results.Select(r => new
|
||||||
|
{
|
||||||
|
title = r.Title,
|
||||||
|
publisher = r.Publisher,
|
||||||
|
score = r.Score,
|
||||||
|
publishedAt = r.PublishedAt.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssZ"),
|
||||||
|
url = r.Url
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
catch (ArgumentException ex)
|
||||||
|
{
|
||||||
|
return BadRequest(ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, new { error = ex.ToString() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[HttpGet("refresh")]
|
||||||
|
public async Task<ActionResult<BoolResponse>> Refresh(
|
||||||
|
[FromQuery] string user,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(user))
|
||||||
|
return BadRequest("User cannot be empty.");
|
||||||
|
|
||||||
|
// 5 second refresh cooldown - abuse protection
|
||||||
|
lock (_refreshLock)
|
||||||
|
{
|
||||||
|
if ((DateTime.UtcNow - _lastRefresh).TotalSeconds < 5)
|
||||||
|
return Ok(new BoolResponse { Success = false });
|
||||||
|
_lastRefresh = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
var symbols = await _interests.FetchAllAsync(user, "symbol");
|
||||||
|
var topics = await _interests.FetchAllAsync(user, "topic");
|
||||||
|
var keywords = symbols.Concat(topics).Distinct().ToList();
|
||||||
|
|
||||||
|
foreach (var keyword in keywords)
|
||||||
|
{
|
||||||
|
await _service.AnalyzeAsync(keyword, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new BoolResponse { Success = true });
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return Ok(new BoolResponse { Success = false });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,13 +35,15 @@ namespace Backend.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("add")]
|
[HttpGet("add")]
|
||||||
public async Task<ActionResult<BoolResponse>> Add(
|
public async Task<ActionResult<BoolResponse>> Add(
|
||||||
[FromBody] UserInterestModifyRequest request)
|
[FromQuery] string user,
|
||||||
|
[FromQuery] string type,
|
||||||
|
[FromQuery] string value)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
bool success = await _store.AddAsync(request.User, request.Type, request.Value);
|
bool success = await _store.AddAsync(user, type, value);
|
||||||
return Ok(new BoolResponse { Success = success });
|
return Ok(new BoolResponse { Success = success });
|
||||||
}
|
}
|
||||||
catch (ArgumentException ex)
|
catch (ArgumentException ex)
|
||||||
@ -50,13 +52,15 @@ namespace Backend.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("remove")]
|
[HttpGet("remove")]
|
||||||
public async Task<ActionResult<BoolResponse>> Remove(
|
public async Task<ActionResult<BoolResponse>> Remove(
|
||||||
[FromBody] UserInterestModifyRequest request)
|
[FromQuery] string user,
|
||||||
|
[FromQuery] string type,
|
||||||
|
[FromQuery] string value)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
bool success = await _store.RemoveAsync(request.User, request.Type, request.Value);
|
bool success = await _store.RemoveAsync(user, type, value);
|
||||||
return Ok(new BoolResponse { Success = success });
|
return Ok(new BoolResponse { Success = success });
|
||||||
}
|
}
|
||||||
catch (ArgumentException ex)
|
catch (ArgumentException ex)
|
||||||
|
|||||||
@ -94,11 +94,14 @@ Console.WriteLine(raw);
|
|||||||
// Intraday fraction: (6.5h × 5 days) / (24h × 7 days) ≈ 0.194
|
// Intraday fraction: (6.5h × 5 days) / (24h × 7 days) ≈ 0.194
|
||||||
// Daily/weekly fraction (weekdays only): 5 / 7 ≈ 0.714
|
// Daily/weekly fraction (weekdays only): 5 / 7 ≈ 0.714
|
||||||
// Monthly bars already map 1:1 to calendar months, no adjustment needed.
|
// Monthly bars already map 1:1 to calendar months, no adjustment needed.
|
||||||
|
// NYSE has ~252 trading days per year (weekdays minus ~10 holidays).
|
||||||
|
// Using 252/365 instead of 5/7 avoids overestimating for 3+ month windows
|
||||||
|
// where accumulated holidays cause the cache check to fail.
|
||||||
double marketFraction = barSize switch
|
double marketFraction = barSize switch
|
||||||
{
|
{
|
||||||
HistoricalBarSize.OneMonth => 1.0,
|
HistoricalBarSize.OneMonth => 1.0,
|
||||||
HistoricalBarSize.OneDay or HistoricalBarSize.FiveDays => 5.0 / 7.0,
|
HistoricalBarSize.OneDay or HistoricalBarSize.FiveDays => 252.0 / 365.0,
|
||||||
_ => (6.5 * 5.0) / (24.0 * 7.0)
|
_ => (6.5 * 252.0) / (24.0 * 365.0)
|
||||||
};
|
};
|
||||||
|
|
||||||
return Math.Max(1, (int)Math.Floor(duration.TotalSeconds * marketFraction / secondsPerBar));
|
return Math.Max(1, (int)Math.Floor(duration.TotalSeconds * marketFraction / secondsPerBar));
|
||||||
|
|||||||
@ -9,6 +9,7 @@ namespace Backend.DatabaseHandler
|
|||||||
public double Score { get; init; }
|
public double Score { get; init; }
|
||||||
public string SentimentLabel { get; init; } = string.Empty;
|
public string SentimentLabel { get; init; } = string.Empty;
|
||||||
public string TitleHash { get; init; } = string.Empty;
|
public string TitleHash { get; init; } = string.Empty;
|
||||||
|
public string Publisher { get; init; } = string.Empty;
|
||||||
public string Url { get; init; } = string.Empty;
|
public string Url { get; init; } = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ namespace Backend.DatabaseHandler
|
|||||||
|
|
||||||
const string sql = """
|
const string sql = """
|
||||||
SELECT source, keyword, published_at, title, score,
|
SELECT source, keyword, published_at, title, score,
|
||||||
sentiment_label, title_hash, url
|
sentiment_label, title_hash, publisher, url
|
||||||
FROM news_sentiment_cache
|
FROM news_sentiment_cache
|
||||||
WHERE keyword = @keyword
|
WHERE keyword = @keyword
|
||||||
ORDER BY published_at DESC;
|
ORDER BY published_at DESC;
|
||||||
@ -45,7 +45,55 @@ namespace Backend.DatabaseHandler
|
|||||||
Score = reader.GetDouble(4),
|
Score = reader.GetDouble(4),
|
||||||
SentimentLabel = reader.GetString(5),
|
SentimentLabel = reader.GetString(5),
|
||||||
TitleHash = reader.GetString(6).TrimEnd(),
|
TitleHash = reader.GetString(6).TrimEnd(),
|
||||||
Url = reader.GetString(7)
|
Publisher = reader.GetString(7),
|
||||||
|
Url = reader.GetString(8)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<NewsSentimentRecord>> GetFeedAsync(
|
||||||
|
IEnumerable<string> keywords,
|
||||||
|
int limit,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var keywordList = keywords.ToList();
|
||||||
|
if (keywordList.Count == 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var results = new List<NewsSentimentRecord>();
|
||||||
|
|
||||||
|
await using var conn = new NpgsqlConnection(_connectionString);
|
||||||
|
await conn.OpenAsync(cancellationToken);
|
||||||
|
|
||||||
|
const string sql = """
|
||||||
|
SELECT source, keyword, published_at, title, score,
|
||||||
|
sentiment_label, title_hash, publisher, url
|
||||||
|
FROM news_sentiment_cache
|
||||||
|
WHERE keyword = ANY(@keywords)
|
||||||
|
ORDER BY published_at DESC
|
||||||
|
LIMIT @limit;
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||||
|
cmd.Parameters.AddWithValue("keywords", keywordList.ToArray());
|
||||||
|
cmd.Parameters.AddWithValue("limit", limit);
|
||||||
|
|
||||||
|
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
|
||||||
|
while (await reader.ReadAsync(cancellationToken))
|
||||||
|
{
|
||||||
|
results.Add(new NewsSentimentRecord
|
||||||
|
{
|
||||||
|
Source = reader.GetString(0),
|
||||||
|
Keyword = reader.GetString(1),
|
||||||
|
PublishedAt = reader.GetFieldValue<DateTimeOffset>(2),
|
||||||
|
Title = reader.GetString(3),
|
||||||
|
Score = reader.GetDouble(4),
|
||||||
|
SentimentLabel = reader.GetString(5),
|
||||||
|
TitleHash = reader.GetString(6).TrimEnd(),
|
||||||
|
Publisher = reader.GetString(7),
|
||||||
|
Url = reader.GetString(8)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,10 +116,10 @@ namespace Backend.DatabaseHandler
|
|||||||
const string sql = """
|
const string sql = """
|
||||||
INSERT INTO news_sentiment_cache
|
INSERT INTO news_sentiment_cache
|
||||||
(source, keyword, published_at, title, score,
|
(source, keyword, published_at, title, score,
|
||||||
sentiment_label, title_hash, url)
|
sentiment_label, title_hash, publisher, url)
|
||||||
VALUES
|
VALUES
|
||||||
(@source, @keyword, @published_at, @title, @score,
|
(@source, @keyword, @published_at, @title, @score,
|
||||||
@sentiment_label, @title_hash, @url)
|
@sentiment_label, @title_hash, @publisher, @url)
|
||||||
ON CONFLICT (title_hash) DO NOTHING;
|
ON CONFLICT (title_hash) DO NOTHING;
|
||||||
""";
|
""";
|
||||||
|
|
||||||
@ -85,6 +133,7 @@ namespace Backend.DatabaseHandler
|
|||||||
cmd.Parameters.AddWithValue("score", record.Score);
|
cmd.Parameters.AddWithValue("score", record.Score);
|
||||||
cmd.Parameters.AddWithValue("sentiment_label", record.SentimentLabel);
|
cmd.Parameters.AddWithValue("sentiment_label", record.SentimentLabel);
|
||||||
cmd.Parameters.AddWithValue("title_hash", record.TitleHash);
|
cmd.Parameters.AddWithValue("title_hash", record.TitleHash);
|
||||||
|
cmd.Parameters.AddWithValue("publisher", record.Publisher);
|
||||||
cmd.Parameters.AddWithValue("url", record.Url);
|
cmd.Parameters.AddWithValue("url", record.Url);
|
||||||
|
|
||||||
await cmd.ExecuteNonQueryAsync(cancellationToken);
|
await cmd.ExecuteNonQueryAsync(cancellationToken);
|
||||||
|
|||||||
@ -49,6 +49,29 @@ public sealed class PostgresUserInterestStore : IUserInterestStore
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<string>> FetchAllDistinctValuesAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SELECT symbol AS value FROM user_interest_symbols
|
||||||
|
UNION
|
||||||
|
SELECT topic AS value FROM user_interest_topics;
|
||||||
|
""";
|
||||||
|
|
||||||
|
var results = new List<string>();
|
||||||
|
|
||||||
|
await using var conn = new NpgsqlConnection(_connectionString);
|
||||||
|
await conn.OpenAsync(cancellationToken);
|
||||||
|
|
||||||
|
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||||
|
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
|
||||||
|
while (await reader.ReadAsync(cancellationToken))
|
||||||
|
{
|
||||||
|
results.Add(reader.GetString(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> AddAsync(string user, string type, string value)
|
public async Task<bool> AddAsync(string user, string type, string value)
|
||||||
{
|
{
|
||||||
ValidateUser(user);
|
ValidateUser(user);
|
||||||
|
|||||||
@ -8,6 +8,11 @@ namespace Backend.Interface
|
|||||||
string keyword,
|
string keyword,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<List<NewsSentimentRecord>> GetFeedAsync(
|
||||||
|
IEnumerable<string> keywords,
|
||||||
|
int limit,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
Task InsertAsync(
|
Task InsertAsync(
|
||||||
IEnumerable<NewsSentimentRecord> records,
|
IEnumerable<NewsSentimentRecord> records,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ namespace Backend.Interface
|
|||||||
public interface IUserInterestStore
|
public interface IUserInterestStore
|
||||||
{
|
{
|
||||||
Task<List<string>> FetchAllAsync(string user, string type);
|
Task<List<string>> FetchAllAsync(string user, string type);
|
||||||
|
Task<List<string>> FetchAllDistinctValuesAsync(CancellationToken cancellationToken = default);
|
||||||
Task<bool> AddAsync(string user, string type, string value);
|
Task<bool> AddAsync(string user, string type, string value);
|
||||||
Task<bool> RemoveAsync(string user, string type, string value);
|
Task<bool> RemoveAsync(string user, string type, string value);
|
||||||
}
|
}
|
||||||
|
|||||||
78
ServiceHandler/NewsSentimentRefreshService.cs
Normal file
78
ServiceHandler/NewsSentimentRefreshService.cs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
using Backend.Interface;
|
||||||
|
|
||||||
|
namespace Backend.ServiceHander
|
||||||
|
{
|
||||||
|
public sealed class NewsSentimentRefreshService : BackgroundService
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan PollInterval = TimeSpan.FromMinutes(15);
|
||||||
|
private static readonly TimeZoneInfo Pacific = TimeZoneInfo.FindSystemTimeZoneById("America/Los_Angeles");
|
||||||
|
|
||||||
|
private readonly IServiceProvider _services;
|
||||||
|
private readonly ILogger<NewsSentimentRefreshService> _logger;
|
||||||
|
|
||||||
|
public NewsSentimentRefreshService(
|
||||||
|
IServiceProvider services,
|
||||||
|
ILogger<NewsSentimentRefreshService> logger)
|
||||||
|
{
|
||||||
|
_services = services;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
if (IsWithinActiveWindow())
|
||||||
|
{
|
||||||
|
await RefreshAsync(stoppingToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Outside active window (weekdays 07:00-20:00 PST), skipping refresh.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(PollInterval, stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsWithinActiveWindow()
|
||||||
|
{
|
||||||
|
var pacificNow = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, Pacific);
|
||||||
|
|
||||||
|
if (pacificNow.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var hour = pacificNow.Hour;
|
||||||
|
return hour >= 7 && hour < 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RefreshAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var interests = _services.GetRequiredService<IUserInterestStore>();
|
||||||
|
var sentiment = _services.GetRequiredService<NewsSentimentService>();
|
||||||
|
|
||||||
|
var keywords = await interests.FetchAllDistinctValuesAsync(cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation("Refreshing news for {Count} keywords.", keywords.Count);
|
||||||
|
|
||||||
|
foreach (var keyword in keywords)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await sentiment.AnalyzeAsync(keyword, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to refresh news for keyword '{Keyword}'.", keyword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "News sentiment refresh cycle failed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -40,17 +40,19 @@ namespace Backend.ServiceHander
|
|||||||
if (existingHashes.Contains(titleHash))
|
if (existingHashes.Contains(titleHash))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var (score, label) = _scorer.Score(title);
|
var (headline, publisher) = ParsePublisher(title);
|
||||||
|
var (score, label) = _scorer.Score(headline);
|
||||||
|
|
||||||
newRecords.Add(new NewsSentimentRecord
|
newRecords.Add(new NewsSentimentRecord
|
||||||
{
|
{
|
||||||
Source = "google_rss",
|
Source = "google_rss",
|
||||||
Keyword = keyword,
|
Keyword = keyword,
|
||||||
PublishedAt = publishedAt,
|
PublishedAt = publishedAt,
|
||||||
Title = title,
|
Title = headline,
|
||||||
Score = score,
|
Score = score,
|
||||||
SentimentLabel = label,
|
SentimentLabel = label,
|
||||||
TitleHash = titleHash,
|
TitleHash = titleHash,
|
||||||
|
Publisher = publisher,
|
||||||
Url = url
|
Url = url
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -92,6 +94,14 @@ namespace Backend.ServiceHander
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static (string Headline, string Publisher) ParsePublisher(string title)
|
||||||
|
{
|
||||||
|
int idx = title.LastIndexOf(" - ");
|
||||||
|
if (idx < 0)
|
||||||
|
return (title, "Unknown");
|
||||||
|
return (title[..idx].Trim(), title[(idx + 3)..].Trim());
|
||||||
|
}
|
||||||
|
|
||||||
private static string ComputeHash(string text)
|
private static string ComputeHash(string text)
|
||||||
{
|
{
|
||||||
byte[] bytes = SHA256.HashData(Encoding.UTF8.GetBytes(text));
|
byte[] bytes = SHA256.HashData(Encoding.UTF8.GetBytes(text));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user