Implement first playlist generation, still much to do.

This commit is contained in:
redxef 2024-10-24 23:53:21 +02:00
parent 1f022d7f88
commit 6208c9c070
Signed by: redxef
GPG key ID: 7DAC3AA211CBD921
6 changed files with 262 additions and 21 deletions

View file

@ -28,10 +28,25 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
interface IComparable<T, E> where T : IComparable<T, E> { interface IComparable<T, E> where T : IComparable<T, E> {
static abstract E operator ==(T left, T right); static abstract E operator ==(T left, T right);
static abstract E operator !=(T left, T right); static abstract E operator !=(T left, T right);
E Equals(T other);
} }
public abstract class Expression : IFormattable { public abstract class Expression: IFormattable, IComparable<Expression, bool> {
public abstract string ToString(string? format, IFormatProvider? provider); public abstract string ToString(string? format, IFormatProvider? provider);
public abstract override int GetHashCode();
public abstract bool Equals(Expression other);
public override bool Equals(object? other) {
if (other is Expression other_e) {
return Equals(other_e);
}
return false;
}
public static bool operator ==(Expression left, Expression right) {
return left.Equals(right);
}
public static bool operator !=(Expression left, Expression right) {
return !left.Equals(right);
}
} }
public abstract class Atom : Expression {} public abstract class Atom : Expression {}
public class Symbol : Atom { public class Symbol : Atom {
@ -40,34 +55,67 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
_name = name; _name = name;
} }
public string name { get => _name; } public string name { get => _name; }
public override int GetHashCode() {
int hash = 17;
hash *= 23;
hash += _name.GetHashCode();
return hash;
}
public override bool Equals(Expression? other) {
if (other is Symbol other_s) {
return _name == other_s._name;
}
return false;
}
public override string ToString(string? format, IFormatProvider? provider) { public override string ToString(string? format, IFormatProvider? provider) {
return _name; return _name;
} }
} }
public class Boolean : Atom, IComparable<Boolean, Boolean> {
public class Boolean : Atom {
private readonly bool _value; private readonly bool _value;
public Boolean(bool value) { public Boolean(bool value) {
_value = value; _value = value;
} }
public bool value { get => _value; } public bool value { get => _value; }
public override int GetHashCode() {
int hash = 17;
hash *= 23;
hash += _value.GetHashCode();
return hash;
}
public override bool Equals(Expression other) {
if (other is Boolean other_b) {
return _value == other_b.value;
}
return false;
}
public override string ToString(string? format, IFormatProvider? provider) { public override string ToString(string? format, IFormatProvider? provider) {
return _value? "t" : "nil"; return _value? "t" : "nil";
} }
public static Boolean operator ==(Boolean a, Boolean b) {
return new Boolean(a.value == b.value);
} }
public static Boolean operator !=(Boolean a, Boolean b) {
return new Boolean(a.value != b.value); public class Integer : Atom, IAddable<Integer>, ISubtractable<Integer>, IMultiplicatable<Integer>, IDivisible<Integer>, ISortable<Integer, Boolean> {
}
}
public class Integer : Atom, IAddable<Integer>, ISubtractable<Integer>, IMultiplicatable<Integer>, IDivisible<Integer>, ISortable<Integer, Boolean>, IComparable<Integer, Boolean> {
private readonly int _value; private readonly int _value;
public Integer(int value) { public Integer(int value) {
_value = value; _value = value;
} }
public int value { get => _value; } public int value { get => _value; }
public override int GetHashCode() {
int hash = 17;
hash *= 23;
hash += _value.GetHashCode();
return hash;
}
public override bool Equals(Expression other) {
if (other is Integer other_i) {
return _value == other_i._value;
}
return false;
}
public override string ToString(string? format, IFormatProvider? provider) { public override string ToString(string? format, IFormatProvider? provider) {
return _value.ToString("0", provider); return _value.ToString();
//return _value.ToString("0", provider);
} }
public static Integer operator +(Integer a, Integer b) { public static Integer operator +(Integer a, Integer b) {
return new Integer(a.value + b.value); return new Integer(a.value + b.value);
@ -103,12 +151,25 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
return new Boolean(a.value != b.value); return new Boolean(a.value != b.value);
} }
} }
public class String : Atom, IAddable<String> { public class String : Atom, IAddable<String> {
private readonly string _value; private readonly string _value;
public String(string value) { public String(string value) {
_value = value; _value = value;
} }
public string value { get => _value; } public string value { get => _value; }
public override int GetHashCode() {
int hash = 17;
hash *= 23;
hash += _value.GetHashCode();
return hash;
}
public override bool Equals(Expression other) {
if (other is String other_s) {
return _value == other_s._value;
}
return false;
}
public override string ToString(string? format, IFormatProvider? provider) { public override string ToString(string? format, IFormatProvider? provider) {
return "\"" + _value + "\""; return "\"" + _value + "\"";
} }
@ -116,12 +177,25 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
return new String (a.value + b.value); return new String (a.value + b.value);
} }
} }
public class Object : Atom { public class Object : Atom {
private readonly object _value; private readonly object _value;
public Object(object value) { public Object(object value) {
_value = value; _value = value;
} }
public object value { get => _value; } public object value { get => _value; }
public override int GetHashCode() {
int hash = 17;
hash *= 23;
hash += _value.GetHashCode();
return hash;
}
public override bool Equals(Expression other) {
if (other is Object other_o) {
return _value == other_o._value;
}
return false;
}
public override string ToString(string? format, IFormatProvider? provider) { public override string ToString(string? format, IFormatProvider? provider) {
return _value.ToString(); return _value.ToString();
} }
@ -145,13 +219,22 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
_expressions = expressions; _expressions = expressions;
} }
public IList<Expression> expressions { get => _expressions; } public IList<Expression> expressions { get => _expressions; }
public override string ToString(string? format, IFormatProvider? provider) { public override int GetHashCode() {
string r = "("; int hash = 17;
foreach (var e in _expressions) { foreach (Expression i in _expressions) {
r += " "; hash *= 23;
r += e.ToString("0", provider); hash += i.GetHashCode();
} }
return r + ")"; return hash;
}
public override bool Equals(Expression other) {
if (other is List other_l) {
return _expressions == other_l._expressions;
}
return false;
}
public override string ToString(string? format, IFormatProvider? provider) {
return "(" + string.Join(" ", _expressions.Select(x => x.ToString("0", provider))) + ")";
} }
public static List operator +(List a, List b) { public static List operator +(List a, List b) {
List<Expression> r = new List<Expression>(); List<Expression> r = new List<Expression>();

View file

@ -22,20 +22,39 @@ using MediaBrowser.Model.Playlists;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Jellyfin.Plugin.SmartPlaylist.Lisp;
using Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler;
using Lisp_Object = Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler.Object;
using Lisp_Boolean = Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler.Boolean;
namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks { namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
public class GeneratePlaylist : IScheduledTask { public class GeneratePlaylist : IScheduledTask {
public static readonly BaseItemKind[] AvailableFilterItems = {
BaseItemKind.Audio
};
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IPlaylistManager _playlistManager;
private readonly IStore _store;
public GeneratePlaylist( public GeneratePlaylist(
ILogger<Plugin> logger, ILogger<Plugin> logger,
ILibraryManager libraryManager, ILibraryManager libraryManager,
IUserManager userManager IUserManager userManager,
IPlaylistManager playlistManager,
IServerApplicationPaths serverApplicationPaths
) { ) {
_logger = logger; _logger = logger;
_libraryManager = libraryManager; _libraryManager = libraryManager;
_userManager = userManager; _userManager = userManager;
_playlistManager = playlistManager;
_store = new Store(new FileSystem(serverApplicationPaths));
} }
public string Category => "Library"; public string Category => "Library";
@ -46,7 +65,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() { public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() {
return new[] { return new[] {
new TaskTriggerInfo { new TaskTriggerInfo {
IntervalTicks = TimeSpan.FromMinutes(1).Ticks, IntervalTicks = TimeSpan.FromHours(24).Ticks,
Type = TaskTriggerInfo.TriggerInterval, Type = TaskTriggerInfo.TriggerInterval,
} }
}; };
@ -56,7 +75,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
foreach (var user in _userManager.Users) { foreach (var user in _userManager.Users) {
_logger.LogInformation("User {0}", user); _logger.LogInformation("User {0}", user);
var query = new InternalItemsQuery(user) { var query = new InternalItemsQuery(user) {
IncludeItemTypes = new[] {BaseItemKind.Audio}, IncludeItemTypes = AvailableFilterItems,
Recursive = true, Recursive = true,
}; };
foreach (BaseItem item in _libraryManager.GetItemsResult(query).Items) { foreach (BaseItem item in _libraryManager.GetItemsResult(query).Items) {
@ -65,9 +84,66 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
} }
} }
private SmartPlaylistId CreateNewPlaylist(SmartPlaylistDto dto, User user) {
var req = new PlaylistCreationRequest {
Name = dto.Name,
UserId = user.Id
};
return Guid.Parse(_playlistManager.CreatePlaylist(req).Result.Id);
}
private IEnumerable<Guid> FilterPlaylistItems(IEnumerable<BaseItem> items, User user, SmartPlaylistDto smartPlaylist) {
List<Guid> results = new List<Guid>();
Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse();
Executor executor = new Executor();
foreach (var i in items) {
executor.environment["item"] = new Lisp_Object(i);
var r = executor.eval(expression);
_logger.LogInformation("Item {0} evaluated to {1}", i, r.ToString());
if (r is Lisp_Boolean r_bool) {
if (r_bool.value) { results.Add(i.Id); }
} else {
_logger.LogInformation("Program did not return a boolean, returned {0}", r.ToString());
}
}
return results;
}
private IEnumerable<BaseItem> GetAllUserMedia(User user) {
var req = new InternalItemsQuery(user) {
IncludeItemTypes = new[] {BaseItemKind.Audio},
Recursive = true,
};
return _libraryManager.GetItemsResult(req).Items;
}
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) {
_logger.LogInformation("This is a test"); _logger.LogInformation("Started regenerate Smart Playlists");
GetUsers(); foreach (SmartPlaylistDto dto in await _store.GetAllSmartPlaylistsAsync()) {
var user = _userManager.GetUserById(dto.User);
List<Playlist> playlists = _playlistManager.GetPlaylists(user.Id).Where(x => x.Id == dto.Id).ToList();
if ((dto.Id == null) || !playlists.Any()) {
_logger.LogInformation("Generating new smart playlist (dto.Id = {0}, playlists.Any() = {1})", dto.Id, playlists.Any());
_store.DeleteSmartPlaylist(dto.Id);
dto.Id = CreateNewPlaylist(dto, user);
await _store.SaveSmartPlaylistAsync(dto);
playlists = _playlistManager.GetPlaylists(user.Id).Where(x => x.Id == dto.Id).ToList();
}
var insertItems = FilterPlaylistItems(GetAllUserMedia(user), user, dto);
Playlist playlist = playlists.First();
await ClearPlaylist(dto, playlist, user);
await _playlistManager.AddItemToPlaylistAsync(playlist.Id, insertItems.ToArray(), user.Id);
}
}
private async Task ClearPlaylist(SmartPlaylistDto smartPlaylist, Playlist playlist, User user) {
var req = new InternalItemsQuery(user)
{
IncludeItemTypes = AvailableFilterItems,
Recursive = true
};
var existingItems = playlist.GetChildren(user, false, req).Select(x => x.Id.ToString()).ToList();
await _playlistManager.RemoveItemFromPlaylistAsync(playlist.Id.ToString(), existingItems);
} }
} }
} }

View file

@ -0,0 +1,11 @@
namespace Jellyfin.Plugin.SmartPlaylist {
[Serializable]
public class SmartPlaylistDto {
public SmartPlaylistId Id { get; set; }
public string Name { get; set; }
public string FileName { get; set; }
public UserId User { get; set; }
public string Program { get; set; }
public int MaxItems { get; set; }
}
}

View file

@ -0,0 +1,28 @@
using MediaBrowser.Controller;
namespace Jellyfin.Plugin.SmartPlaylist {
public interface ISmartPlaylistFileSystem {
public string StoragePath { get; }
public string GetSmartPlaylistFilePath(SmartPlaylistId smartPlaylistId);
public string FindSmartPlaylistFilePath(SmartPlaylistId smartPlaylistId);
public string[] FindAllSmartPlaylistFilePaths();
}
public class FileSystem : ISmartPlaylistFileSystem {
public FileSystem(IServerApplicationPaths serverApplicationPaths) {
StoragePath = Path.Combine(serverApplicationPaths.DataPath, "smartplaylists");
if (!Directory.Exists(StoragePath)) { Directory.CreateDirectory(StoragePath); }
}
public string StoragePath { get; }
public string GetSmartPlaylistFilePath(SmartPlaylistId smartPlaylistId) {
return Path.Combine(StoragePath, $"{smartPlaylistId}.json");
}
public string FindSmartPlaylistFilePath(SmartPlaylistId smartPlaylistId) {
return Directory.GetFiles(StoragePath, $"{smartPlaylistId}.json", SearchOption.AllDirectories).First();
}
public string[] FindAllSmartPlaylistFilePaths() {
return Directory.GetFiles(StoragePath);
}
}
}

View file

@ -0,0 +1,39 @@
using System.Text.Json;
namespace Jellyfin.Plugin.SmartPlaylist {
public interface IStore {
Task<SmartPlaylistDto?> GetSmartPlaylistAsync(SmartPlaylistId smartPlaylistId);
Task<SmartPlaylistDto[]> GetAllSmartPlaylistsAsync();
Task SaveSmartPlaylistAsync(SmartPlaylistDto smartPlaylist);
void DeleteSmartPlaylist(SmartPlaylistId smartPlaylistId);
}
public class Store : IStore {
private readonly ISmartPlaylistFileSystem _fileSystem;
public Store(ISmartPlaylistFileSystem fileSystem) {
_fileSystem = fileSystem;
}
private async Task<SmartPlaylistDto?> LoadPlaylistAsync(string filename) {
await using var r = File.OpenRead(filename);
return await JsonSerializer.DeserializeAsync<SmartPlaylistDto>(r).ConfigureAwait(false);
}
public async Task<SmartPlaylistDto?> GetSmartPlaylistAsync(SmartPlaylistId smartPlaylistId) {
string filename = _fileSystem.FindSmartPlaylistFilePath(smartPlaylistId);
return await LoadPlaylistAsync(filename).ConfigureAwait(false);
}
public async Task<SmartPlaylistDto[]> GetAllSmartPlaylistsAsync() {
var t = _fileSystem.FindAllSmartPlaylistFilePaths().Select(LoadPlaylistAsync).ToArray();
await Task.WhenAll(t).ConfigureAwait(false);
return t.Where(x => x != null).Select(x => x.Result).ToArray();
}
public async Task SaveSmartPlaylistAsync(SmartPlaylistDto smartPlaylist) {
string filename = _fileSystem.GetSmartPlaylistFilePath(smartPlaylist.Id);
await using var w = File.Create(filename);
await JsonSerializer.SerializeAsync(w, smartPlaylist).ConfigureAwait(false);
}
public void DeleteSmartPlaylist(SmartPlaylistId smartPlaylistId) {
string filename = _fileSystem.FindSmartPlaylistFilePath(smartPlaylistId);
if (File.Exists(filename)) { File.Delete(filename); }
}
}
}

View file

@ -0,0 +1,4 @@
global using System;
global using UserId = System.Guid;
global using SmartPlaylistId = System.Guid;