Support Jellyfin 10.7

This commit is contained in:
crobibero
2021-02-24 10:29:09 -07:00
parent 1458fd7534
commit 84a1d0934e
35 changed files with 699 additions and 751 deletions
@@ -0,0 +1,178 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Jellyfin.Plugin.Simkl.API;
using Jellyfin.Plugin.Simkl.API.Exceptions;
using Jellyfin.Plugin.Simkl.Configuration;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.Simkl.Services
{
/// <summary>
/// Playback progress scrobbler.
/// </summary>
public class PlaybackScrobbler : IServerEntryPoint
{
private readonly ISessionManager _sessionManager; // Needed to set up de startPlayBack and endPlayBack functions
private readonly ILogger<PlaybackScrobbler> _logger;
private readonly Dictionary<string, Guid> _lastScrobbled; // Library ID of last scrobbled item
private readonly SimklApi _simklApi;
private DateTime _nextTry;
/// <summary>
/// Initializes a new instance of the <see cref="PlaybackScrobbler"/> class.
/// </summary>
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{Scrobbler}"/> interface.</param>
/// <param name="simklApi">Instance of the <see cref="SimklApi"/>.</param>
public PlaybackScrobbler(
ISessionManager sessionManager,
ILogger<PlaybackScrobbler> logger,
SimklApi simklApi)
{
_sessionManager = sessionManager;
_logger = logger;
_simklApi = simklApi;
_lastScrobbled = new Dictionary<string, Guid>();
_nextTry = DateTime.UtcNow;
}
/// <inheritdoc />
public Task RunAsync()
{
_sessionManager.PlaybackProgress += OnPlaybackProgress;
_sessionManager.PlaybackStopped += OnPlaybackStopped;
return Task.CompletedTask;
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose.
/// </summary>
/// <param name="disposing">Dispose all resources.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_sessionManager.PlaybackProgress -= OnPlaybackProgress;
_sessionManager.PlaybackStopped -= OnPlaybackStopped;
}
}
private static bool CanBeScrobbled(UserConfig config, SessionInfo session, BaseItemDto mediaInfo, bool playedToCompletion)
{
if (!playedToCompletion)
{
if (session.NowPlayingItem.RunTimeTicks != null)
{
var percentageWatched = session.PlayState.PositionTicks / (float)session.NowPlayingItem.RunTimeTicks * 100f;
// If percentage watched is below minimum, can't scrobble
if (percentageWatched < config.ScrobblePercentage)
{
return false;
}
}
// If it's below minimum length, can't scrobble
if (session.NowPlayingItem.RunTimeTicks < 60 * 10000 * config.MinLength)
{
return false;
}
}
return mediaInfo.Type switch
{
nameof(Movie) => config.ScrobbleMovies,
nameof(Episode) => config.ScrobbleShows,
_ => false
};
}
private async void OnPlaybackProgress(object? sessions, PlaybackProgressEventArgs e)
{
if (DateTime.UtcNow < _nextTry)
{
return;
}
_nextTry = DateTime.UtcNow.AddSeconds(30);
await ScrobbleSession(e, false);
}
private async void OnPlaybackStopped(object? sessions, PlaybackStopEventArgs e)
{
await ScrobbleSession(e, e.PlayedToCompletion);
}
private async Task ScrobbleSession(PlaybackProgressEventArgs eventArgs, bool playedToCompletion)
{
try
{
var userId = eventArgs.Session.UserId;
var userConfig = SimklPlugin.Instance?.Configuration.GetByGuid(userId);
if (userConfig == null || string.IsNullOrEmpty(userConfig.UserToken))
{
_logger.LogError(
"Can't scrobble: User {UserName} not logged in ({UserConfigStatus})",
eventArgs.Session.UserName,
userConfig == null);
return;
}
if (!CanBeScrobbled(userConfig, eventArgs.Session, eventArgs.MediaInfo, playedToCompletion))
{
return;
}
if (_lastScrobbled.ContainsKey(eventArgs.Session.Id) && _lastScrobbled[eventArgs.Session.Id] == eventArgs.MediaInfo.Id)
{
_logger.LogDebug("Already scrobbled {ItemName} for {UserName}", eventArgs.MediaInfo.Name, eventArgs.Session.UserName);
return;
}
_logger.LogInformation(
"Trying to scrobble {Name} ({NowPlayingId}) for {UserName} ({UserId}) - {PlayingItemPath} on {SessionId}",
eventArgs.MediaInfo.Name,
eventArgs.MediaInfo.Id,
eventArgs.Session.UserName,
userId,
eventArgs.MediaInfo.Path,
eventArgs.Session.Id);
var response = await _simklApi.MarkAsWatched(eventArgs.MediaInfo, userConfig.UserToken);
if (response.Success)
{
_logger.LogDebug("Scrobbled without errors");
_lastScrobbled[eventArgs.Session.Id] = eventArgs.MediaInfo.Id;
}
}
catch (InvalidTokenException)
{
_logger.LogDebug("Deleted user token");
}
catch (InvalidDataException ex)
{
_logger.LogError(ex, "Couldn't scrobble");
_lastScrobbled[eventArgs.Session.Id] = eventArgs.MediaInfo.Id;
}
catch (Exception ex)
{
_logger.LogError(ex, "Caught unknown exception while trying to scrobble");
}
}
}
}
-206
View File
@@ -1,206 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Plugin.Simkl.API;
using Jellyfin.Plugin.Simkl.API.Exceptions;
using Jellyfin.Plugin.Simkl.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Notifications;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.Simkl.Services
{
/// <inheritdoc />
public class Scrobbler : IServerEntryPoint
{
private readonly ISessionManager _sessionManager; // Needed to set up de startPlayBack and endPlayBack functions
private readonly ILogger<Scrobbler> _logger;
private readonly IJsonSerializer _json;
private readonly INotificationManager _notifications;
private readonly Dictionary<string, Guid> _lastScrobbled; // Library ID of last scrobbled item
private SimklApi _api;
private DateTime _nextTry;
/// <summary>
/// Initializes a new instance of the <see cref="Scrobbler"/> class.
/// </summary>
/// <param name="json">Instance of the <see cref="IJsonSerializer"/> interface.</param>
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILogger{Scrobbler}"/> interface.</param>
/// <param name="httpClient">Instance of the <see cref="IHttpClient"/> interface.</param>
/// <param name="notifications">Instance of the <see cref="INotificationManager"/> interface.</param>
public Scrobbler(
IJsonSerializer json,
ISessionManager sessionManager,
ILoggerFactory loggerFactory,
IHttpClient httpClient,
INotificationManager notifications)
{
_json = json;
_sessionManager = sessionManager;
_logger = loggerFactory.CreateLogger<Scrobbler>();
_notifications = notifications;
_api = new SimklApi(json, loggerFactory.CreateLogger<SimklApi>(), httpClient);
_lastScrobbled = new Dictionary<string, Guid>();
_nextTry = DateTime.UtcNow;
}
/// <inheritdoc />
public Task RunAsync()
{
_sessionManager.PlaybackProgress += OnPlaybackProgress;
return Task.CompletedTask;
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose.
/// </summary>
/// <param name="disposing">Dispose all resources.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_sessionManager.PlaybackProgress -= OnPlaybackProgress;
_api = null;
}
}
/// <summary>
/// Session can be scrobbled.
/// </summary>
/// <param name="config">The plugin configuration.</param>
/// <param name="session">The session.</param>
/// <returns>If session can be scrobbled.</returns>
private static bool CanBeScrobbled(UserConfig config, SessionInfo session)
{
if (session.NowPlayingItem.RunTimeTicks != null)
{
var percentageWatched = session.PlayState.PositionTicks / (float)session.NowPlayingItem.RunTimeTicks * 100f;
// If percentage watched is below minimum, can't scrobble
if (percentageWatched < config.ScrobblePercentage)
{
return false;
}
}
// If it's below minimum length, can't scrobble
if (session.NowPlayingItem.RunTimeTicks < 60 * 10000 * config.MinLength)
{
return false;
}
var item = session.FullNowPlayingItem;
return item switch
{
Movie _ => config.ScrobbleMovies,
Episode _ => config.ScrobbleShows,
_ => false
};
}
private bool CanSendNotification(BaseItemDto item)
{
if (item.IsMovie == true || item.Type == "Movie")
{
return _notifications.GetNotificationTypes().Any(t => t.Type == SimklNotificationsFactory.NotificationMovieType && t.Enabled);
}
if (item.IsSeries == true || item.Type == "Episode")
{
return _notifications.GetNotificationTypes().Any(t => t.Type == SimklNotificationsFactory.NotificationShowType && t.Enabled);
}
return false;
}
private async void OnPlaybackProgress(object sessions, PlaybackProgressEventArgs e)
{
var sid = e.PlaySessionId;
Guid uid = e.Session.UserId, npid = e.Session.NowPlayingItem.Id;
try
{
if (DateTime.UtcNow < _nextTry)
{
return;
}
_nextTry = DateTime.UtcNow.AddSeconds(30);
var userConfig = SimklPlugin.Instance.Configuration.GetByGuid(uid);
if (userConfig == null || string.IsNullOrEmpty(userConfig.UserToken))
{
_logger.LogError("Can't scrobble: User " + e.Session.UserName + " not logged in (" + (userConfig == null) + ")");
return;
}
if (!CanBeScrobbled(userConfig, e.Session))
{
return;
}
if (_lastScrobbled.ContainsKey(sid) && _lastScrobbled[sid] == npid)
{
_logger.LogDebug("Already scrobbled {0} for {1}", e.Session.NowPlayingItem.Name, e.Session.UserName);
return;
}
_logger.LogDebug(_json.SerializeToString(e.Session.NowPlayingItem));
_logger.LogInformation(
"Trying to scrobble {0} ({1}) for {2} ({3}) - {4} on {5}",
e.Session.NowPlayingItem.Name,
npid,
e.Session.UserName,
uid,
e.Session.NowPlayingItem.Path,
sid);
var response = await _api.MarkAsWatched(e.MediaInfo, userConfig.UserToken).ConfigureAwait(false);
if (response.success)
{
_logger.LogDebug("Scrobbled without errors");
_lastScrobbled[sid] = npid;
if (CanSendNotification(response.item))
{
await _notifications.SendNotification(
SimklNotificationsFactory.GetNotificationRequest(response.item, e.Session.UserId),
e.Session.FullNowPlayingItem,
CancellationToken.None)
.ConfigureAwait(false);
}
}
}
catch (InvalidTokenException)
{
_logger.LogDebug("Deleted user token");
}
catch (InvalidDataException ex)
{
_logger.LogError(ex, "Couldn't scrobble.");
_lastScrobbled[sid] = npid;
}
catch (Exception ex)
{
_logger.LogError(ex, "Caught unknown exception while trying to scrobble.");
}
}
}
}
@@ -1,86 +0,0 @@
using System;
using System.Collections.Generic;
using MediaBrowser.Controller.Notifications;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Notifications;
namespace Jellyfin.Plugin.Simkl.Services
{
/// <inheritdoc />
public class SimklNotificationsFactory : INotificationTypeFactory
{
/// <summary>
/// Notification category.
/// </summary>
public const string NotificationCategory = "Simkl Scrobbling";
/// <summary>
/// Notification movie type.
/// </summary>
public const string NotificationMovieType = "SimklScrobblingMovie";
/// <summary>
/// Notification show type.
/// </summary>
public const string NotificationShowType = "SimklScrobblingShow";
/// <inheritdoc />
public IEnumerable<NotificationTypeInfo> GetNotificationTypes()
{
yield return new NotificationTypeInfo
{
Type = NotificationMovieType,
Name = "Scrobbling Movie",
Category = NotificationCategory,
Enabled = true,
IsBasedOnUserEvent = false
};
yield return new NotificationTypeInfo
{
Type = NotificationShowType,
Name = "Scrobbling TV Show",
Category = NotificationCategory,
Enabled = true,
IsBasedOnUserEvent = false
};
}
/// <summary>
/// Get notification request.
/// </summary>
/// <param name="item">Item.</param>
/// <param name="userId">USer id.</param>
/// <returns>The notification request.</returns>
public static NotificationRequest GetNotificationRequest(BaseItemDto item, Guid userId)
{
var nr = new NotificationRequest
{
Date = DateTime.UtcNow,
UserIds = new[] { userId },
SendToUserMode = SendToUserType.Custom
};
// TODO: Set url parameter to simkl's movie url
if (item.IsMovie == true || item.Type == "Movie")
{
nr.NotificationType = NotificationMovieType;
nr.Name = "Movie Scrobbled to Simkl";
nr.Description = "The movie " + item.Name;
nr.Description += " has been scrobbled to your account";
}
if (item.IsSeries == true || item.Type == "Episode")
{
nr.NotificationType = NotificationShowType;
nr.Name = "Episode Scrobbled to Simkl";
nr.Description = item.SeriesName;
nr.Description += " S" + item.ParentIndexNumber + ":E" + item.IndexNumber;
nr.Description += " - " + item.Name;
nr.Description += " has been scrobbled to your account";
}
return nr;
}
}
}