Merge pull request #87 from MBR-0001/apollo

This commit is contained in:
Cody Robibero 2021-12-07 15:37:38 -07:00 committed by GitHub
commit ee797cbc81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 86 additions and 115 deletions

View File

@ -199,12 +199,10 @@ namespace Jellyfin.Plugin.OpenSubtitles
Comment = i.Attributes?.Comments, Comment = i.Attributes?.Comments,
CommunityRating = i.Attributes?.Ratings, CommunityRating = i.Attributes?.Ratings,
DownloadCount = i.Attributes?.DownloadCount, DownloadCount = i.Attributes?.DownloadCount,
Format = i.Attributes?.Format ?? "srt", Format = "srt",
ProviderName = Name, ProviderName = Name,
ThreeLetterISOLanguageName = request.Language, ThreeLetterISOLanguageName = request.Language,
Id = $"srt-{request.Language}-{i.Attributes?.Files[0].FileId}",
// new API (currently) does not return the format
Id = $"{i.Attributes?.Format ?? "srt"}-{request.Language}-{i.Attributes?.Files[0].FileId}",
Name = i.Attributes?.Release, Name = i.Attributes?.Release,
DateCreated = i.Attributes?.UploadDate, DateCreated = i.Attributes?.UploadDate,
IsHashMatch = i.Attributes?.MovieHashMatch IsHashMatch = i.Attributes?.MovieHashMatch
@ -314,7 +312,7 @@ namespace Jellyfin.Plugin.OpenSubtitles
var res = await OpenSubtitlesHandler.OpenSubtitles.DownloadSubtitleAsync(info.Data.Link, cancellationToken).ConfigureAwait(false); var res = await OpenSubtitlesHandler.OpenSubtitles.DownloadSubtitleAsync(info.Data.Link, cancellationToken).ConfigureAwait(false);
if (!res.Ok || string.IsNullOrWhiteSpace(res.Data)) if (res.Code != HttpStatusCode.OK || string.IsNullOrWhiteSpace(res.Body))
{ {
var msg = string.Format( var msg = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
@ -325,7 +323,7 @@ namespace Jellyfin.Plugin.OpenSubtitles
throw new HttpRequestException(msg); throw new HttpRequestException(msg);
} }
return new SubtitleResponse { Format = format, Language = language, Stream = new MemoryStream(Encoding.UTF8.GetBytes(res.Data)) }; return new SubtitleResponse { Format = format, Language = language, Stream = new MemoryStream(Encoding.UTF8.GetBytes(res.Body)) };
} }
private async Task Login(CancellationToken cancellationToken) private async Task Login(CancellationToken cancellationToken)

View File

@ -42,12 +42,6 @@ namespace Jellyfin.Plugin.OpenSubtitles.OpenSubtitlesHandler.Models
Body = response.Reason; Body = response.Reason;
} }
if (typeof(T) == typeof(string))
{
Data = (T)(object)Body;
return;
}
if (!Ok) if (!Ok)
{ {
// don't bother parsing json if HTTP status code is bad // don't bother parsing json if HTTP status code is bad

View File

@ -15,12 +15,6 @@ namespace Jellyfin.Plugin.OpenSubtitles.OpenSubtitlesHandler.Models.Responses
[JsonPropertyName("download_count")] [JsonPropertyName("download_count")]
public int DownloadCount { get; set; } public int DownloadCount { get; set; }
/// <summary>
/// Gets or sets the subtitle format.
/// </summary>
[JsonPropertyName("format")]
public string? Format { get; set; }
/// <summary> /// <summary>
/// Gets or sets the subtitle rating. /// Gets or sets the subtitle rating.
/// </summary> /// </summary>

View File

@ -20,12 +20,6 @@ namespace Jellyfin.Plugin.OpenSubtitles.OpenSubtitlesHandler.Models.Responses
[JsonPropertyName("remaining")] [JsonPropertyName("remaining")]
public int Remaining { get; set; } public int Remaining { get; set; }
/// <summary>
/// Gets or sets the message.
/// </summary>
[JsonPropertyName("message")]
public string? Message { get; set; }
/// <summary> /// <summary>
/// Gets or sets the reset time. /// Gets or sets the reset time.
/// </summary> /// </summary>

View File

@ -26,7 +26,7 @@ namespace Jellyfin.Plugin.OpenSubtitles.OpenSubtitlesHandler
public static async Task<ApiResponse<LoginInfo>> LogInAsync(string username, string password, string apiKey, CancellationToken cancellationToken) public static async Task<ApiResponse<LoginInfo>> LogInAsync(string username, string password, string apiKey, CancellationToken cancellationToken)
{ {
var body = new { username, password }; var body = new { username, password };
var response = await RequestHandler.SendRequestAsync("/login", HttpMethod.Post, body, null, apiKey, cancellationToken).ConfigureAwait(false); var response = await RequestHandler.SendRequestAsync("/login", HttpMethod.Post, body, null, apiKey, 1, cancellationToken).ConfigureAwait(false);
return new ApiResponse<LoginInfo>(response); return new ApiResponse<LoginInfo>(response);
} }
@ -47,7 +47,7 @@ namespace Jellyfin.Plugin.OpenSubtitles.OpenSubtitlesHandler
var headers = new Dictionary<string, string> { { "Authorization", user.Token } }; var headers = new Dictionary<string, string> { { "Authorization", user.Token } };
var response = await RequestHandler.SendRequestAsync("/logout", HttpMethod.Delete, null, headers, apiKey, cancellationToken).ConfigureAwait(false); var response = await RequestHandler.SendRequestAsync("/logout", HttpMethod.Delete, null, headers, apiKey, 1, cancellationToken).ConfigureAwait(false);
return new ApiResponse<object>(response).Ok; return new ApiResponse<object>(response).Ok;
} }
@ -68,7 +68,7 @@ namespace Jellyfin.Plugin.OpenSubtitles.OpenSubtitlesHandler
var headers = new Dictionary<string, string> { { "Authorization", user.Token } }; var headers = new Dictionary<string, string> { { "Authorization", user.Token } };
var response = await RequestHandler.SendRequestAsync("/infos/user", HttpMethod.Get, null, headers, apiKey, cancellationToken).ConfigureAwait(false); var response = await RequestHandler.SendRequestAsync("/infos/user", HttpMethod.Get, null, headers, apiKey, 1, cancellationToken).ConfigureAwait(false);
return new ApiResponse<EncapsulatedUserInfo>(response); return new ApiResponse<EncapsulatedUserInfo>(response);
} }
@ -91,7 +91,7 @@ namespace Jellyfin.Plugin.OpenSubtitles.OpenSubtitlesHandler
var headers = new Dictionary<string, string> { { "Authorization", user.Token } }; var headers = new Dictionary<string, string> { { "Authorization", user.Token } };
var body = new { file_id = file }; var body = new { file_id = file };
var response = await RequestHandler.SendRequestAsync("/download", HttpMethod.Post, body, headers, apiKey, cancellationToken).ConfigureAwait(false); var response = await RequestHandler.SendRequestAsync("/download", HttpMethod.Post, body, headers, apiKey, 1, cancellationToken).ConfigureAwait(false);
return new ApiResponse<SubtitleDownloadInfo>(response, $"file id: {file}"); return new ApiResponse<SubtitleDownloadInfo>(response, $"file id: {file}");
} }
@ -101,12 +101,21 @@ namespace Jellyfin.Plugin.OpenSubtitles.OpenSubtitlesHandler
/// </summary> /// </summary>
/// <param name="url">the subtitle url.</param> /// <param name="url">the subtitle url.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The subtitle string.</returns> /// <returns>The Http response.</returns>
public static async Task<ApiResponse<string>> DownloadSubtitleAsync(string url, CancellationToken cancellationToken) public static async Task<HttpResponse> DownloadSubtitleAsync(string url, CancellationToken cancellationToken)
{ {
var response = await RequestHandler.SendRequestAsync(url, HttpMethod.Get, null, null, null, cancellationToken).ConfigureAwait(false); var response = await OpenSubtitlesRequestHelper.Instance!.SendRequestAsync(
url,
HttpMethod.Get,
null,
new Dictionary<string, string>(),
cancellationToken).ConfigureAwait(false);
return new ApiResponse<string>(response); return new HttpResponse
{
Body = response.body,
Code = response.statusCode
};
} }
/// <summary> /// <summary>
@ -141,7 +150,7 @@ namespace Jellyfin.Plugin.OpenSubtitles.OpenSubtitlesHandler
opts.Add(key.ToLower(CultureInfo.InvariantCulture), value.ToLower(CultureInfo.InvariantCulture)); opts.Add(key.ToLower(CultureInfo.InvariantCulture), value.ToLower(CultureInfo.InvariantCulture));
} }
response = await RequestHandler.SendRequestAsync($"/subtitles?{opts}", HttpMethod.Get, null, null, apiKey, cancellationToken).ConfigureAwait(false); response = await RequestHandler.SendRequestAsync($"/subtitles?{opts}", HttpMethod.Get, null, null, apiKey, 1, cancellationToken).ConfigureAwait(false);
last = new ApiResponse<SearchResult>(response, $"query: {opts}", $"page: {current}"); last = new ApiResponse<SearchResult>(response, $"query: {opts}", $"page: {current}");
@ -177,7 +186,7 @@ namespace Jellyfin.Plugin.OpenSubtitles.OpenSubtitlesHandler
/// <returns>The list of languages.</returns> /// <returns>The list of languages.</returns>
public static async Task<ApiResponse<EncapsulatedLanguageList>> GetLanguageList(string apiKey, CancellationToken cancellationToken) public static async Task<ApiResponse<EncapsulatedLanguageList>> GetLanguageList(string apiKey, CancellationToken cancellationToken)
{ {
var response = await RequestHandler.SendRequestAsync("/infos/languages", HttpMethod.Get, null, null, apiKey, cancellationToken).ConfigureAwait(false); var response = await RequestHandler.SendRequestAsync("/infos/languages", HttpMethod.Get, null, null, apiKey, 1, cancellationToken).ConfigureAwait(false);
return new ApiResponse<EncapsulatedLanguageList>(response); return new ApiResponse<EncapsulatedLanguageList>(response);
} }

View File

@ -79,7 +79,12 @@ namespace Jellyfin.Plugin.OpenSubtitles.OpenSubtitlesHandler
} }
} }
internal async Task<(string, Dictionary<string, string>, HttpStatusCode)> SendRequestAsync(string url, HttpMethod method, object? body, Dictionary<string, string> headers, CancellationToken cancellationToken) internal async Task<(string body, Dictionary<string, string> headers, HttpStatusCode statusCode)> SendRequestAsync(
string url,
HttpMethod method,
object? body,
Dictionary<string, string> headers,
CancellationToken cancellationToken)
{ {
var client = _clientFactory.CreateClient("Default"); var client = _clientFactory.CreateClient("Default");

View File

@ -30,96 +30,96 @@ namespace Jellyfin.Plugin.OpenSubtitles.OpenSubtitlesHandler
/// <param name="body">The request body.</param> /// <param name="body">The request body.</param>
/// <param name="headers">The headers.</param> /// <param name="headers">The headers.</param>
/// <param name="apiKey">The api key.</param> /// <param name="apiKey">The api key.</param>
/// <param name="attempt">The request attempt key.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The response.</returns> /// <returns>The response.</returns>
/// <exception cref="ArgumentException">API Key is empty.</exception> /// <exception cref="ArgumentException">API Key is empty.</exception>
public static async Task<HttpResponse> SendRequestAsync(string endpoint, HttpMethod method, object? body, Dictionary<string, string>? headers, string? apiKey, CancellationToken cancellationToken) public static async Task<HttpResponse> SendRequestAsync(
string endpoint,
HttpMethod method,
object? body,
Dictionary<string, string>? headers,
string? apiKey,
int attempt,
CancellationToken cancellationToken)
{ {
var url = endpoint.StartsWith('/') ? BaseApiUrl + endpoint : endpoint;
var isFullUrl = url.StartsWith(BaseApiUrl, StringComparison.OrdinalIgnoreCase);
headers ??= new Dictionary<string, string>(); headers ??= new Dictionary<string, string>();
if (isFullUrl) if (string.IsNullOrWhiteSpace(apiKey))
{ {
if (string.IsNullOrWhiteSpace(apiKey)) throw new ArgumentException("Provided API key is blank", nameof(apiKey));
{ }
throw new ArgumentException("Provided API key is blank", nameof(apiKey));
}
if (!headers.ContainsKey("Api-Key")) if (!headers.ContainsKey("Api-Key"))
{ {
headers.Add("Api-Key", apiKey); headers.Add("Api-Key", apiKey);
} }
if (_hRemaining == 0) if (_hRemaining == 0)
{
await Task.Delay(1000 * _hReset, cancellationToken).ConfigureAwait(false);
_hRemaining = -1;
_hReset = -1;
}
if (_requestCount == 40)
{
var diff = DateTime.UtcNow.Subtract(_windowStart).TotalSeconds;
if (diff <= 10)
{ {
await Task.Delay(1000 * _hReset, cancellationToken).ConfigureAwait(false); await Task.Delay(1000 * (int)Math.Ceiling(10 - diff), cancellationToken).ConfigureAwait(false);
_hRemaining = -1; _hRemaining = -1;
_hReset = -1; _hReset = -1;
} }
if (_requestCount == 40)
{
var diff = DateTime.UtcNow.Subtract(_windowStart).TotalSeconds;
if (diff <= 10)
{
await Task.Delay(1000 * (int)Math.Ceiling(10 - diff), cancellationToken).ConfigureAwait(false);
_hRemaining = -1;
_hReset = -1;
}
}
if (DateTime.UtcNow.Subtract(_windowStart).TotalSeconds >= 10)
{
_windowStart = DateTime.UtcNow;
_requestCount = 0;
}
} }
var (response, responseHeaders, httpStatusCode) = await OpenSubtitlesRequestHelper.Instance!.SendRequestAsync(url, method, body, headers, cancellationToken).ConfigureAwait(false); if (DateTime.UtcNow.Subtract(_windowStart).TotalSeconds >= 10)
if (!isFullUrl)
{ {
return new HttpResponse _windowStart = DateTime.UtcNow;
{ _requestCount = 0;
Body = response,
Code = httpStatusCode
};
} }
var response = await OpenSubtitlesRequestHelper.Instance!.SendRequestAsync(BaseApiUrl + endpoint, method, body, headers, cancellationToken).ConfigureAwait(false);
_requestCount++; _requestCount++;
if (responseHeaders.TryGetValue("x-ratelimit-remaining-second", out var value)) if (response.headers.TryGetValue("x-ratelimit-remaining-second", out var value))
{ {
_ = int.TryParse(value, out _hRemaining); _ = int.TryParse(value, out _hRemaining);
} }
if (responseHeaders.TryGetValue("ratelimit-reset", out value)) if (response.headers.TryGetValue("ratelimit-reset", out value))
{ {
_ = int.TryParse(value, out _hReset); _ = int.TryParse(value, out _hReset);
} }
if (httpStatusCode != HttpStatusCode.TooManyRequests) if (response.statusCode == HttpStatusCode.TooManyRequests && attempt <= 4)
{ {
if (!responseHeaders.TryGetValue("x-reason", out value)) var time = _hReset == -1 ? 5 : _hReset;
{
value = string.Empty;
}
return new HttpResponse await Task.Delay(time * 1000, cancellationToken).ConfigureAwait(false);
{
Body = response, return await SendRequestAsync(endpoint, method, body, headers, apiKey, attempt + 1, cancellationToken).ConfigureAwait(false);
Code = httpStatusCode,
Reason = value
};
} }
var time = _hReset == -1 ? 5 : _hReset; if (response.statusCode == HttpStatusCode.BadGateway && attempt <= 3)
{
await Task.Delay(500, cancellationToken).ConfigureAwait(false);
await Task.Delay(time * 1000, cancellationToken).ConfigureAwait(false); return await SendRequestAsync(endpoint, method, body, headers, apiKey, attempt + 1, cancellationToken).ConfigureAwait(false);
}
return await SendRequestAsync(endpoint, method, body, headers, apiKey, cancellationToken).ConfigureAwait(false); if (!response.headers.TryGetValue("x-reason", out value))
{
value = string.Empty;
}
return new HttpResponse
{
Body = response.body,
Code = response.statusCode,
Reason = value
};
} }
} }
} }

View File

@ -1,23 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
</Project>

View File

@ -18,7 +18,7 @@
## About ## About
This is a plugin allows you to download subtitles from [Open Subtitles](https://opensubtitles.org) for your media. This is a plugin allows you to download subtitles from [Open Subtitles](https://opensubtitles.com) for your media.
## Installation ## Installation