fix(jellyfin): fixes in jellyfin integration

This commit is contained in:
ITQ
2026-05-22 18:04:28 +03:00
parent 5b9b4ce03a
commit 92add56cf7
16 changed files with 785 additions and 67 deletions
@@ -398,8 +398,14 @@
if (!statusEl) return;
try {
const state = await ApiClient.getJSON(ApiClient.getUrl(`MovieNight/SyncState`));
if (state && state.lastSyncAt) {
statusEl.innerText = `Last sync: ${new Date(state.lastSyncAt).toLocaleString()}`;
const states = Array.isArray(state) ? state : [];
const latest = states
.map(s => s.lastSuccessfulSyncAt || s.lastSyncedAt)
.filter(Boolean)
.sort()
.pop();
if (latest) {
statusEl.innerText = `Last sync: ${new Date(latest).toLocaleString()}`;
}
} catch (err) { /* ignore */ }
}
@@ -87,9 +87,10 @@ public class MovieNightController : ControllerBase
/// <returns>Backend response.</returns>
[HttpGet("SyncState")]
[Authorize]
public async Task<ActionResult<string>> SyncState(CancellationToken cancellationToken)
public async Task<IActionResult> SyncState(CancellationToken cancellationToken)
{
return await _backendClient.GetSyncStateAsync(cancellationToken).ConfigureAwait(false);
var body = await _backendClient.GetSyncStateAsync(cancellationToken).ConfigureAwait(false);
return Content(body, "application/json");
}
/// <summary>
@@ -97,14 +98,15 @@ public class MovieNightController : ControllerBase
/// </summary>
[HttpGet("Users/{userId}/Recommendations")]
[Authorize]
public async Task<ActionResult<string>> GetRecommendations(
public async Task<IActionResult> GetRecommendations(
[FromRoute] string userId,
[FromQuery] string? contentType,
[FromQuery] string? mood,
[FromQuery] int limit = 10,
CancellationToken cancellationToken = default)
{
return await _backendClient.GetRecommendationsAsync(userId, contentType, mood, limit, cancellationToken).ConfigureAwait(false);
var body = await _backendClient.GetRecommendationsAsync(userId, contentType, mood, limit, cancellationToken).ConfigureAwait(false);
return Content(body, "application/json");
}
/// <summary>
@@ -142,11 +144,12 @@ public class MovieNightController : ControllerBase
/// </summary>
[HttpGet("Users/{userId}/Preferences")]
[Authorize]
public async Task<ActionResult<string?>> GetPreferences(
public async Task<IActionResult> GetPreferences(
[FromRoute] string userId,
CancellationToken cancellationToken)
{
return await _backendClient.GetPreferencesAsync(userId, cancellationToken).ConfigureAwait(false);
var body = await _backendClient.GetPreferencesAsync(userId, cancellationToken).ConfigureAwait(false);
return body is null ? NotFound() : Content(body, "application/json");
}
/// <summary>
@@ -40,7 +40,7 @@ public class MovieNightBackendClient
{
var payload = new MovieNightEventPayload(
EventId: $"plugin-test:{Guid.NewGuid():N}",
EventType: "playback.stopped",
EventType: "plugin.test",
OccurredAt: DateTimeOffset.UtcNow,
JellyfinUserId: "movienight-plugin-test-user",
ItemId: "movienight-plugin-test-item",
@@ -95,11 +95,12 @@ public class MovieNightBackendClient
/// </summary>
public async Task<string> GetRecommendationsAsync(string userId, string? contentType, string? mood, int limit, CancellationToken cancellationToken)
{
userId = NormalizeJellyfinId(userId);
var query = $"?limit={limit}";
if (!string.IsNullOrEmpty(contentType)) query += $"&contentType={Uri.EscapeDataString(contentType)}";
if (!string.IsNullOrEmpty(mood)) query += $"&mood={Uri.EscapeDataString(mood)}";
var request = CreateRequest(HttpMethod.Get, $"/api/users/{userId}/recommendations{query}");
var request = CreateRequest(HttpMethod.Get, $"/api/integrations/jellyfin/users/{userId}/recommendations{query}");
if (request is null) return "Plugin is not configured.";
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
@@ -113,7 +114,9 @@ public class MovieNightBackendClient
/// </summary>
public async Task PostRatingAsync(string userId, string filmId, int score, string? note, CancellationToken cancellationToken)
{
var request = CreateRequest(HttpMethod.Post, $"/api/users/{userId}/ratings/films/{filmId}");
userId = NormalizeJellyfinId(userId);
filmId = NormalizeJellyfinId(filmId);
var request = CreateRequest(HttpMethod.Post, $"/api/integrations/jellyfin/users/{userId}/ratings/items/{filmId}");
if (request is null) return;
request.Content = JsonContent.Create(new { score, note }, options: JsonOptions);
@@ -126,7 +129,8 @@ public class MovieNightBackendClient
/// </summary>
public async Task<string> GetRatingsAsync(string userId, CancellationToken cancellationToken)
{
var request = CreateRequest(HttpMethod.Get, $"/api/users/{userId}/ratings");
userId = NormalizeJellyfinId(userId);
var request = CreateRequest(HttpMethod.Get, $"/api/integrations/jellyfin/users/{userId}/ratings");
if (request is null) return "[]";
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
@@ -140,7 +144,9 @@ public class MovieNightBackendClient
/// </summary>
public async Task MarkViewedAsync(string userId, string filmId, DateTimeOffset? watchedAt, CancellationToken cancellationToken)
{
var request = CreateRequest(HttpMethod.Post, $"/api/users/{userId}/library/films/{filmId}/viewed");
userId = NormalizeJellyfinId(userId);
filmId = NormalizeJellyfinId(filmId);
var request = CreateRequest(HttpMethod.Post, $"/api/integrations/jellyfin/users/{userId}/library/items/{filmId}/viewed");
if (request is null) return;
request.Content = JsonContent.Create(new { watchedAt }, options: JsonOptions);
@@ -172,13 +178,15 @@ public class MovieNightBackendClient
/// </summary>
public async Task<string?> GetPreferencesAsync(string userId, CancellationToken cancellationToken)
{
var request = CreateRequest(HttpMethod.Get, $"/api/users/{userId}/preferences");
userId = NormalizeJellyfinId(userId);
var request = CreateRequest(HttpMethod.Get, $"/api/integrations/jellyfin/users/{userId}/preferences");
if (request is null) return null;
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound) return null;
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return body;
}
@@ -187,7 +195,8 @@ public class MovieNightBackendClient
/// </summary>
public async Task CompleteOnboardingAsync(string userId, object payload, CancellationToken cancellationToken)
{
var request = CreateRequest(HttpMethod.Post, $"/api/users/{userId}/recommendation-onboarding");
userId = NormalizeJellyfinId(userId);
var request = CreateRequest(HttpMethod.Post, $"/api/integrations/jellyfin/users/{userId}/recommendation-onboarding");
if (request is null) return;
request.Content = JsonContent.Create(payload, options: JsonOptions);
@@ -257,6 +266,11 @@ public class MovieNightBackendClient
return string.IsNullOrWhiteSpace(value) ? null : value.TrimEnd('/');
}
private static string NormalizeJellyfinId(string value)
{
return Guid.TryParse(value, out var guid) ? guid.ToString("N") : value;
}
private static bool IsEnabled()
{
var configuration = Plugin.Instance?.Configuration;
@@ -64,6 +64,11 @@ public class MovieNightSyncService
var items = _libraryManager.GetItemList(query);
var users = _userManager.Users;
var syncUsers = users.Select(u => new
{
jellyfinUserId = u.Id.ToString("N"),
name = u.Username
}).ToList();
var syncItems = new List<object>();
foreach (var item in items)
@@ -71,11 +76,12 @@ public class MovieNightSyncService
if (item is not Movie movie) continue;
var jellyfinItemId = movie.Id.ToString("N");
var title = string.IsNullOrWhiteSpace(movie.Name) ? jellyfinItemId : movie.Name;
var itemData = new Dictionary<string, object?>
{
["jellyfinItemId"] = jellyfinItemId,
["title"] = movie.Name,
["title"] = title,
["originalTitle"] = movie.OriginalTitle,
["description"] = movie.Overview,
["year"] = movie.ProductionYear,
@@ -99,7 +105,7 @@ public class MovieNightSyncService
syncItems.Add(itemData);
}
await _backendClient.SyncAsync(new { items = syncItems }, cancellationToken).ConfigureAwait(false);
await _backendClient.SyncAsync(new { users = syncUsers, items = syncItems }, cancellationToken).ConfigureAwait(false);
_logger.LogInformation("MovieNight library sync completed");
}
}