mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 17:24:27 +08:00
0.7.7.1
This commit is contained in:
113
VoiceHubLanDesktop/Services/VoiceHubApiService.cs
Normal file
113
VoiceHubLanDesktop/Services/VoiceHubApiService.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using VoiceHubLanDesktop.Models;
|
||||
|
||||
namespace VoiceHubLanDesktop.Services;
|
||||
|
||||
/// <summary>
|
||||
/// VoiceHub API 服务
|
||||
/// </summary>
|
||||
public sealed class VoiceHubApiService : IDisposable
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
private const string DefaultApiUrl = "https://voicehub.lao-shui.top/api/songs/public";
|
||||
private const int MaxRetryCount = 3;
|
||||
private readonly TimeSpan _requestTimeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
public VoiceHubApiService()
|
||||
{
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
|
||||
_jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取公开排期数据
|
||||
/// </summary>
|
||||
public async Task<ApiResult<IReadOnlyList<SongItem>>> GetPublicScheduleAsync(
|
||||
string? apiUrl = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var url = string.IsNullOrWhiteSpace(apiUrl) ? DefaultApiUrl : apiUrl.Trim();
|
||||
|
||||
for (var attempt = 0; attempt < MaxRetryCount; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cts.CancelAfter(_requestTimeout);
|
||||
|
||||
var jsonResponse = await _httpClient.GetStringAsync(url, cts.Token);
|
||||
var items = JsonSerializer.Deserialize<List<SongItem>>(jsonResponse, _jsonOptions);
|
||||
|
||||
if (items is null)
|
||||
{
|
||||
return ApiResult<IReadOnlyList<SongItem>>.Failure("数据解析失败");
|
||||
}
|
||||
|
||||
return ApiResult<IReadOnlyList<SongItem>>.Success(items);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
if (attempt == MaxRetryCount - 1)
|
||||
{
|
||||
return ApiResult<IReadOnlyList<SongItem>>.Failure($"网络错误: {ex.Message}");
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
if (attempt == MaxRetryCount - 1)
|
||||
{
|
||||
return ApiResult<IReadOnlyList<SongItem>>.Failure("请求超时");
|
||||
}
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
return ApiResult<IReadOnlyList<SongItem>>.Failure($"数据格式错误: {ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ApiResult<IReadOnlyList<SongItem>>.Failure($"未知错误: {ex.Message}");
|
||||
}
|
||||
|
||||
// 指数退避
|
||||
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)), cancellationToken);
|
||||
}
|
||||
|
||||
return ApiResult<IReadOnlyList<SongItem>>.Failure("获取数据失败");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// API 结果
|
||||
/// </summary>
|
||||
public sealed class ApiResult<T>
|
||||
{
|
||||
public bool IsSuccess { get; }
|
||||
public T? Data { get; }
|
||||
public string? ErrorMessage { get; }
|
||||
|
||||
private ApiResult(bool isSuccess, T? data, string? errorMessage)
|
||||
{
|
||||
IsSuccess = isSuccess;
|
||||
Data = data;
|
||||
ErrorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public static ApiResult<T> Success(T data) => new(true, data, null);
|
||||
public static ApiResult<T> Failure(string errorMessage) => new(false, default, errorMessage);
|
||||
}
|
||||
164
VoiceHubLanDesktop/Services/VoiceHubScheduleService.cs
Normal file
164
VoiceHubLanDesktop/Services/VoiceHubScheduleService.cs
Normal file
@@ -0,0 +1,164 @@
|
||||
using VoiceHubLanDesktop.Models;
|
||||
|
||||
namespace VoiceHubLanDesktop.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 排期管理服务
|
||||
/// </summary>
|
||||
public sealed class VoiceHubScheduleService
|
||||
{
|
||||
private readonly VoiceHubApiService _apiService;
|
||||
private readonly VoiceHubSettingsService _settingsService;
|
||||
private IReadOnlyList<SongItem> _cachedSchedule = [];
|
||||
private DateTime _cacheTime = DateTime.MinValue;
|
||||
private readonly TimeSpan _cacheExpiry = TimeSpan.FromMinutes(5);
|
||||
|
||||
public event EventHandler<ScheduleUpdatedEventArgs>? ScheduleUpdated;
|
||||
|
||||
public VoiceHubScheduleService(VoiceHubApiService apiService, VoiceHubSettingsService settingsService)
|
||||
{
|
||||
_apiService = apiService;
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取今日排期
|
||||
/// </summary>
|
||||
public async Task<DisplayData> GetTodayScheduleAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var settings = _settingsService.GetSettings();
|
||||
|
||||
// 检查缓存
|
||||
if (_cachedSchedule.Count > 0 && DateTime.Now - _cacheTime < _cacheExpiry)
|
||||
{
|
||||
return BuildDisplayData(_cachedSchedule);
|
||||
}
|
||||
|
||||
// 从 API 获取
|
||||
var result = await _apiService.GetPublicScheduleAsync(settings.ApiUrl, cancellationToken);
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
return new DisplayData
|
||||
{
|
||||
State = ComponentState.NetworkError,
|
||||
ErrorMessage = result.ErrorMessage ?? "获取排期失败"
|
||||
};
|
||||
}
|
||||
|
||||
var items = result.Data ?? [];
|
||||
|
||||
// 更新缓存
|
||||
_cachedSchedule = items;
|
||||
_cacheTime = DateTime.Now;
|
||||
|
||||
return BuildDisplayData(items);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 强制刷新
|
||||
/// </summary>
|
||||
public async Task<DisplayData> RefreshAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
_cachedSchedule = [];
|
||||
_cacheTime = DateTime.MinValue;
|
||||
return await GetTodayScheduleAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除缓存
|
||||
/// </summary>
|
||||
public void ClearCache()
|
||||
{
|
||||
_cachedSchedule = [];
|
||||
_cacheTime = DateTime.MinValue;
|
||||
}
|
||||
|
||||
private DisplayData BuildDisplayData(IReadOnlyList<SongItem> items)
|
||||
{
|
||||
if (items.Count == 0)
|
||||
{
|
||||
return new DisplayData
|
||||
{
|
||||
State = ComponentState.NoSchedule,
|
||||
ErrorMessage = "暂无排期数据"
|
||||
};
|
||||
}
|
||||
|
||||
// 过滤有效日期
|
||||
var validItems = items.Where(s => s.GetPlayDate() != DateTime.MinValue).ToList();
|
||||
|
||||
if (validItems.Count == 0)
|
||||
{
|
||||
return new DisplayData
|
||||
{
|
||||
State = ComponentState.NoSchedule,
|
||||
ErrorMessage = "暂无有效排期数据"
|
||||
};
|
||||
}
|
||||
|
||||
// 找到今天或最近未来的排期
|
||||
var today = DateTime.Today;
|
||||
var todaySchedule = validItems
|
||||
.Where(s => s.GetPlayDate() == today)
|
||||
.OrderBy(s => s.Sequence)
|
||||
.ToList();
|
||||
|
||||
List<SongItem> displayItems;
|
||||
DateTime actualDate;
|
||||
|
||||
if (todaySchedule.Count > 0)
|
||||
{
|
||||
displayItems = todaySchedule;
|
||||
actualDate = today;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 找最近的未来排期
|
||||
var futureSchedule = validItems
|
||||
.Where(s => s.GetPlayDate() > today)
|
||||
.GroupBy(s => s.GetPlayDate())
|
||||
.OrderBy(g => g.Key)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (futureSchedule != null)
|
||||
{
|
||||
displayItems = futureSchedule.OrderBy(s => s.Sequence).ToList();
|
||||
actualDate = futureSchedule.Key;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new DisplayData
|
||||
{
|
||||
State = ComponentState.NoSchedule,
|
||||
ErrorMessage = "暂无排期数据"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 触发更新事件
|
||||
ScheduleUpdated?.Invoke(this, new ScheduleUpdatedEventArgs(displayItems, actualDate));
|
||||
|
||||
return new DisplayData
|
||||
{
|
||||
State = ComponentState.Normal,
|
||||
Songs = displayItems,
|
||||
DisplayDate = actualDate
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 排期更新事件参数
|
||||
/// </summary>
|
||||
public sealed class ScheduleUpdatedEventArgs : EventArgs
|
||||
{
|
||||
public IReadOnlyList<SongItem> Songs { get; }
|
||||
public DateTime DisplayDate { get; }
|
||||
|
||||
public ScheduleUpdatedEventArgs(IReadOnlyList<SongItem> songs, DateTime displayDate)
|
||||
{
|
||||
Songs = songs;
|
||||
DisplayDate = displayDate;
|
||||
}
|
||||
}
|
||||
97
VoiceHubLanDesktop/Services/VoiceHubSettingsService.cs
Normal file
97
VoiceHubLanDesktop/Services/VoiceHubSettingsService.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using VoiceHubLanDesktop.Models;
|
||||
|
||||
namespace VoiceHubLanDesktop.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 插件设置服务
|
||||
/// </summary>
|
||||
public sealed class VoiceHubSettingsService
|
||||
{
|
||||
private readonly IPluginSettingsService _settingsService;
|
||||
private const string SettingsSectionId = "voicehub-settings";
|
||||
private PluginSettings? _cachedSettings;
|
||||
|
||||
public event EventHandler<PluginSettings>? SettingsChanged;
|
||||
|
||||
public VoiceHubSettingsService(IPluginSettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取设置
|
||||
/// </summary>
|
||||
public PluginSettings GetSettings()
|
||||
{
|
||||
if (_cachedSettings != null)
|
||||
{
|
||||
return _cachedSettings;
|
||||
}
|
||||
|
||||
var settings = new PluginSettings();
|
||||
|
||||
try
|
||||
{
|
||||
var apiUrl = _settingsService.GetValue<string>(SettingsScope.Plugin, "apiUrl", SettingsSectionId);
|
||||
if (!string.IsNullOrWhiteSpace(apiUrl))
|
||||
{
|
||||
settings.ApiUrl = apiUrl;
|
||||
}
|
||||
|
||||
var showRequester = _settingsService.GetValue<bool?>(SettingsScope.Plugin, "showRequester", SettingsSectionId);
|
||||
if (showRequester.HasValue)
|
||||
{
|
||||
settings.ShowRequester = showRequester.Value;
|
||||
}
|
||||
|
||||
var showVoteCount = _settingsService.GetValue<bool?>(SettingsScope.Plugin, "showVoteCount", SettingsSectionId);
|
||||
if (showVoteCount.HasValue)
|
||||
{
|
||||
settings.ShowVoteCount = showVoteCount.Value;
|
||||
}
|
||||
|
||||
var refreshInterval = _settingsService.GetValue<string>(SettingsScope.Plugin, "refreshInterval", SettingsSectionId);
|
||||
if (!string.IsNullOrWhiteSpace(refreshInterval) && int.TryParse(refreshInterval, out var minutes))
|
||||
{
|
||||
settings.RefreshIntervalMinutes = minutes;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 使用默认值
|
||||
}
|
||||
|
||||
_cachedSettings = settings;
|
||||
return settings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存设置
|
||||
/// </summary>
|
||||
public void SaveSettings(PluginSettings settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
_settingsService.SetValue(SettingsScope.Plugin, "apiUrl", settings.ApiUrl, sectionId: SettingsSectionId);
|
||||
_settingsService.SetValue(SettingsScope.Plugin, "showRequester", settings.ShowRequester, sectionId: SettingsSectionId);
|
||||
_settingsService.SetValue(SettingsScope.Plugin, "showVoteCount", settings.ShowVoteCount, sectionId: SettingsSectionId);
|
||||
_settingsService.SetValue(SettingsScope.Plugin, "refreshInterval", settings.RefreshIntervalMinutes.ToString(), sectionId: SettingsSectionId);
|
||||
|
||||
_cachedSettings = settings;
|
||||
SettingsChanged?.Invoke(this, settings);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略保存错误
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除缓存
|
||||
/// </summary>
|
||||
public void ClearCache()
|
||||
{
|
||||
_cachedSettings = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user