Refactor Jellyfin plugin: integrate UI components and implement backend contract.
- Integrated "Recommend Film" button and Rating UI into Jellyfin web interface via MutationObserver. - Implemented full library sync (metadata + user state) in MovieNightSyncService. - Updated MovieNightBackendClient to support sync, recommendations, ratings, and viewed status. - Added proxy endpoints to MovieNightController for frontend-backend communication. - Refined sync logic to handle large libraries and ensured .NET 9.0 compatibility. Co-authored-by: devitq <118541411+devitq@users.noreply.github.com>
This commit is contained in:
co-authored by
devitq
parent
73b8e4ee02
commit
0a90c3eadf
@@ -16,6 +16,11 @@ const movieNightConfigPage = {
|
|||||||
config.EnablePeriodicSync !== false;
|
config.EnablePeriodicSync !== false;
|
||||||
view.querySelector("#EnablePlaybackEvents").checked =
|
view.querySelector("#EnablePlaybackEvents").checked =
|
||||||
config.EnablePlaybackEvents !== false;
|
config.EnablePlaybackEvents !== false;
|
||||||
|
|
||||||
|
const uiScriptUrl = ApiClient.getUrl("web/ConfigurationPage", {
|
||||||
|
name: "MovieNight.ui.js",
|
||||||
|
});
|
||||||
|
view.querySelector("#UIScriptUrl").innerText = uiScriptUrl;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
Dashboard.hideLoadingMsg();
|
Dashboard.hideLoadingMsg();
|
||||||
|
|||||||
@@ -53,6 +53,12 @@
|
|||||||
<span>Test connection</span>
|
<span>Test connection</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 2em; padding: 1em; background: #333; border-radius: 4px;">
|
||||||
|
<h3>UI Integration</h3>
|
||||||
|
<p>To enable the "Recommend Film" button and Rating UI in the main Jellyfin interface, you must add the following script URL to your Jellyfin <strong>Custom JavaScript</strong> setting (Dashboard > General):</p>
|
||||||
|
<code id="UIScriptUrl" style="display: block; padding: 0.5em; background: #000; word-break: break-all;"></code>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
(function () {
|
||||||
|
const PLUGIN_ID = "42c72919-d6ff-4f62-bb8c-0fac39efafdb";
|
||||||
|
|
||||||
|
function injectUI() {
|
||||||
|
// 1. Inject "Recommend me a film" button in Library views
|
||||||
|
const headerButtons = document.querySelector('.headerViewButtons');
|
||||||
|
if (headerButtons && !document.querySelector('.btnMovieNightRecommend')) {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'emby-button raised btnMovieNightRecommend';
|
||||||
|
btn.innerHTML = '<span>Recommend Film</span>';
|
||||||
|
btn.style.marginLeft = '1em';
|
||||||
|
btn.onclick = showRecommendation;
|
||||||
|
headerButtons.appendChild(btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Inject Rating UI in Item Details
|
||||||
|
const detailButtons = document.querySelector('.itemDetailButtons');
|
||||||
|
if (detailButtons && !document.querySelector('.movieNightRatingContainer')) {
|
||||||
|
const itemId = getItemIdFromUrl();
|
||||||
|
if (itemId) {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'movieNightRatingContainer';
|
||||||
|
container.style.display = 'inline-flex';
|
||||||
|
container.style.alignItems = 'center';
|
||||||
|
container.style.marginLeft = '1em';
|
||||||
|
|
||||||
|
const label = document.createElement('span');
|
||||||
|
label.innerText = 'MovieNight: ';
|
||||||
|
container.appendChild(label);
|
||||||
|
|
||||||
|
const select = document.createElement('select');
|
||||||
|
select.className = 'emby-select';
|
||||||
|
for (let i = 0; i <= 10; i++) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = i;
|
||||||
|
opt.innerText = i === 0 ? 'Rate...' : i;
|
||||||
|
select.appendChild(opt);
|
||||||
|
}
|
||||||
|
select.onchange = (e) => submitRating(itemId, e.target.value);
|
||||||
|
container.appendChild(select);
|
||||||
|
|
||||||
|
detailButtons.appendChild(container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItemIdFromUrl() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
return params.get('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showRecommendation() {
|
||||||
|
const userId = ApiClient.getCurrentUserId();
|
||||||
|
try {
|
||||||
|
const response = await ApiClient.getJSON(ApiClient.getUrl(`MovieNight/Users/${userId}/Recommendations`));
|
||||||
|
const recommendations = typeof response === 'string' ? JSON.parse(response) : response;
|
||||||
|
|
||||||
|
if (recommendations && recommendations.length > 0) {
|
||||||
|
const rec = recommendations[0];
|
||||||
|
const film = rec.film || rec;
|
||||||
|
Dashboard.alert({
|
||||||
|
title: 'MovieNight Recommendation',
|
||||||
|
text: `How about watching: ${film.title}?\n\nReason: ${rec.reasons?.join(', ') || 'Based on your preferences'}`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Dashboard.alert('No recommendations found at the moment.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to get recommendations', err);
|
||||||
|
Dashboard.alert('Failed to get recommendations from MovieNight.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitRating(itemId, score) {
|
||||||
|
if (score === "0") return;
|
||||||
|
const userId = ApiClient.getCurrentUserId();
|
||||||
|
try {
|
||||||
|
await ApiClient.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: ApiClient.getUrl(`MovieNight/Users/${userId}/Ratings/Films/${itemId}`),
|
||||||
|
data: JSON.stringify({ score: parseInt(score), note: 'From Jellyfin UI' }),
|
||||||
|
contentType: 'application/json'
|
||||||
|
});
|
||||||
|
Dashboard.alert('Rating submitted!');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to submit rating', err);
|
||||||
|
Dashboard.alert('Failed to submit rating to MovieNight.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(injectUI);
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
// Initial call
|
||||||
|
injectUI();
|
||||||
|
})();
|
||||||
@@ -16,14 +16,15 @@ namespace Jellyfin.Plugin.MovieNight.Controllers;
|
|||||||
public class MovieNightController : ControllerBase
|
public class MovieNightController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly MovieNightBackendClient _backendClient;
|
private readonly MovieNightBackendClient _backendClient;
|
||||||
|
private readonly MovieNightSyncService _syncService;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="MovieNightController"/> class.
|
/// Initializes a new instance of the <see cref="MovieNightController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="backendClient">Backend client.</param>
|
public MovieNightController(MovieNightBackendClient backendClient, MovieNightSyncService syncService)
|
||||||
public MovieNightController(MovieNightBackendClient backendClient)
|
|
||||||
{
|
{
|
||||||
_backendClient = backendClient;
|
_backendClient = backendClient;
|
||||||
|
_syncService = syncService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -61,7 +62,8 @@ public class MovieNightController : ControllerBase
|
|||||||
[HttpPost("Sync")]
|
[HttpPost("Sync")]
|
||||||
public async Task<ActionResult<string>> Sync(CancellationToken cancellationToken)
|
public async Task<ActionResult<string>> Sync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return await _backendClient.TriggerSyncAsync(cancellationToken).ConfigureAwait(false);
|
await _syncService.PerformSyncAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
return Ok("Sync triggered");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -74,8 +76,60 @@ public class MovieNightController : ControllerBase
|
|||||||
{
|
{
|
||||||
return await _backendClient.GetSyncStateAsync(cancellationToken).ConfigureAwait(false);
|
return await _backendClient.GetSyncStateAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets recommendations for the current user.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("Users/{userId}/Recommendations")]
|
||||||
|
public async Task<ActionResult<string>> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Posts a rating for a film.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("Users/{userId}/Ratings/Films/{filmId}")]
|
||||||
|
public async Task<ActionResult> PostRating(
|
||||||
|
[FromRoute] string userId,
|
||||||
|
[FromRoute] string filmId,
|
||||||
|
[FromBody] RatingRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _backendClient.PostRatingAsync(userId, filmId, request.Score, request.Note, cancellationToken).ConfigureAwait(false);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks a film as viewed.
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("Users/{userId}/Library/Films/{filmId}/Viewed")]
|
||||||
|
public async Task<ActionResult> MarkViewed(
|
||||||
|
[FromRoute] string userId,
|
||||||
|
[FromRoute] string filmId,
|
||||||
|
[FromBody] ViewedRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _backendClient.MarkViewedAsync(userId, filmId, request.WatchedAt, cancellationToken).ConfigureAwait(false);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rating request.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record RatingRequest(int Score, string? Note);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Viewed request.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ViewedRequest(DateTimeOffset? WatchedAt);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// MovieNight plugin status response.
|
/// MovieNight plugin status response.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -27,8 +27,10 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="Configuration\configPage.html" />
|
<None Remove="Configuration\configPage.html" />
|
||||||
<None Remove="Configuration\config.js" />
|
<None Remove="Configuration\config.js" />
|
||||||
|
<None Remove="Configuration\ui.js" />
|
||||||
<EmbeddedResource Include="Configuration\configPage.html" />
|
<EmbeddedResource Include="Configuration\configPage.html" />
|
||||||
<EmbeddedResource Include="Configuration\config.js" />
|
<EmbeddedResource Include="Configuration\config.js" />
|
||||||
|
<EmbeddedResource Include="Configuration\ui.js" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
|
|||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
"{0}.Configuration.config.js",
|
"{0}.Configuration.config.js",
|
||||||
GetType().Namespace)
|
GetType().Namespace)
|
||||||
|
},
|
||||||
|
new PluginPageInfo
|
||||||
|
{
|
||||||
|
Name = Name + ".ui.js",
|
||||||
|
EmbeddedResourcePath = string.Format(
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
"{0}.Configuration.ui.js",
|
||||||
|
GetType().Namespace)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ public class PluginServiceRegistrator : IPluginServiceRegistrator
|
|||||||
public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost)
|
public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost)
|
||||||
{
|
{
|
||||||
serviceCollection.AddSingleton<MovieNightBackendClient>();
|
serviceCollection.AddSingleton<MovieNightBackendClient>();
|
||||||
|
serviceCollection.AddSingleton<MovieNightSyncService>();
|
||||||
serviceCollection.AddHostedService<MovieNightPeriodicSyncService>();
|
serviceCollection.AddHostedService<MovieNightPeriodicSyncService>();
|
||||||
serviceCollection.AddHostedService<MovieNightPlaybackEventService>();
|
serviceCollection.AddHostedService<MovieNightPlaybackEventService>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,11 +70,12 @@ public class MovieNightBackendClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Triggers the current backend Jellyfin sync endpoint.
|
/// Pushes library sync data to the backend.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="payload">Sync payload.</param>
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
/// <returns>Backend response body.</returns>
|
/// <returns>Backend response body.</returns>
|
||||||
public async Task<string> TriggerSyncAsync(CancellationToken cancellationToken)
|
public async Task<string> SyncAsync(object payload, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var request = CreateRequest(HttpMethod.Post, "/api/integrations/jellyfin/sync");
|
var request = CreateRequest(HttpMethod.Post, "/api/integrations/jellyfin/sync");
|
||||||
if (request is null)
|
if (request is null)
|
||||||
@@ -82,12 +83,71 @@ public class MovieNightBackendClient
|
|||||||
return "Plugin is not configured.";
|
return "Plugin is not configured.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
request.Content = JsonContent.Create(payload, options: JsonOptions);
|
||||||
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||||
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets recommendations for a user.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string> GetRecommendationsAsync(string userId, string? contentType, string? mood, int limit, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
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}");
|
||||||
|
if (request is null) return "Plugin is not configured.";
|
||||||
|
|
||||||
|
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||||
|
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Posts a rating for a film.
|
||||||
|
/// </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}");
|
||||||
|
if (request is null) return;
|
||||||
|
|
||||||
|
request.Content = JsonContent.Create(new { score, note }, options: JsonOptions);
|
||||||
|
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets ratings for a user.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string> GetRatingsAsync(string userId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var request = CreateRequest(HttpMethod.Get, $"/api/users/{userId}/ratings");
|
||||||
|
if (request is null) return "[]";
|
||||||
|
|
||||||
|
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||||
|
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks a film as viewed.
|
||||||
|
/// </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");
|
||||||
|
if (request is null) return;
|
||||||
|
|
||||||
|
request.Content = JsonContent.Create(new { watchedAt }, options: JsonOptions);
|
||||||
|
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads backend sync state.
|
/// Reads backend sync state.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
+12
-6
@@ -1,6 +1,14 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Entities.Movies;
|
||||||
|
using MediaBrowser.Model.Entities;
|
||||||
|
using MediaBrowser.Model.Querying;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -11,19 +19,17 @@ namespace Jellyfin.Plugin.MovieNight.Services;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class MovieNightPeriodicSyncService : BackgroundService
|
public sealed class MovieNightPeriodicSyncService : BackgroundService
|
||||||
{
|
{
|
||||||
private readonly MovieNightBackendClient _backendClient;
|
private readonly MovieNightSyncService _syncService;
|
||||||
private readonly ILogger<MovieNightPeriodicSyncService> _logger;
|
private readonly ILogger<MovieNightPeriodicSyncService> _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="MovieNightPeriodicSyncService"/> class.
|
/// Initializes a new instance of the <see cref="MovieNightPeriodicSyncService"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="backendClient">Backend client.</param>
|
|
||||||
/// <param name="logger">Logger.</param>
|
|
||||||
public MovieNightPeriodicSyncService(
|
public MovieNightPeriodicSyncService(
|
||||||
MovieNightBackendClient backendClient,
|
MovieNightSyncService syncService,
|
||||||
ILogger<MovieNightPeriodicSyncService> logger)
|
ILogger<MovieNightPeriodicSyncService> logger)
|
||||||
{
|
{
|
||||||
_backendClient = backendClient;
|
_syncService = syncService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +47,7 @@ public sealed class MovieNightPeriodicSyncService : BackgroundService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _backendClient.TriggerSyncAsync(stoppingToken).ConfigureAwait(false);
|
await _syncService.PerformSyncAsync(stoppingToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Entities.Movies;
|
||||||
|
using MediaBrowser.Model.Entities;
|
||||||
|
using MediaBrowser.Model.Querying;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Jellyfin.Plugin.MovieNight.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for synchronizing the Jellyfin library with MovieNight.
|
||||||
|
/// </summary>
|
||||||
|
public class MovieNightSyncService
|
||||||
|
{
|
||||||
|
private readonly MovieNightBackendClient _backendClient;
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
private readonly IUserDataManager _userDataManager;
|
||||||
|
private readonly ILogger<MovieNightSyncService> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MovieNightSyncService"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public MovieNightSyncService(
|
||||||
|
MovieNightBackendClient backendClient,
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
IUserManager userManager,
|
||||||
|
IUserDataManager userDataManager,
|
||||||
|
ILogger<MovieNightSyncService> logger)
|
||||||
|
{
|
||||||
|
_backendClient = backendClient;
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
_userManager = userManager;
|
||||||
|
_userDataManager = userDataManager;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs a full library sync.
|
||||||
|
/// </summary>
|
||||||
|
public async Task PerformSyncAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Starting MovieNight library sync");
|
||||||
|
|
||||||
|
var items = _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
IncludeItemTypes = new[] { BaseItemKind.Movie },
|
||||||
|
Recursive = true
|
||||||
|
});
|
||||||
|
|
||||||
|
var users = _userManager.Users;
|
||||||
|
var syncItems = new List<object>();
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (item is not Movie movie) continue;
|
||||||
|
|
||||||
|
var jellyfinItemId = movie.Id.ToString("N");
|
||||||
|
|
||||||
|
var itemData = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["jellyfinItemId"] = jellyfinItemId,
|
||||||
|
["title"] = movie.Name,
|
||||||
|
["originalTitle"] = movie.OriginalTitle,
|
||||||
|
["description"] = movie.Overview,
|
||||||
|
["year"] = movie.ProductionYear,
|
||||||
|
["duration"] = movie.RunTimeTicks,
|
||||||
|
["genres"] = movie.Genres,
|
||||||
|
["imdbId"] = movie.GetProviderId(MetadataProvider.Imdb),
|
||||||
|
["tmdbId"] = movie.GetProviderId(MetadataProvider.Tmdb),
|
||||||
|
["userStates"] = users.Select(u => {
|
||||||
|
var userData = _userDataManager.GetUserData(u, movie);
|
||||||
|
return new {
|
||||||
|
jellyfinUserId = u.Id.ToString("N"),
|
||||||
|
isViewed = userData?.Played ?? false,
|
||||||
|
playCount = userData?.PlayCount ?? 0,
|
||||||
|
lastPlayedAt = userData?.LastPlayedDate,
|
||||||
|
userRating = userData?.Rating
|
||||||
|
};
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
syncItems.Add(itemData);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _backendClient.SyncAsync(new { items = syncItems }, cancellationToken).ConfigureAwait(false);
|
||||||
|
_logger.LogInformation("MovieNight library sync completed");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user