Implement first playlist generation, still much to do.
This commit is contained in:
parent
1f022d7f88
commit
6208c9c070
6 changed files with 262 additions and 21 deletions
|
@ -28,10 +28,25 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
|||
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);
|
||||
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 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 class Symbol : Atom {
|
||||
|
@ -40,34 +55,67 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
|||
_name = 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) {
|
||||
return _name;
|
||||
}
|
||||
}
|
||||
public class Boolean : Atom, IComparable<Boolean, Boolean> {
|
||||
|
||||
public class Boolean : Atom {
|
||||
private readonly bool _value;
|
||||
public Boolean(bool value) {
|
||||
_value = 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) {
|
||||
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>, IComparable<Integer, Boolean> {
|
||||
|
||||
public class Integer : Atom, IAddable<Integer>, ISubtractable<Integer>, IMultiplicatable<Integer>, IDivisible<Integer>, ISortable<Integer, Boolean> {
|
||||
private readonly int _value;
|
||||
public Integer(int value) {
|
||||
_value = 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) {
|
||||
return _value.ToString("0", provider);
|
||||
return _value.ToString();
|
||||
//return _value.ToString("0", provider);
|
||||
}
|
||||
public static Integer operator +(Integer a, Integer b) {
|
||||
return new Integer(a.value + b.value);
|
||||
|
@ -103,12 +151,25 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
|||
return new Boolean(a.value != b.value);
|
||||
}
|
||||
}
|
||||
|
||||
public class String : Atom, IAddable<String> {
|
||||
private readonly string _value;
|
||||
public String(string value) {
|
||||
_value = 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) {
|
||||
return "\"" + _value + "\"";
|
||||
}
|
||||
|
@ -116,12 +177,25 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
|||
return new String (a.value + b.value);
|
||||
}
|
||||
}
|
||||
|
||||
public class Object : Atom {
|
||||
private readonly object _value;
|
||||
public Object(object value) {
|
||||
_value = 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) {
|
||||
return _value.ToString();
|
||||
}
|
||||
|
@ -145,13 +219,22 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
|||
_expressions = expressions;
|
||||
}
|
||||
public IList<Expression> expressions { get => _expressions; }
|
||||
public override string ToString(string? format, IFormatProvider? provider) {
|
||||
string r = "(";
|
||||
foreach (var e in _expressions) {
|
||||
r += " ";
|
||||
r += e.ToString("0", provider);
|
||||
public override int GetHashCode() {
|
||||
int hash = 17;
|
||||
foreach (Expression i in _expressions) {
|
||||
hash *= 23;
|
||||
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) {
|
||||
List<Expression> r = new List<Expression>();
|
||||
|
|
|
@ -22,20 +22,39 @@ using MediaBrowser.Model.Playlists;
|
|||
using MediaBrowser.Model.Tasks;
|
||||
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 {
|
||||
public class GeneratePlaylist : IScheduledTask {
|
||||
|
||||
public static readonly BaseItemKind[] AvailableFilterItems = {
|
||||
BaseItemKind.Audio
|
||||
};
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IPlaylistManager _playlistManager;
|
||||
|
||||
private readonly IStore _store;
|
||||
|
||||
public GeneratePlaylist(
|
||||
ILogger<Plugin> logger,
|
||||
ILibraryManager libraryManager,
|
||||
IUserManager userManager
|
||||
IUserManager userManager,
|
||||
IPlaylistManager playlistManager,
|
||||
IServerApplicationPaths serverApplicationPaths
|
||||
) {
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
_playlistManager = playlistManager;
|
||||
|
||||
_store = new Store(new FileSystem(serverApplicationPaths));
|
||||
}
|
||||
|
||||
public string Category => "Library";
|
||||
|
@ -46,7 +65,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
|||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() {
|
||||
return new[] {
|
||||
new TaskTriggerInfo {
|
||||
IntervalTicks = TimeSpan.FromMinutes(1).Ticks,
|
||||
IntervalTicks = TimeSpan.FromHours(24).Ticks,
|
||||
Type = TaskTriggerInfo.TriggerInterval,
|
||||
}
|
||||
};
|
||||
|
@ -56,7 +75,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
|||
foreach (var user in _userManager.Users) {
|
||||
_logger.LogInformation("User {0}", user);
|
||||
var query = new InternalItemsQuery(user) {
|
||||
IncludeItemTypes = new[] {BaseItemKind.Audio},
|
||||
IncludeItemTypes = AvailableFilterItems,
|
||||
Recursive = true,
|
||||
};
|
||||
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) {
|
||||
_logger.LogInformation("This is a test");
|
||||
GetUsers();
|
||||
_logger.LogInformation("Started regenerate Smart Playlists");
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
11
Jellyfin.Plugin.SmartPlaylist/SmartPlaylistDto.cs
Normal file
11
Jellyfin.Plugin.SmartPlaylist/SmartPlaylistDto.cs
Normal 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; }
|
||||
}
|
||||
}
|
28
Jellyfin.Plugin.SmartPlaylist/SmartPlaylistFileSystem.cs
Normal file
28
Jellyfin.Plugin.SmartPlaylist/SmartPlaylistFileSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
39
Jellyfin.Plugin.SmartPlaylist/Store.cs
Normal file
39
Jellyfin.Plugin.SmartPlaylist/Store.cs
Normal 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); }
|
||||
}
|
||||
}
|
||||
}
|
4
Jellyfin.Plugin.SmartPlaylist/Types.cs
Normal file
4
Jellyfin.Plugin.SmartPlaylist/Types.cs
Normal file
|
@ -0,0 +1,4 @@
|
|||
global using System;
|
||||
|
||||
global using UserId = System.Guid;
|
||||
global using SmartPlaylistId = System.Guid;
|
Loading…
Reference in a new issue