#nullable enable using System.Collections.Generic; using System.Threading.Tasks; using API.Data; using API.DTOs.Collection; using API.DTOs.KavitaPlus.ExternalMetadata; using API.DTOs.KavitaPlus.Metadata; using API.DTOs.Metadata.Matching; using API.DTOs.Scrobbling; using API.Entities.Enums; using API.Extensions; using Flurl.Http; using Kavita.Common; using Microsoft.Extensions.Logging; namespace API.Services.Plus; /// /// All Http requests to K+ should be contained in this service, the service will not handle any errors. /// This is expected from the caller. /// public interface IKavitaPlusApiService { Task HasTokenExpired(string license, string token, ScrobbleProvider provider); Task GetRateLimit(string license, string token); Task PostScrobbleUpdate(ScrobbleDto data, string license); Task> GetMalStacks(string malUsername, string license); Task> MatchSeries(MatchSeriesRequestDto request); Task GetSeriesDetail(PlusSeriesRequestDto request); Task GetSeriesDetailById(ExternalMetadataIdsDto request); } public class KavitaPlusApiService(ILogger logger, IUnitOfWork unitOfWork): IKavitaPlusApiService { private const string ScrobblingPath = "/api/scrobbling/"; public async Task HasTokenExpired(string license, string token, ScrobbleProvider provider) { var res = await Get(ScrobblingPath + "valid-key?provider=" + provider + "&key=" + token, license, token); var str = await res.GetStringAsync(); return bool.Parse(str); } public async Task GetRateLimit(string license, string token) { var res = await Get(ScrobblingPath + "rate-limit?accessToken=" + token, license, token); var str = await res.GetStringAsync(); return int.Parse(str); } public async Task PostScrobbleUpdate(ScrobbleDto data, string license) { return await PostAndReceive(ScrobblingPath + "update", data, license); } public async Task> GetMalStacks(string malUsername, string license) { return await $"{Configuration.KavitaPlusApiUrl}/api/metadata/v2/stacks?username={malUsername}" .WithKavitaPlusHeaders(license) .GetJsonAsync>(); } public async Task> MatchSeries(MatchSeriesRequestDto request) { var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; var token = (await unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/match-series") .WithKavitaPlusHeaders(license, token) .PostJsonAsync(request) .ReceiveJson>(); } public async Task GetSeriesDetail(PlusSeriesRequestDto request) { var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; var token = (await unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-detail") .WithKavitaPlusHeaders(license, token) .PostJsonAsync(request) .ReceiveJson(); } public async Task GetSeriesDetailById(ExternalMetadataIdsDto request) { var license = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.LicenseKey)).Value; var token = (await unitOfWork.UserRepository.GetDefaultAdminUser()).AniListAccessToken; return await (Configuration.KavitaPlusApiUrl + "/api/metadata/v2/series-by-ids") .WithKavitaPlusHeaders(license, token) .PostJsonAsync(request) .ReceiveJson(); } /// /// Send a GET request to K+ /// /// only path of the uri, the host is added /// /// /// private static async Task Get(string url, string license, string? aniListToken = null) { return await (Configuration.KavitaPlusApiUrl + url) .WithKavitaPlusHeaders(license, aniListToken) .GetAsync(); } /// /// Send a POST request to K+ /// /// only path of the uri, the host is added /// /// /// /// Return type /// private static async Task PostAndReceive(string url, object body, string license, string? aniListToken = null) { return await (Configuration.KavitaPlusApiUrl + url) .WithKavitaPlusHeaders(license, aniListToken) .PostJsonAsync(body) .ReceiveJson(); } }