using System ;
using System.Collections.Concurrent ;
using System.Collections.Generic ;
using System.Globalization ;
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
using Jellyfin.Data.Entities ;
using Jellyfin.Data.Events ;
using Jellyfin.Extensions ;
using MediaBrowser.Controller.Channels ;
using MediaBrowser.Controller.Configuration ;
using MediaBrowser.Controller.Entities ;
using MediaBrowser.Controller.Entities.Audio ;
using MediaBrowser.Controller.Library ;
using MediaBrowser.Controller.Providers ;
using MediaBrowser.Controller.Session ;
using MediaBrowser.Model.Entities ;
using MediaBrowser.Model.Session ;
using Microsoft.Extensions.Hosting ;
using Microsoft.Extensions.Logging ;
namespace Emby.Server.Implementations.EntryPoints ;
/// <summary>
/// A <see cref="IHostedService"/> responsible for notifying users when libraries are updated.
/// </summary>
public sealed class LibraryChangedNotifier : IHostedService , IDisposable
{
private readonly ILibraryManager _libraryManager ;
private readonly IServerConfigurationManager _configurationManager ;
private readonly IProviderManager _providerManager ;
private readonly ISessionManager _sessionManager ;
private readonly IUserManager _userManager ;
private readonly ILogger < LibraryChangedNotifier > _logger ;
private readonly object _libraryChangedSyncLock = new ( ) ;
private readonly List < Folder > _foldersAddedTo = new ( ) ;
private readonly List < Folder > _foldersRemovedFrom = new ( ) ;
private readonly List < BaseItem > _itemsAdded = new ( ) ;
private readonly List < BaseItem > _itemsRemoved = new ( ) ;
private readonly List < BaseItem > _itemsUpdated = new ( ) ;
private readonly ConcurrentDictionary < Guid , DateTime > _lastProgressMessageTimes = new ( ) ;
private Timer ? _libraryUpdateTimer ;
/// <summary>
/// Initializes a new instance of the <see cref="LibraryChangedNotifier"/> class.
/// </summary>
/// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
/// <param name="configurationManager">The <see cref="IServerConfigurationManager"/>.</param>
/// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
/// <param name="userManager">The <see cref="IUserManager"/>.</param>
/// <param name="logger">The <see cref="ILogger"/>.</param>
/// <param name="providerManager">The <see cref="IProviderManager"/>.</param>
public LibraryChangedNotifier (
ILibraryManager libraryManager ,
IServerConfigurationManager configurationManager ,
ISessionManager sessionManager ,
IUserManager userManager ,
ILogger < LibraryChangedNotifier > logger ,
IProviderManager providerManager )
{
_libraryManager = libraryManager ;
_configurationManager = configurationManager ;
_sessionManager = sessionManager ;
_userManager = userManager ;
_logger = logger ;
_providerManager = providerManager ;
}
/// <inheritdoc />
public Task StartAsync ( CancellationToken cancellationToken )
{
_libraryManager . ItemAdded + = OnLibraryItemAdded ;
_libraryManager . ItemUpdated + = OnLibraryItemUpdated ;
_libraryManager . ItemRemoved + = OnLibraryItemRemoved ;
_providerManager . RefreshCompleted + = OnProviderRefreshCompleted ;
_providerManager . RefreshStarted + = OnProviderRefreshStarted ;
_providerManager . RefreshProgress + = OnProviderRefreshProgress ;
return Task . CompletedTask ;
}
/// <inheritdoc />
public Task StopAsync ( CancellationToken cancellationToken )
{
_libraryManager . ItemAdded - = OnLibraryItemAdded ;
_libraryManager . ItemUpdated - = OnLibraryItemUpdated ;
_libraryManager . ItemRemoved - = OnLibraryItemRemoved ;
_providerManager . RefreshCompleted - = OnProviderRefreshCompleted ;
_providerManager . RefreshStarted - = OnProviderRefreshStarted ;
_providerManager . RefreshProgress - = OnProviderRefreshProgress ;
return Task . CompletedTask ;
}
private void OnProviderRefreshProgress ( object? sender , GenericEventArgs < Tuple < BaseItem , double > > e )
{
var item = e . Argument . Item1 ;
if ( ! EnableRefreshMessage ( item ) )
{
return ;
}
var progress = e . Argument . Item2 ;
if ( _lastProgressMessageTimes . TryGetValue ( item . Id , out var lastMessageSendTime ) )
{
if ( progress > 0 & & progress < 100 & & ( DateTime . UtcNow - lastMessageSendTime ) . TotalMilliseconds < 1000 )
{
return ;
}
}
_lastProgressMessageTimes . AddOrUpdate ( item . Id , _ = > DateTime . UtcNow , ( _ , _ ) = > DateTime . UtcNow ) ;
var dict = new Dictionary < string , string > ( ) ;
dict [ "ItemId" ] = item . Id . ToString ( "N" , CultureInfo . InvariantCulture ) ;
dict [ "Progress" ] = progress . ToString ( CultureInfo . InvariantCulture ) ;
try
{
_sessionManager . SendMessageToAdminSessions ( SessionMessageType . RefreshProgress , dict , CancellationToken . None ) ;
}
catch
{
}
var collectionFolders = _libraryManager . GetCollectionFolders ( item ) ;
foreach ( var collectionFolder in collectionFolders )
{
var collectionFolderDict = new Dictionary < string , string >
{
["ItemId"] = collectionFolder . Id . ToString ( "N" , CultureInfo . InvariantCulture ) ,
["Progress"] = ( collectionFolder . GetRefreshProgress ( ) ? ? 0 ) . ToString ( CultureInfo . InvariantCulture )
} ;
try
{
_sessionManager . SendMessageToAdminSessions ( SessionMessageType . RefreshProgress , collectionFolderDict , CancellationToken . None ) ;
}
catch
{
}
}
}
private void OnProviderRefreshStarted ( object? sender , GenericEventArgs < BaseItem > e )
= > OnProviderRefreshProgress ( sender , new GenericEventArgs < Tuple < BaseItem , double > > ( new Tuple < BaseItem , double > ( e . Argument , 0 ) ) ) ;
private void OnProviderRefreshCompleted ( object? sender , GenericEventArgs < BaseItem > e )
{
OnProviderRefreshProgress ( sender , new GenericEventArgs < Tuple < BaseItem , double > > ( new Tuple < BaseItem , double > ( e . Argument , 100 ) ) ) ;
_lastProgressMessageTimes . TryRemove ( e . Argument . Id , out _ ) ;
}
private static bool EnableRefreshMessage ( BaseItem item )
= > item is Folder { IsRoot : false , IsTopParent : true }
and not ( AggregateFolder or UserRootFolder or UserView or Channel ) ;
private void OnLibraryItemAdded ( object? sender , ItemChangeEventArgs e )
= > OnLibraryChange ( e . Item , e . Parent , _itemsAdded , _foldersAddedTo ) ;
private void OnLibraryItemUpdated ( object? sender , ItemChangeEventArgs e )
= > OnLibraryChange ( e . Item , e . Parent , _itemsUpdated , null ) ;
private void OnLibraryItemRemoved ( object? sender , ItemChangeEventArgs e )
= > OnLibraryChange ( e . Item , e . Parent , _itemsRemoved , _foldersRemovedFrom ) ;
private void OnLibraryChange ( BaseItem item , BaseItem parent , List < BaseItem > itemsList , List < Folder > ? foldersList )
{
if ( ! FilterItem ( item ) )
{
return ;
}
lock ( _libraryChangedSyncLock )
{
var updateDuration = TimeSpan . FromSeconds ( _configurationManager . Configuration . LibraryUpdateDuration ) ;
if ( _libraryUpdateTimer is null )
{
_libraryUpdateTimer = new Timer ( LibraryUpdateTimerCallback , null , updateDuration , Timeout . InfiniteTimeSpan ) ;
}
else
{
_libraryUpdateTimer . Change ( updateDuration , Timeout . InfiniteTimeSpan ) ;
}
if ( foldersList is not null & & parent is Folder folder )
{
foldersList . Add ( folder ) ;
}
itemsList . Add ( item ) ;
}
}
private async void LibraryUpdateTimerCallback ( object? state )
{
List < Folder > foldersAddedTo ;
List < Folder > foldersRemovedFrom ;
List < BaseItem > itemsUpdated ;
List < BaseItem > itemsAdded ;
List < BaseItem > itemsRemoved ;
lock ( _libraryChangedSyncLock )
{
// Remove dupes in case some were saved multiple times
foldersAddedTo = _foldersAddedTo
. DistinctBy ( x = > x . Id )
. ToList ( ) ;
foldersRemovedFrom = _foldersRemovedFrom
. DistinctBy ( x = > x . Id )
. ToList ( ) ;
itemsUpdated = _itemsUpdated
. Where ( i = > ! _itemsAdded . Contains ( i ) )
. DistinctBy ( x = > x . Id )
. ToList ( ) ;
itemsAdded = _itemsAdded . ToList ( ) ;
itemsRemoved = _itemsRemoved . ToList ( ) ;
if ( _libraryUpdateTimer is not null )
{
_libraryUpdateTimer . Dispose ( ) ;
_libraryUpdateTimer = null ;
}
_itemsAdded . Clear ( ) ;
_itemsRemoved . Clear ( ) ;
_itemsUpdated . Clear ( ) ;
_foldersAddedTo . Clear ( ) ;
_foldersRemovedFrom . Clear ( ) ;
}
await SendChangeNotifications ( itemsAdded , itemsUpdated , itemsRemoved , foldersAddedTo , foldersRemovedFrom , CancellationToken . None ) . ConfigureAwait ( false ) ;
}
private async Task SendChangeNotifications (
List < BaseItem > itemsAdded ,
List < BaseItem > itemsUpdated ,
List < BaseItem > itemsRemoved ,
List < Folder > foldersAddedTo ,
List < Folder > foldersRemovedFrom ,
CancellationToken cancellationToken )
{
var userIds = _sessionManager . Sessions
. Select ( i = > i . UserId )
. Where ( i = > ! i . IsEmpty ( ) )
. Distinct ( )
. ToArray ( ) ;
foreach ( var userId in userIds )
{
LibraryUpdateInfo info ;
try
{
info = GetLibraryUpdateInfo ( itemsAdded , itemsUpdated , itemsRemoved , foldersAddedTo , foldersRemovedFrom , userId ) ;
}
catch ( Exception ex )
{
_logger . LogError ( ex , "Error in GetLibraryUpdateInfo" ) ;
return ;
}
if ( info . IsEmpty )
{
continue ;
}
try
{
await _sessionManager . SendMessageToUserSessions (
new List < Guid > { userId } ,
SessionMessageType . LibraryChanged ,
info ,
cancellationToken )
. ConfigureAwait ( false ) ;
}
catch ( Exception ex )
{
_logger . LogError ( ex , "Error sending LibraryChanged message" ) ;
}
}
}
private LibraryUpdateInfo GetLibraryUpdateInfo (
List < BaseItem > itemsAdded ,
List < BaseItem > itemsUpdated ,
List < BaseItem > itemsRemoved ,
List < Folder > foldersAddedTo ,
List < Folder > foldersRemovedFrom ,
Guid userId )
{
var user = _userManager . GetUserById ( userId ) ;
ArgumentNullException . ThrowIfNull ( user ) ;
var newAndRemoved = new List < BaseItem > ( ) ;
newAndRemoved . AddRange ( foldersAddedTo ) ;
newAndRemoved . AddRange ( foldersRemovedFrom ) ;
var allUserRootChildren = _libraryManager . GetUserRootFolder ( )
. GetChildren ( user , true )
. OfType < Folder > ( )
. ToList ( ) ;
return new LibraryUpdateInfo
{
ItemsAdded = itemsAdded . SelectMany ( i = > TranslatePhysicalItemToUserLibrary ( i , user ) )
. Select ( i = > i . Id . ToString ( "N" , CultureInfo . InvariantCulture ) )
. Distinct ( )
. ToArray ( ) ,
ItemsUpdated = itemsUpdated . SelectMany ( i = > TranslatePhysicalItemToUserLibrary ( i , user ) )
. Select ( i = > i . Id . ToString ( "N" , CultureInfo . InvariantCulture ) )
. Distinct ( )
. ToArray ( ) ,
ItemsRemoved = itemsRemoved . SelectMany ( i = > TranslatePhysicalItemToUserLibrary ( i , user , true ) )
. Select ( i = > i . Id . ToString ( "N" , CultureInfo . InvariantCulture ) )
. Distinct ( )
. ToArray ( ) ,
FoldersAddedTo = foldersAddedTo . SelectMany ( i = > TranslatePhysicalItemToUserLibrary ( i , user ) )
. Select ( i = > i . Id . ToString ( "N" , CultureInfo . InvariantCulture ) )
. Distinct ( )
. ToArray ( ) ,
FoldersRemovedFrom = foldersRemovedFrom . SelectMany ( i = > TranslatePhysicalItemToUserLibrary ( i , user ) )
. Select ( i = > i . Id . ToString ( "N" , CultureInfo . InvariantCulture ) )
. Distinct ( )
. ToArray ( ) ,
CollectionFolders = GetTopParentIds ( newAndRemoved , allUserRootChildren ) . ToArray ( )
} ;
}
private static bool FilterItem ( BaseItem item )
{
if ( ! item . IsFolder & & ! item . HasPathProtocol )
{
return false ;
}
if ( item is IItemByName & & item is not MusicArtist )
{
return false ;
}
return item . SourceType = = SourceType . Library ;
}
private static IEnumerable < string > GetTopParentIds ( List < BaseItem > items , List < Folder > allUserRootChildren )
{
var list = new List < string > ( ) ;
foreach ( var item in items )
{
// If the physical root changed, return the user root
if ( item is AggregateFolder )
{
continue ;
}
foreach ( var folder in allUserRootChildren )
{
list . Add ( folder . Id . ToString ( "N" , CultureInfo . InvariantCulture ) ) ;
}
}
return list . Distinct ( StringComparer . Ordinal ) ;
}
private T [ ] TranslatePhysicalItemToUserLibrary < T > ( T item , User user , bool includeIfNotFound = false )
where T : BaseItem
{
// If the physical root changed, return the user root
if ( item is AggregateFolder )
{
return _libraryManager . GetUserRootFolder ( ) is T t ? new [ ] { t } : Array . Empty < T > ( ) ;
}
// Return it only if it's in the user's library
if ( includeIfNotFound | | item . IsVisibleStandalone ( user ) )
{
return new [ ] { item } ;
}
return Array . Empty < T > ( ) ;
}
/// <inheritdoc />
public void Dispose ( )
{
_libraryUpdateTimer ? . Dispose ( ) ;
_libraryUpdateTimer = null ;
}
}