228 lines
10 KiB
C#
228 lines
10 KiB
C#
using System.Globalization;
|
|
using MediaBrowser.Model.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
using MediaBrowser.Controller;
|
|
using MediaBrowser.Controller.Library;
|
|
using MediaBrowser.Controller.Playlists;
|
|
using MediaBrowser.Controller.Providers;
|
|
using MediaBrowser.Model.IO;
|
|
using Jellyfin.Data.Entities;
|
|
using Jellyfin.Data.Enums;
|
|
using MediaBrowser.Model.Entities;
|
|
using MediaBrowser.Controller.Entities;
|
|
using MediaBrowser.Model.Playlists;
|
|
|
|
using Jellyfin.Plugin.SmartPlaylist.Lisp;
|
|
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
|
|
using Lisp_Object = Jellyfin.Plugin.SmartPlaylist.Lisp.Object;
|
|
using Lisp_Boolean = Jellyfin.Plugin.SmartPlaylist.Lisp.Boolean;
|
|
|
|
|
|
namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
|
public class GeneratePlaylist : IScheduledTask {
|
|
|
|
public static readonly BaseItemKind[] AvailableFilterItems = {
|
|
BaseItemKind.Audio,
|
|
BaseItemKind.MusicAlbum,
|
|
BaseItemKind.Playlist,
|
|
};
|
|
|
|
private readonly ILogger _logger;
|
|
private readonly ILibraryManager _libraryManager;
|
|
private readonly IUserManager _userManager;
|
|
private readonly IProviderManager _providerManager;
|
|
private readonly IFileSystem _fileSystem;
|
|
private readonly IPlaylistManager _playlistManager;
|
|
|
|
private readonly IStore _store;
|
|
|
|
public GeneratePlaylist(
|
|
ILogger<Plugin> logger,
|
|
ILibraryManager libraryManager,
|
|
IUserManager userManager,
|
|
IProviderManager providerManager,
|
|
IFileSystem fileSystem,
|
|
IPlaylistManager playlistManager,
|
|
IServerApplicationPaths serverApplicationPaths
|
|
) {
|
|
_logger = logger;
|
|
_libraryManager = libraryManager;
|
|
_userManager = userManager;
|
|
_providerManager = providerManager;
|
|
_fileSystem = fileSystem;
|
|
_playlistManager = playlistManager;
|
|
|
|
_store = new Store(new SmartPlaylistFileSystem(serverApplicationPaths));
|
|
}
|
|
|
|
public string Category => "Library";
|
|
public string Name => "(re)generate Smart Playlists";
|
|
public string Description => "Generate or regenerate all Smart Playlists";
|
|
public string Key => nameof(GeneratePlaylist);
|
|
|
|
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() {
|
|
return new[] {
|
|
new TaskTriggerInfo {
|
|
IntervalTicks = TimeSpan.FromHours(24).Ticks,
|
|
Type = TaskTriggerInfo.TriggerInterval,
|
|
}
|
|
};
|
|
}
|
|
|
|
private void GetUsers() {
|
|
foreach (var user in _userManager.Users) {
|
|
_logger.LogInformation("User {0}", user);
|
|
var query = new InternalItemsQuery(user) {
|
|
IncludeItemTypes = AvailableFilterItems,
|
|
Recursive = true,
|
|
};
|
|
foreach (BaseItem item in _libraryManager.GetItemsResult(query).Items) {
|
|
_logger.LogInformation("Item {0}", item);
|
|
}
|
|
}
|
|
}
|
|
|
|
private PlaylistId CreateNewPlaylist(string name, UserId userId) {
|
|
_logger.LogDebug("Creating playlist '{0}'", name);
|
|
var req = new PlaylistCreationRequest {
|
|
Name = name,
|
|
UserId = userId,
|
|
Users = [new PlaylistUserPermissions(userId)],
|
|
Public = false,
|
|
};
|
|
var playlistGuid = Guid.Parse(_playlistManager.CreatePlaylist(req).Result.Id);
|
|
return playlistGuid;
|
|
}
|
|
|
|
private Executor SetupExecutor() {
|
|
var env = new DefaultEnvironment();
|
|
var executor = new Executor(env);
|
|
executor.builtins["logd"] = (x) => {
|
|
_logger.LogDebug(((Lisp.String)x.First()).Value(), x.Skip(1).ToArray());
|
|
return Lisp_Boolean.TRUE;
|
|
};
|
|
executor.builtins["logi"] = (x) => {
|
|
_logger.LogInformation(((Lisp.String)x.First()).Value(), x.Skip(1).ToArray());
|
|
return Lisp_Boolean.TRUE;
|
|
};
|
|
executor.builtins["logw"] = (x) => {
|
|
_logger.LogWarning(((Lisp.String)x.First()).Value(), x.Skip(1).ToArray());
|
|
return Lisp_Boolean.TRUE;
|
|
};
|
|
executor.builtins["loge"] = (x) => {
|
|
_logger.LogError(((Lisp.String)x.First()).Value(), x.Skip(1).ToArray());
|
|
return Lisp_Boolean.TRUE;
|
|
};
|
|
if (Plugin.Instance is not null) {
|
|
executor.eval(Plugin.Instance.Configuration.InitialProgram);
|
|
} else {
|
|
throw new ApplicationException("Plugin Instance is not yet initialized");
|
|
}
|
|
return executor;
|
|
}
|
|
|
|
private IEnumerable<Guid> FilterPlaylistItems(IEnumerable<BaseItem> items, User user, SmartPlaylistDto smartPlaylist) {
|
|
List<BaseItem> results = new List<BaseItem>();
|
|
Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse(); // parse here, so that we don't repeat the work for each item
|
|
Executor executor = SetupExecutor();
|
|
|
|
executor.environment.Set("*user*", Lisp_Object.FromBase(user));
|
|
foreach (var i in items) {
|
|
executor.environment.Set("*item*", Lisp_Object.FromBase(i));
|
|
var r = executor.eval(expression);
|
|
_logger.LogTrace("Item {0} evaluated to {1}", i, r.ToString());
|
|
if ((r is not Lisp_Boolean r_bool) || (r_bool.Value())) {
|
|
_logger.LogDebug("Added '{0}' to Smart Playlist {1}", i, smartPlaylist.Name);
|
|
results.Add(i);
|
|
}
|
|
}
|
|
executor = SetupExecutor();
|
|
executor.environment.Set("*user*", Lisp_Object.FromBase(user));
|
|
executor.environment.Set("*items*", Lisp_Object.FromBase(results));
|
|
results = new List<BaseItem>();
|
|
var sort_result = executor.eval(smartPlaylist.SortProgram);
|
|
if (sort_result is Cons sorted_items) {
|
|
foreach (var i in sorted_items.ToList()) {
|
|
if (i is Lisp_Object iObject && iObject.Value() is BaseItem iBaseItem) {
|
|
results.Add(iBaseItem);
|
|
continue;
|
|
}
|
|
throw new ApplicationException($"Returned sorted list does contain unexpected items, got {i}");
|
|
}
|
|
} else if (sort_result == Lisp_Boolean.FALSE) {
|
|
} else {
|
|
throw new ApplicationException($"Did not return a list of items, returned {sort_result}");
|
|
}
|
|
return results.Select(x => x.Id);
|
|
}
|
|
|
|
private IEnumerable<BaseItem> GetAllUserMedia(User user) {
|
|
var req = new InternalItemsQuery(user) {
|
|
IncludeItemTypes = AvailableFilterItems,
|
|
Recursive = true,
|
|
};
|
|
return _libraryManager.GetItemsResult(req).Items;
|
|
}
|
|
|
|
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) {
|
|
_logger.LogInformation("Started regenerate Smart Playlists");
|
|
_logger.LogDebug("Loaded Assemblies:");
|
|
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) {
|
|
_logger.LogDebug("- {0}", asm);
|
|
}
|
|
var i = 0;
|
|
var all_playlists = await _store.GetAllSmartPlaylistsAsync();
|
|
foreach (SmartPlaylistDto dto in all_playlists) {
|
|
if (!dto.Enabled) {
|
|
i += 1;
|
|
continue;
|
|
}
|
|
var changedDto = false;
|
|
if (dto.Playlists.Length == 0) {
|
|
dto.Playlists = _userManager.UsersIds.Select(x => new SmartPlaylistLinkDto {
|
|
UserId = x,
|
|
PlaylistId = CreateNewPlaylist(dto.Name, x),
|
|
}).ToArray();
|
|
changedDto = true;
|
|
}
|
|
foreach (SmartPlaylistLinkDto playlistLink in dto.Playlists) {
|
|
if (playlistLink.PlaylistId == Guid.Empty) {
|
|
// not initialized
|
|
playlistLink.PlaylistId = CreateNewPlaylist(dto.Name, playlistLink.UserId);
|
|
changedDto = true;
|
|
} else if (_playlistManager.GetPlaylists(playlistLink.UserId).Where(x => x.Id == playlistLink.PlaylistId).ToArray().Length == 0) {
|
|
// somehow the corresponding playlist doesnt
|
|
// exist anymore, did the user delete it?
|
|
playlistLink.PlaylistId = CreateNewPlaylist(dto.Name, playlistLink.UserId);
|
|
changedDto = true;
|
|
}
|
|
}
|
|
if (changedDto) {
|
|
_store.DeleteSmartPlaylist(dto); // delete in case the file was not the canonical one.
|
|
await _store.SaveSmartPlaylistAsync(dto);
|
|
}
|
|
foreach (SmartPlaylistLinkDto playlistLink in dto.Playlists) {
|
|
User? user = _userManager.GetUserById(playlistLink.UserId);
|
|
if (user == null) {
|
|
continue;
|
|
}
|
|
var insertItems = FilterPlaylistItems(GetAllUserMedia(user), user, dto).ToArray();
|
|
var playlist = _playlistManager.GetPlaylists(playlistLink.UserId).Where(x => x.Id == playlistLink.PlaylistId).First();
|
|
await ClearPlaylist(playlist);
|
|
await _playlistManager.AddItemToPlaylistAsync(playlist.Id, insertItems, playlistLink.UserId);
|
|
}
|
|
i += 1;
|
|
progress.Report(100 * ((double)i)/all_playlists.Count());
|
|
}
|
|
}
|
|
|
|
private async Task ClearPlaylist(Playlist playlist) {
|
|
// fuck if I know
|
|
if (_libraryManager.GetItemById(playlist.Id) is not Playlist playlist_new) {
|
|
throw new ArgumentException("");
|
|
}
|
|
var existingItems = playlist_new.GetManageableItems().ToList();
|
|
await _playlistManager.RemoveItemFromPlaylistAsync(playlist.Id.ToString(), existingItems.Select(x => x.Item1.ItemId?.ToString("N", CultureInfo.InvariantCulture)));
|
|
}
|
|
}
|
|
}
|