Cleanup and allow calling methods with arguments.
This commit is contained in:
parent
6208c9c070
commit
faca13d393
6 changed files with 103 additions and 42 deletions
|
@ -31,8 +31,8 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
E Equals(T other);
|
E Equals(T other);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class Expression: IFormattable, IComparable<Expression, bool> {
|
public abstract class Expression: IComparable<Expression, bool> {
|
||||||
public abstract string ToString(string? format, IFormatProvider? provider);
|
public override abstract string ToString();
|
||||||
public abstract override int GetHashCode();
|
public abstract override int GetHashCode();
|
||||||
public abstract bool Equals(Expression other);
|
public abstract bool Equals(Expression other);
|
||||||
public override bool Equals(object? other) {
|
public override bool Equals(object? other) {
|
||||||
|
@ -47,6 +47,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
public static bool operator !=(Expression left, Expression right) {
|
public static bool operator !=(Expression left, Expression right) {
|
||||||
return !left.Equals(right);
|
return !left.Equals(right);
|
||||||
}
|
}
|
||||||
|
public abstract object Inner();
|
||||||
}
|
}
|
||||||
public abstract class Atom : Expression {}
|
public abstract class Atom : Expression {}
|
||||||
public class Symbol : Atom {
|
public class Symbol : Atom {
|
||||||
|
@ -67,7 +68,10 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
public override string ToString(string? format, IFormatProvider? provider) {
|
public override string ToString() {
|
||||||
|
return _name;
|
||||||
|
}
|
||||||
|
public override object Inner() {
|
||||||
return _name;
|
return _name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,9 +94,12 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
public override string ToString(string? format, IFormatProvider? provider) {
|
public override string ToString() {
|
||||||
return _value? "t" : "nil";
|
return _value? "t" : "nil";
|
||||||
}
|
}
|
||||||
|
public override object Inner() {
|
||||||
|
return _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> {
|
||||||
|
@ -113,9 +120,8 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
public override string ToString(string? format, IFormatProvider? provider) {
|
public override string ToString() {
|
||||||
return _value.ToString();
|
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);
|
||||||
|
@ -150,6 +156,9 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
public static Boolean operator !=(Integer a, Integer b) {
|
public static Boolean operator !=(Integer a, Integer b) {
|
||||||
return new Boolean(a.value != b.value);
|
return new Boolean(a.value != b.value);
|
||||||
}
|
}
|
||||||
|
public override object Inner() {
|
||||||
|
return _value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class String : Atom, IAddable<String> {
|
public class String : Atom, IAddable<String> {
|
||||||
|
@ -170,12 +179,15 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
public override string ToString(string? format, IFormatProvider? provider) {
|
public override string ToString() {
|
||||||
return "\"" + _value + "\"";
|
return "\"" + _value + "\"";
|
||||||
}
|
}
|
||||||
public static String operator +(String a, String b) {
|
public static String operator +(String a, String b) {
|
||||||
return new String (a.value + b.value);
|
return new String (a.value + b.value);
|
||||||
}
|
}
|
||||||
|
public override object Inner() {
|
||||||
|
return _value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Object : Atom {
|
public class Object : Atom {
|
||||||
|
@ -196,7 +208,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
public override string ToString(string? format, IFormatProvider? provider) {
|
public override string ToString() {
|
||||||
return _value.ToString();
|
return _value.ToString();
|
||||||
}
|
}
|
||||||
public static Atom FromBase(object o) {
|
public static Atom FromBase(object o) {
|
||||||
|
@ -211,6 +223,9 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
return new Object(o);
|
return new Object(o);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public override object Inner() {
|
||||||
|
return _value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class List : Expression {
|
public class List : Expression {
|
||||||
|
@ -233,8 +248,8 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
public override string ToString(string? format, IFormatProvider? provider) {
|
public override string ToString() {
|
||||||
return "(" + string.Join(" ", _expressions.Select(x => x.ToString("0", provider))) + ")";
|
return "(" + string.Join(" ", _expressions.Select(x => x.ToString())) + ")";
|
||||||
}
|
}
|
||||||
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>();
|
||||||
|
@ -242,6 +257,9 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp.Compiler {
|
||||||
r.AddRange(b.expressions);
|
r.AddRange(b.expressions);
|
||||||
return new List(r);
|
return new List(r);
|
||||||
}
|
}
|
||||||
|
public override object Inner() {
|
||||||
|
return _expressions.Select(x => x.Inner()).ToArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Parser {
|
public class Parser {
|
||||||
|
|
|
@ -30,6 +30,7 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
this["length"] = _length;
|
this["length"] = _length;
|
||||||
this["haskeys"] = _haskeys;
|
this["haskeys"] = _haskeys;
|
||||||
this["getitems"] = _getitems;
|
this["getitems"] = _getitems;
|
||||||
|
this["invoke"] = _invoke;
|
||||||
//this[new Symbol("!=")] = _ne;
|
//this[new Symbol("!=")] = _ne;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,20 +203,27 @@ namespace Jellyfin.Plugin.SmartPlaylist.Lisp {
|
||||||
r.Add(Compiler.Object.FromBase(pi.GetValue(o.value)));
|
r.Add(Compiler.Object.FromBase(pi.GetValue(o.value)));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
MethodInfo? mi = o.value.GetType().GetMethod(s.value);
|
|
||||||
if (mi != null) {
|
|
||||||
r.Add(Compiler.Object.FromBase(mi.Invoke(o.value, null)));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
FieldInfo? fi = o.value.GetType().GetField(s.value);
|
FieldInfo? fi = o.value.GetType().GetField(s.value);
|
||||||
if (fi != null) {
|
if (fi != null) {
|
||||||
r.Add(Compiler.Object.FromBase(fi.GetValue(o.value)));
|
r.Add(Compiler.Object.FromBase(fi.GetValue(o.value)));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
throw new ApplicationException();
|
throw new ApplicationException($"{o.value} has no property or field {s.value}");
|
||||||
}
|
}
|
||||||
return new Compiler.List(r);
|
return new Compiler.List(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Expression _invoke(IList<Expression> args) {
|
||||||
|
Compiler.Object o = (Compiler.Object) args[0];
|
||||||
|
Compiler.String s = (Compiler.String) args[1];
|
||||||
|
Compiler.List l = (Compiler.List) args[2];
|
||||||
|
IList<Expression> r = new List<Expression>();
|
||||||
|
MethodInfo? mi = o.value.GetType().GetMethod(s.value);
|
||||||
|
if (mi == null) {
|
||||||
|
throw new ApplicationException($"{o.value} has not method {s.value}");
|
||||||
|
}
|
||||||
|
return Compiler.Object.FromBase(mi.Invoke(o.value, (object?[]?) l.Inner()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BuiltinsLater : Dictionary<string, FunctionLater> {
|
public class BuiltinsLater : Dictionary<string, FunctionLater> {
|
||||||
|
|
|
@ -38,6 +38,8 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
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 IProviderManager _providerManager;
|
||||||
|
private readonly IFileSystem _fileSystem;
|
||||||
private readonly IPlaylistManager _playlistManager;
|
private readonly IPlaylistManager _playlistManager;
|
||||||
|
|
||||||
private readonly IStore _store;
|
private readonly IStore _store;
|
||||||
|
@ -46,15 +48,19 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
ILogger<Plugin> logger,
|
ILogger<Plugin> logger,
|
||||||
ILibraryManager libraryManager,
|
ILibraryManager libraryManager,
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
|
IProviderManager providerManager,
|
||||||
|
IFileSystem fileSystem,
|
||||||
IPlaylistManager playlistManager,
|
IPlaylistManager playlistManager,
|
||||||
IServerApplicationPaths serverApplicationPaths
|
IServerApplicationPaths serverApplicationPaths
|
||||||
) {
|
) {
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
|
_providerManager = providerManager;
|
||||||
|
_fileSystem = fileSystem;
|
||||||
_playlistManager = playlistManager;
|
_playlistManager = playlistManager;
|
||||||
|
|
||||||
_store = new Store(new FileSystem(serverApplicationPaths));
|
_store = new Store(new SmartPlaylistFileSystem(serverApplicationPaths));
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Category => "Library";
|
public string Category => "Library";
|
||||||
|
@ -84,24 +90,29 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SmartPlaylistId CreateNewPlaylist(SmartPlaylistDto dto, User user) {
|
private SmartPlaylistId CreateNewPlaylist(SmartPlaylistDto dto) {
|
||||||
var req = new PlaylistCreationRequest {
|
var req = new PlaylistCreationRequest {
|
||||||
Name = dto.Name,
|
Name = dto.Name,
|
||||||
UserId = user.Id
|
UserId = dto.User
|
||||||
};
|
};
|
||||||
return Guid.Parse(_playlistManager.CreatePlaylist(req).Result.Id);
|
var playlistGuid = Guid.Parse(_playlistManager.CreatePlaylist(req).Result.Id);
|
||||||
|
return playlistGuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<Guid> FilterPlaylistItems(IEnumerable<BaseItem> items, User user, SmartPlaylistDto smartPlaylist) {
|
private IEnumerable<Guid> FilterPlaylistItems(IEnumerable<BaseItem> items, User user, SmartPlaylistDto smartPlaylist) {
|
||||||
List<Guid> results = new List<Guid>();
|
List<Guid> results = new List<Guid>();
|
||||||
Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse();
|
Expression expression = new Parser(StringTokenStream.generate(smartPlaylist.Program)).parse();
|
||||||
Executor executor = new Executor();
|
Executor executor = new Executor();
|
||||||
|
executor.environment["user"] = new Lisp_Object(user);
|
||||||
foreach (var i in items) {
|
foreach (var i in items) {
|
||||||
executor.environment["item"] = new Lisp_Object(i);
|
executor.environment["item"] = new Lisp_Object(i);
|
||||||
var r = executor.eval(expression);
|
var r = executor.eval(expression);
|
||||||
_logger.LogInformation("Item {0} evaluated to {1}", i, r.ToString());
|
_logger.LogDebug("Item {0} evaluated to {1}", i, r.ToString());
|
||||||
if (r is Lisp_Boolean r_bool) {
|
if (r is Lisp_Boolean r_bool) {
|
||||||
if (r_bool.value) { results.Add(i.Id); }
|
if (r_bool.value) {
|
||||||
|
_logger.LogDebug("Added "{0}" to Smart Playlist {1}", i, smartPlaylist.Name);
|
||||||
|
results.Add(i.Id);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_logger.LogInformation("Program did not return a boolean, returned {0}", r.ToString());
|
_logger.LogInformation("Program did not return a boolean, returned {0}", r.ToString());
|
||||||
}
|
}
|
||||||
|
@ -124,8 +135,8 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
List<Playlist> playlists = _playlistManager.GetPlaylists(user.Id).Where(x => x.Id == dto.Id).ToList();
|
List<Playlist> playlists = _playlistManager.GetPlaylists(user.Id).Where(x => x.Id == dto.Id).ToList();
|
||||||
if ((dto.Id == null) || !playlists.Any()) {
|
if ((dto.Id == null) || !playlists.Any()) {
|
||||||
_logger.LogInformation("Generating new smart playlist (dto.Id = {0}, playlists.Any() = {1})", dto.Id, playlists.Any());
|
_logger.LogInformation("Generating new smart playlist (dto.Id = {0}, playlists.Any() = {1})", dto.Id, playlists.Any());
|
||||||
_store.DeleteSmartPlaylist(dto.Id);
|
_store.DeleteSmartPlaylist(dto);
|
||||||
dto.Id = CreateNewPlaylist(dto, user);
|
dto.Id = CreateNewPlaylist(dto);
|
||||||
await _store.SaveSmartPlaylistAsync(dto);
|
await _store.SaveSmartPlaylistAsync(dto);
|
||||||
playlists = _playlistManager.GetPlaylists(user.Id).Where(x => x.Id == dto.Id).ToList();
|
playlists = _playlistManager.GetPlaylists(user.Id).Where(x => x.Id == dto.Id).ToList();
|
||||||
}
|
}
|
||||||
|
@ -137,13 +148,12 @@ namespace Jellyfin.Plugin.SmartPlaylist.ScheduledTasks {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ClearPlaylist(SmartPlaylistDto smartPlaylist, Playlist playlist, User user) {
|
private async Task ClearPlaylist(SmartPlaylistDto smartPlaylist, Playlist playlist, User user) {
|
||||||
var req = new InternalItemsQuery(user)
|
// fuck if I know
|
||||||
{
|
if (_libraryManager.GetItemById(playlist.Id) is not Playlist playlist_new) {
|
||||||
IncludeItemTypes = AvailableFilterItems,
|
throw new ArgumentException("");
|
||||||
Recursive = true
|
}
|
||||||
};
|
var existingItems = playlist_new.GetManageableItems().ToList();
|
||||||
var existingItems = playlist.GetChildren(user, false, req).Select(x => x.Id.ToString()).ToList();
|
await _playlistManager.RemoveItemFromPlaylistAsync(playlist.Id.ToString(), existingItems.Select(x => x.Item1.Id));
|
||||||
await _playlistManager.RemoveItemFromPlaylistAsync(playlist.Id.ToString(), existingItems);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,28 @@
|
||||||
|
using System.Runtime.Serialization;
|
||||||
namespace Jellyfin.Plugin.SmartPlaylist {
|
namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class SmartPlaylistDto {
|
public class SmartPlaylistDto {
|
||||||
|
private string DEFAULT_PROGRAM = "(begin (invoke item 'IsFavoriteOrLiked' (user)))";
|
||||||
public SmartPlaylistId Id { get; set; }
|
public SmartPlaylistId Id { get; set; }
|
||||||
public string Name { get; set; }
|
public string? Name { get; set; }
|
||||||
public string FileName { get; set; }
|
|
||||||
public UserId User { get; set; }
|
public UserId User { get; set; }
|
||||||
public string Program { get; set; }
|
public string? Program { get; set; }
|
||||||
public int MaxItems { get; set; }
|
public string? Filename { get; set; }
|
||||||
|
public int MaxItems { get; set; } = -1;
|
||||||
|
|
||||||
|
public void Fill(string filename) {
|
||||||
|
if (Id == Guid.Empty) {
|
||||||
|
Id = Guid.NewGuid();
|
||||||
|
}
|
||||||
|
if (Name == null) {
|
||||||
|
Name = Id.ToString();
|
||||||
|
}
|
||||||
|
if (Program == null) {
|
||||||
|
Program = DEFAULT_PROGRAM;
|
||||||
|
}
|
||||||
|
if (Filename == null) {
|
||||||
|
Filename = filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,8 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
public string[] FindAllSmartPlaylistFilePaths();
|
public string[] FindAllSmartPlaylistFilePaths();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FileSystem : ISmartPlaylistFileSystem {
|
public class SmartPlaylistFileSystem : ISmartPlaylistFileSystem {
|
||||||
public FileSystem(IServerApplicationPaths serverApplicationPaths) {
|
public SmartPlaylistFileSystem(IServerApplicationPaths serverApplicationPaths) {
|
||||||
StoragePath = Path.Combine(serverApplicationPaths.DataPath, "smartplaylists");
|
StoragePath = Path.Combine(serverApplicationPaths.DataPath, "smartplaylists");
|
||||||
if (!Directory.Exists(StoragePath)) { Directory.CreateDirectory(StoragePath); }
|
if (!Directory.Exists(StoragePath)) { Directory.CreateDirectory(StoragePath); }
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
Task<SmartPlaylistDto?> GetSmartPlaylistAsync(SmartPlaylistId smartPlaylistId);
|
Task<SmartPlaylistDto?> GetSmartPlaylistAsync(SmartPlaylistId smartPlaylistId);
|
||||||
Task<SmartPlaylistDto[]> GetAllSmartPlaylistsAsync();
|
Task<SmartPlaylistDto[]> GetAllSmartPlaylistsAsync();
|
||||||
Task SaveSmartPlaylistAsync(SmartPlaylistDto smartPlaylist);
|
Task SaveSmartPlaylistAsync(SmartPlaylistDto smartPlaylist);
|
||||||
void DeleteSmartPlaylist(SmartPlaylistId smartPlaylistId);
|
void DeleteSmartPlaylist(SmartPlaylistDto smartPlaylist);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Store : IStore {
|
public class Store : IStore {
|
||||||
|
@ -15,7 +15,9 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
}
|
}
|
||||||
private async Task<SmartPlaylistDto?> LoadPlaylistAsync(string filename) {
|
private async Task<SmartPlaylistDto?> LoadPlaylistAsync(string filename) {
|
||||||
await using var r = File.OpenRead(filename);
|
await using var r = File.OpenRead(filename);
|
||||||
return await JsonSerializer.DeserializeAsync<SmartPlaylistDto>(r).ConfigureAwait(false);
|
var dto = (await JsonSerializer.DeserializeAsync<SmartPlaylistDto>(r).ConfigureAwait(false));
|
||||||
|
dto.Fill(filename);
|
||||||
|
return dto;
|
||||||
}
|
}
|
||||||
public async Task<SmartPlaylistDto?> GetSmartPlaylistAsync(SmartPlaylistId smartPlaylistId) {
|
public async Task<SmartPlaylistDto?> GetSmartPlaylistAsync(SmartPlaylistId smartPlaylistId) {
|
||||||
string filename = _fileSystem.FindSmartPlaylistFilePath(smartPlaylistId);
|
string filename = _fileSystem.FindSmartPlaylistFilePath(smartPlaylistId);
|
||||||
|
@ -31,9 +33,15 @@ namespace Jellyfin.Plugin.SmartPlaylist {
|
||||||
await using var w = File.Create(filename);
|
await using var w = File.Create(filename);
|
||||||
await JsonSerializer.SerializeAsync(w, smartPlaylist).ConfigureAwait(false);
|
await JsonSerializer.SerializeAsync(w, smartPlaylist).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
public void DeleteSmartPlaylist(SmartPlaylistId smartPlaylistId) {
|
private void DeleteSmartPlaylistById(SmartPlaylistId smartPlaylistId) {
|
||||||
|
try {
|
||||||
string filename = _fileSystem.FindSmartPlaylistFilePath(smartPlaylistId);
|
string filename = _fileSystem.FindSmartPlaylistFilePath(smartPlaylistId);
|
||||||
if (File.Exists(filename)) { File.Delete(filename); }
|
if (File.Exists(filename)) { File.Delete(filename); }
|
||||||
|
} catch (System.InvalidOperationException) {}
|
||||||
|
}
|
||||||
|
public void DeleteSmartPlaylist(SmartPlaylistDto smartPlaylist) {
|
||||||
|
if (File.Exists(smartPlaylist.Filename)) { File.Delete(smartPlaylist.Filename); }
|
||||||
|
DeleteSmartPlaylistById(smartPlaylist.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue