From 7bd021874f9856ef46c8b9e25f090f08c8e7b4e3 Mon Sep 17 00:00:00 2001 From: pudding Date: Sun, 28 Jan 2024 14:05:45 +0000 Subject: [PATCH] Support custom tags provided by plugin (scripts). Settings dialog has new tab to define custom tags. These tags can be then be consumed by other scripts (library/playlist) or read directly. The values are persisted to the database so can be searched etc like normal meta data. --- FoxTunes.Core/Extensions.cs | 9 + .../Database/IDatabaseInitializer.cs | 3 +- .../Interfaces/Database/IDatabaseTables.cs | 2 + .../IDatabaseFactory.cs | 0 .../Factories/IMessageSinkFactory.cs | 8 +- .../Factories/IMetaDataDecoratorFactory.cs | 13 + .../Interfaces/IStandardComponents.cs | 2 + .../Interfaces/IStandardFactories.cs | 2 + FoxTunes.Core/Interfaces/IStandardManagers.cs | 2 + .../Managers/IMetaDataProviderManager.cs | 9 + .../Interfaces/MetaData/IMetaDataDecorator.cs | 13 + .../Interfaces/MetaData/IMetaDataProvider.cs | 13 + .../MetaData/IMetaDataProviderCache.cs | 10 + .../Library/LibraryHierarchyPopulator.cs | 14 +- FoxTunes.Core/Library/LibraryUpdater.cs | 4 + .../Managers/MetaDataProviderManager.cs | 95 ++++++++ FoxTunes.Core/MetaData/MetaDataItem.cs | 3 +- FoxTunes.Core/MetaData/MetaDataProvider.cs | 122 ++++++++++ .../MetaData/MetaDataProviderCache.cs | 105 ++++++++ .../MetaData/MetaDataRefreshBehaviour.cs | 24 +- FoxTunes.Core/Playlist/PlaylistBrowser.cs | 3 - FoxTunes.Core/Scripting/ScriptRunner.cs | 45 +++- FoxTunes.Core/Signal/CommonSignals.cs | 2 + FoxTunes.Core/StandardComponents.cs | 8 + FoxTunes.Core/StandardFactories.cs | 8 + FoxTunes.Core/StandardManagers.cs | 8 + FoxTunes.DB.SQLite/Resources/Database.sql | 8 + FoxTunes.DB.SqlServer/Resources.Designer.cs | 2 +- FoxTunes.DB.SqlServer/Resources/Database.sql | 8 + FoxTunes.DB/DatabaseTables.cs | 3 + .../FileNameMetaDataSourceFactory.cs | 3 +- .../Managers/DocumentManager.cs | 8 +- .../TagLibMetaDataSourceFactory.cs | 3 +- FoxTunes.MetaData/MetaDataDecorator.cs | 91 +++++++ FoxTunes.MetaData/MetaDataDecoratorFactory.cs | 48 ++++ FoxTunes.MetaData/MetaDataSourceFactory.cs | 17 +- FoxTunes.MetaData/MetaDataSourceWrapper.cs | 47 ++++ .../Providers/ScriptMetaDataProvider.cs | 162 +++++++++++++ .../ViewModel/LibraryBrowser.cs | 4 + .../MetaDataProvidersSettingsDialog.xaml | 121 ++++++++++ .../MetaDataProvidersSettingsDialog.xaml.cs | 15 ++ .../PlaylistSettingsDialog.xaml | 1 + .../Properties/Strings.Designer.cs | 144 +++++++++++ FoxTunes.UI.Windows/Properties/Strings.resx | 48 ++++ FoxTunes.UI.Windows/SettingsWindow.xaml | 3 + .../ViewModel/MetaDataProvidersSettings.cs | 227 ++++++++++++++++++ .../ViewModel/MetaDataSelector.cs | 31 ++- .../ViewModel/PlaylistSettings.cs | 4 +- .../ViewModel/StringResources.cs | 80 ++++++ FoxTunes.UI.Windows/WindowsUserInterface.cs | 16 +- 50 files changed, 1561 insertions(+), 60 deletions(-) rename FoxTunes.Core/Interfaces/{Database => Factories}/IDatabaseFactory.cs (100%) create mode 100644 FoxTunes.Core/Interfaces/Factories/IMetaDataDecoratorFactory.cs create mode 100644 FoxTunes.Core/Interfaces/Managers/IMetaDataProviderManager.cs create mode 100644 FoxTunes.Core/Interfaces/MetaData/IMetaDataDecorator.cs create mode 100644 FoxTunes.Core/Interfaces/MetaData/IMetaDataProvider.cs create mode 100644 FoxTunes.Core/Interfaces/MetaData/IMetaDataProviderCache.cs create mode 100644 FoxTunes.Core/Managers/MetaDataProviderManager.cs create mode 100644 FoxTunes.Core/MetaData/MetaDataProvider.cs create mode 100644 FoxTunes.Core/MetaData/MetaDataProviderCache.cs create mode 100644 FoxTunes.MetaData/MetaDataDecorator.cs create mode 100644 FoxTunes.MetaData/MetaDataDecoratorFactory.cs create mode 100644 FoxTunes.MetaData/MetaDataSourceWrapper.cs create mode 100644 FoxTunes.MetaData/Providers/ScriptMetaDataProvider.cs create mode 100644 FoxTunes.UI.Windows/MetaDataProvidersSettingsDialog.xaml create mode 100644 FoxTunes.UI.Windows/MetaDataProvidersSettingsDialog.xaml.cs create mode 100644 FoxTunes.UI.Windows/ViewModel/MetaDataProvidersSettings.cs diff --git a/FoxTunes.Core/Extensions.cs b/FoxTunes.Core/Extensions.cs index d1856524e..d1d9d5a45 100644 --- a/FoxTunes.Core/Extensions.cs +++ b/FoxTunes.Core/Extensions.cs @@ -529,5 +529,14 @@ public static float Similarity(this string subject, string value, bool ignoreSpa var distance = subject.Distance(value, false); return (1.0f - ((float)distance / (float)Math.Max(subject.Length, value.Length))); } + + public static IList AsList(this IEnumerable sequence) + { + if (sequence is IList list) + { + return list; + } + return sequence.ToList(); + } } } diff --git a/FoxTunes.Core/Interfaces/Database/IDatabaseInitializer.cs b/FoxTunes.Core/Interfaces/Database/IDatabaseInitializer.cs index 5faf32aca..1fb290679 100644 --- a/FoxTunes.Core/Interfaces/Database/IDatabaseInitializer.cs +++ b/FoxTunes.Core/Interfaces/Database/IDatabaseInitializer.cs @@ -15,6 +15,7 @@ public enum DatabaseInitializeType : byte None = 0, Library = 1, Playlist = 2, - All = Library | Playlist + MetaData = 4, + All = Library | Playlist | MetaData } } diff --git a/FoxTunes.Core/Interfaces/Database/IDatabaseTables.cs b/FoxTunes.Core/Interfaces/Database/IDatabaseTables.cs index f72741c32..a2719724c 100644 --- a/FoxTunes.Core/Interfaces/Database/IDatabaseTables.cs +++ b/FoxTunes.Core/Interfaces/Database/IDatabaseTables.cs @@ -19,5 +19,7 @@ public interface IDatabaseTables : IBaseComponent ITableConfig LibraryHierarchyLevel { get; } ITableConfig LibraryHierarchyNode { get; } + + ITableConfig MetaDataProvider { get; } } } diff --git a/FoxTunes.Core/Interfaces/Database/IDatabaseFactory.cs b/FoxTunes.Core/Interfaces/Factories/IDatabaseFactory.cs similarity index 100% rename from FoxTunes.Core/Interfaces/Database/IDatabaseFactory.cs rename to FoxTunes.Core/Interfaces/Factories/IDatabaseFactory.cs diff --git a/FoxTunes.Core/Interfaces/Factories/IMessageSinkFactory.cs b/FoxTunes.Core/Interfaces/Factories/IMessageSinkFactory.cs index 89c1a2d59..85a38bc4e 100644 --- a/FoxTunes.Core/Interfaces/Factories/IMessageSinkFactory.cs +++ b/FoxTunes.Core/Interfaces/Factories/IMessageSinkFactory.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace FoxTunes.Interfaces +namespace FoxTunes.Interfaces { public interface IMessageSinkFactory : IBaseFactory { diff --git a/FoxTunes.Core/Interfaces/Factories/IMetaDataDecoratorFactory.cs b/FoxTunes.Core/Interfaces/Factories/IMetaDataDecoratorFactory.cs new file mode 100644 index 000000000..2b4ec0fcb --- /dev/null +++ b/FoxTunes.Core/Interfaces/Factories/IMetaDataDecoratorFactory.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace FoxTunes.Interfaces +{ + public interface IMetaDataDecoratorFactory : IStandardFactory + { + IEnumerable> Supported { get; } + + bool CanCreate { get; } + + IMetaDataDecorator Create(); + } +} diff --git a/FoxTunes.Core/Interfaces/IStandardComponents.cs b/FoxTunes.Core/Interfaces/IStandardComponents.cs index db15ed965..5e5916c23 100644 --- a/FoxTunes.Core/Interfaces/IStandardComponents.cs +++ b/FoxTunes.Core/Interfaces/IStandardComponents.cs @@ -32,6 +32,8 @@ public interface IStandardComponents IMetaDataCache MetaDataCache { get; } + IMetaDataProviderCache MetaDataProviderCache { get; } + IMetaDataSynchronizer MetaDataSynchronizer { get; } IOnDemandMetaDataProvider OnDemandMetaDataProvider { get; } diff --git a/FoxTunes.Core/Interfaces/IStandardFactories.cs b/FoxTunes.Core/Interfaces/IStandardFactories.cs index f11b4fa11..c13aa7219 100644 --- a/FoxTunes.Core/Interfaces/IStandardFactories.cs +++ b/FoxTunes.Core/Interfaces/IStandardFactories.cs @@ -5,5 +5,7 @@ public interface IStandardFactories IDatabaseFactory Database { get; } IMetaDataSourceFactory MetaDataSource { get; } + + IMetaDataDecoratorFactory MetaDataDecorator { get; } } } diff --git a/FoxTunes.Core/Interfaces/IStandardManagers.cs b/FoxTunes.Core/Interfaces/IStandardManagers.cs index c69c87728..8c04aaafb 100644 --- a/FoxTunes.Core/Interfaces/IStandardManagers.cs +++ b/FoxTunes.Core/Interfaces/IStandardManagers.cs @@ -14,6 +14,8 @@ public interface IStandardManagers IMetaDataManager MetaData { get; } + IMetaDataProviderManager MetaDataProvider { get; } + IFileActionHandlerManager FileActionHandler { get; } } } diff --git a/FoxTunes.Core/Interfaces/Managers/IMetaDataProviderManager.cs b/FoxTunes.Core/Interfaces/Managers/IMetaDataProviderManager.cs new file mode 100644 index 000000000..d125c547c --- /dev/null +++ b/FoxTunes.Core/Interfaces/Managers/IMetaDataProviderManager.cs @@ -0,0 +1,9 @@ +namespace FoxTunes.Interfaces +{ + public interface IMetaDataProviderManager : IStandardManager, IDatabaseInitializer + { + IMetaDataProvider GetProvider(MetaDataProvider metaDataProvider); + + MetaDataProvider[] GetProviders(); + } +} diff --git a/FoxTunes.Core/Interfaces/MetaData/IMetaDataDecorator.cs b/FoxTunes.Core/Interfaces/MetaData/IMetaDataDecorator.cs new file mode 100644 index 000000000..cc9c4f9bf --- /dev/null +++ b/FoxTunes.Core/Interfaces/MetaData/IMetaDataDecorator.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace FoxTunes.Interfaces +{ + public interface IMetaDataDecorator : IBaseComponent + { + IEnumerable GetWarnings(string fileName); + + void Decorate(string fileName, IList metaDataItems, ISet names = null); + + void Decorate(IFileAbstraction fileAbstraction, IList metaDataItems, ISet names = null); + } +} diff --git a/FoxTunes.Core/Interfaces/MetaData/IMetaDataProvider.cs b/FoxTunes.Core/Interfaces/MetaData/IMetaDataProvider.cs new file mode 100644 index 000000000..6d14819c8 --- /dev/null +++ b/FoxTunes.Core/Interfaces/MetaData/IMetaDataProvider.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace FoxTunes.Interfaces +{ + public interface IMetaDataProvider : IBaseComponent + { + MetaDataProviderType Type { get; } + + bool AddOrUpdate(string fileName, IList metaDataItems, MetaDataProvider provider); + + bool AddOrUpdate(IFileAbstraction fileAbstraction, IList metaDataItems, MetaDataProvider provider); + } +} diff --git a/FoxTunes.Core/Interfaces/MetaData/IMetaDataProviderCache.cs b/FoxTunes.Core/Interfaces/MetaData/IMetaDataProviderCache.cs new file mode 100644 index 000000000..8dacf0d05 --- /dev/null +++ b/FoxTunes.Core/Interfaces/MetaData/IMetaDataProviderCache.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace FoxTunes.Interfaces +{ + public interface IMetaDataProviderCache : IStandardComponent + { + MetaDataProvider[] GetProviders(Func> factory); + } +} diff --git a/FoxTunes.Core/Library/LibraryHierarchyPopulator.cs b/FoxTunes.Core/Library/LibraryHierarchyPopulator.cs index 3c461b555..d4a1add6a 100644 --- a/FoxTunes.Core/Library/LibraryHierarchyPopulator.cs +++ b/FoxTunes.Core/Library/LibraryHierarchyPopulator.cs @@ -285,12 +285,18 @@ protected IEnumerable GetRoots() protected override void OnDisposing() { - foreach (var context in this.Contexts.Values) + if (this.Contexts != null) { - context.Dispose(); + foreach (var context in this.Contexts.Values) + { + context.Dispose(); + } + this.Contexts.Dispose(); + } + if (this.Writer != null) + { + this.Writer.Dispose(); } - this.Contexts.Dispose(); - this.Writer.Dispose(); base.OnDisposing(); } } diff --git a/FoxTunes.Core/Library/LibraryUpdater.cs b/FoxTunes.Core/Library/LibraryUpdater.cs index 9636f6a09..695dde8ab 100644 --- a/FoxTunes.Core/Library/LibraryUpdater.cs +++ b/FoxTunes.Core/Library/LibraryUpdater.cs @@ -135,6 +135,10 @@ protected override void OnElapsed(object sender, ElapsedEventArgs e) { lock (SyncRoot) { + if (this.Timer == null) + { + return; + } switch (this.Timer.Interval) { case NORMAL_INTERVAL: diff --git a/FoxTunes.Core/Managers/MetaDataProviderManager.cs b/FoxTunes.Core/Managers/MetaDataProviderManager.cs new file mode 100644 index 000000000..63077411f --- /dev/null +++ b/FoxTunes.Core/Managers/MetaDataProviderManager.cs @@ -0,0 +1,95 @@ +using FoxDb; +using FoxTunes.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace FoxTunes +{ + [ComponentDependency(Slot = ComponentSlots.Database)] + public class MetaDataProviderManager : StandardManager, IMetaDataProviderManager + { + public MetaDataProviderManager() + { + this._Providers = new Lazy>( + () => ComponentRegistry.Instance.GetComponents().ToDictionary( + component => component.Type + ) + ); + } + + public Lazy> _Providers { get; private set; } + + public IMetaDataProviderCache MetaDataProviderCache { get; private set; } + + public IDatabaseFactory DatabaseFactory { get; private set; } + + public override void InitializeComponent(ICore core) + { + this.MetaDataProviderCache = core.Components.MetaDataProviderCache; + this.DatabaseFactory = core.Factories.Database; + base.InitializeComponent(core); + } + + public IMetaDataProvider GetProvider(MetaDataProvider metaDataProvider) + { + var provider = default(IMetaDataProvider); + if (!this._Providers.Value.TryGetValue(metaDataProvider.Type, out provider)) + { + return null; + } + return provider; + } + + public MetaDataProvider[] GetProviders() + { + return this.MetaDataProviderCache.GetProviders(this.GetProvidersCore); + } + + public IEnumerable GetProvidersCore() + { + using (var database = this.DatabaseFactory.Create()) + { + using (var transaction = database.BeginTransaction(database.PreferredIsolationLevel)) + { + var set = database.Set(transaction); + //It's easier to just filter enabled/disabled in memory, there isn't much data. + //set.Fetch.Filter.AddColumn( + // set.Table.GetColumn(ColumnConfig.By("Enabled", ColumnFlags.None)) + //).With(filter => filter.Right = filter.CreateConstant(1)); + foreach (var element in set) + { + yield return element; + } + } + } + } + + public string Checksum + { + get + { + return "6E3C885D-65D6-4A69-9991-CEC5156121A5"; + } + } + + public void InitializeDatabase(IDatabaseComponent database, DatabaseInitializeType type) + { + //IMPORTANT: When editing this function remember to change the checksum. + if (!type.HasFlag(DatabaseInitializeType.MetaData)) + { + return; + } + using (var transaction = database.BeginTransaction(database.PreferredIsolationLevel)) + { + var set = database.Set(transaction); + set.Clear(); + //No default data, yet. + if (transaction.HasTransaction) + { + transaction.Commit(); + } + } + } + } +} diff --git a/FoxTunes.Core/MetaData/MetaDataItem.cs b/FoxTunes.Core/MetaData/MetaDataItem.cs index e28cbe7c1..d106a3081 100644 --- a/FoxTunes.Core/MetaData/MetaDataItem.cs +++ b/FoxTunes.Core/MetaData/MetaDataItem.cs @@ -196,6 +196,7 @@ public enum MetaDataItemType : byte Image = 4, Statistic = 8, Document = 16, - All = Tag | Property | Image | Statistic | Document + CustomTag = 32, + All = Tag | Property | Image | Statistic | Document | CustomTag } } diff --git a/FoxTunes.Core/MetaData/MetaDataProvider.cs b/FoxTunes.Core/MetaData/MetaDataProvider.cs new file mode 100644 index 000000000..abaf3b094 --- /dev/null +++ b/FoxTunes.Core/MetaData/MetaDataProvider.cs @@ -0,0 +1,122 @@ +using System; + +namespace FoxTunes +{ + public class MetaDataProvider : PersistableComponent + { + public MetaDataProvider() + { + + } + + private string _Name { get; set; } + + public string Name + { + get + { + return this._Name; + } + set + { + this._Name = value; + this.OnNameChanged(); + } + } + + protected virtual void OnNameChanged() + { + if (this.NameChanged != null) + { + this.NameChanged(this, EventArgs.Empty); + } + this.OnPropertyChanged("Name"); + } + + public event EventHandler NameChanged; + + private MetaDataProviderType _Type { get; set; } + + public MetaDataProviderType Type + { + get + { + return this._Type; + } + set + { + this._Type = value; + this.OnTypeChanged(); + } + } + + protected virtual void OnTypeChanged() + { + if (this.TypeChanged != null) + { + this.TypeChanged(this, EventArgs.Empty); + } + this.OnPropertyChanged("Type"); + } + + public event EventHandler TypeChanged; + + private string _Script { get; set; } + + public string Script + { + get + { + return this._Script; + } + set + { + this._Script = value; + this.OnScriptChanged(); + } + } + + protected virtual void OnScriptChanged() + { + if (this.ScriptChanged != null) + { + this.ScriptChanged(this, EventArgs.Empty); + } + this.OnPropertyChanged("Script"); + } + + public event EventHandler ScriptChanged; + + private bool _Enabled { get; set; } + + public bool Enabled + { + get + { + return this._Enabled; + } + set + { + this._Enabled = value; + this.OnEnabledChanged(); + } + } + + protected virtual void OnEnabledChanged() + { + if (this.EnabledChanged != null) + { + this.EnabledChanged(this, EventArgs.Empty); + } + this.OnPropertyChanged("Enabled"); + } + + public event EventHandler EnabledChanged; + } + + public enum MetaDataProviderType : byte + { + None = 0, + Script = 1 + } +} diff --git a/FoxTunes.Core/MetaData/MetaDataProviderCache.cs b/FoxTunes.Core/MetaData/MetaDataProviderCache.cs new file mode 100644 index 000000000..c1b828aa1 --- /dev/null +++ b/FoxTunes.Core/MetaData/MetaDataProviderCache.cs @@ -0,0 +1,105 @@ +using FoxTunes.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace FoxTunes +{ + [ComponentDependency(Slot = ComponentSlots.Database)] + public class MetaDataProviderCache : StandardComponent, IMetaDataProviderCache, IDisposable + { + public Lazy Providers { get; private set; } + + public ISignalEmitter SignalEmitter { get; private set; } + + public MetaDataProviderCache() + { + this.Reset(); + } + + public override void InitializeComponent(ICore core) + { + this.SignalEmitter = core.Components.SignalEmitter; + this.SignalEmitter.Signal += this.OnSignal; + base.InitializeComponent(core); + } + + protected virtual Task OnSignal(object sender, ISignal signal) + { + switch (signal.Name) + { + case CommonSignals.MetaDataProvidersUpdated: + var providers = signal.State as IEnumerable; + if (providers != null && providers.Any()) + { + //Nothing to do for indivudual column change. + } + else + { + Logger.Write(this, LogLevel.Debug, "Providers were updated, resetting cache."); + this.Providers = null; + } + break; + } +#if NET40 + return TaskEx.FromResult(false); +#else + return Task.CompletedTask; +#endif + } + + public MetaDataProvider[] GetProviders(Func> factory) + { + if (this.Providers == null) + { + this.Providers = new Lazy(() => factory().ToArray()); + } + return this.Providers.Value; + } + + public void Reset() + { + this.Providers = null; + } + + public bool IsDisposed { get; private set; } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (this.IsDisposed || !disposing) + { + return; + } + this.OnDisposing(); + this.IsDisposed = true; + } + + protected virtual void OnDisposing() + { + if (this.SignalEmitter != null) + { + this.SignalEmitter.Signal -= this.OnSignal; + } + } + + ~MetaDataProviderCache() + { + Logger.Write(this, LogLevel.Error, "Component was not disposed: {0}", this.GetType().Name); + try + { + this.Dispose(true); + } + catch + { + //Nothing can be done, never throw on GC thread. + } + } + } +} diff --git a/FoxTunes.Core/MetaData/MetaDataRefreshBehaviour.cs b/FoxTunes.Core/MetaData/MetaDataRefreshBehaviour.cs index b509b7a76..646bf52c5 100644 --- a/FoxTunes.Core/MetaData/MetaDataRefreshBehaviour.cs +++ b/FoxTunes.Core/MetaData/MetaDataRefreshBehaviour.cs @@ -22,6 +22,8 @@ public MetaDataRefreshBehaviour() public ILibraryManager LibraryManager { get; private set; } + public ISignalEmitter SignalEmitter { get; private set; } + public IConfiguration Configuration { get; private set; } public override void InitializeComponent(ICore core) @@ -29,6 +31,7 @@ public override void InitializeComponent(ICore core) this.LibraryHierarchyCache = core.Components.LibraryHierarchyCache; this.UserInterface = core.Components.UserInterface; this.LibraryManager = core.Managers.Library; + this.SignalEmitter = core.Components.SignalEmitter; this.Configuration = core.Components.Configuration; this.Monitor(); this.Enable(); @@ -37,12 +40,17 @@ public override void InitializeComponent(ICore core) public void Enable() { + this.SignalEmitter.Signal += this.OnSignal; this.Configuration.Saved += this.OnSaved; Logger.Write(this, LogLevel.Info, "Enabled."); } public void Disable() { + if (this.SignalEmitter != null) + { + this.SignalEmitter.Signal -= this.OnSignal; + } if (this.Configuration != null) { this.Configuration.Saved -= this.OnSaved; @@ -80,6 +88,20 @@ public void Monitor(ConfigurationElement element) this.Elements[element] = element.GetPersistentValue(); } + protected virtual Task OnSignal(object sender, ISignal signal) + { + switch (signal.Name) + { + case CommonSignals.MetaDataProvidersUpdated: + return this.Refresh(); + } +#if NET40 + return TaskEx.FromResult(false); +#else + return Task.CompletedTask; +#endif + } + protected virtual void OnSaved(object sender, EventArgs e) { if (!this.LibraryHierarchyCache.HasItems) @@ -102,7 +124,7 @@ protected virtual void OnSaved(object sender, EventArgs e) return; } Logger.Write(this, LogLevel.Info, "Configuration was changed, updating meta data."); - this.Refresh(); + var task = this.Refresh(); } public Task Refresh() diff --git a/FoxTunes.Core/Playlist/PlaylistBrowser.cs b/FoxTunes.Core/Playlist/PlaylistBrowser.cs index 3c63b2927..6514f591e 100644 --- a/FoxTunes.Core/Playlist/PlaylistBrowser.cs +++ b/FoxTunes.Core/Playlist/PlaylistBrowser.cs @@ -35,8 +35,6 @@ protected virtual void OnStateChanged() public event EventHandler StateChanged; - public ICore Core { get; private set; } - public IPlaylistManager PlaylistManager { get; private set; } public IPlaylistCache PlaylistCache { get; private set; } @@ -51,7 +49,6 @@ protected virtual void OnStateChanged() public override void InitializeComponent(ICore core) { - this.Core = core; this.PlaylistManager = core.Managers.Playlist; this.PlaylistCache = core.Components.PlaylistCache; this.DatabaseFactory = core.Factories.Database; diff --git a/FoxTunes.Core/Scripting/ScriptRunner.cs b/FoxTunes.Core/Scripting/ScriptRunner.cs index 803cb6270..a98c48fe6 100644 --- a/FoxTunes.Core/Scripting/ScriptRunner.cs +++ b/FoxTunes.Core/Scripting/ScriptRunner.cs @@ -22,11 +22,15 @@ protected ScriptRunner(IScriptingContext scriptingContext, T item, string script public virtual void Prepare() { - var collections = new Dictionary>() + var tags = new Dictionary(StringComparer.OrdinalIgnoreCase); + var properties = new Dictionary(StringComparer.OrdinalIgnoreCase); + var documents = new Dictionary(StringComparer.OrdinalIgnoreCase); + var collections = new Dictionary>() { - { MetaDataItemType.Tag, new Dictionary(StringComparer.OrdinalIgnoreCase) }, - { MetaDataItemType.Property, new Dictionary(StringComparer.OrdinalIgnoreCase) }, - { MetaDataItemType.Document, new Dictionary(StringComparer.OrdinalIgnoreCase) } + { MetaDataItemType.Tag, tags }, + { MetaDataItemType.Property, properties }, + { MetaDataItemType.Document, documents }, + { MetaDataItemType.CustomTag, tags }, }; var fileName = default(string); var directoryName = default(string); @@ -40,25 +44,46 @@ public virtual void Prepare() { continue; } - var key = item.Name.ToLower(); - if (collections[item.Type].ContainsKey(key)) + var key = this.GetKey(item); + var collection = collections[item.Type]; + if (collection.ContainsKey(key)) { //Not sure what to do. Doesn't happen often. continue; } - collections[item.Type].Add(key, this.GetValue(item)); + collection.Add(key, this.GetValue(item)); } } fileName = this.Item.FileName; directoryName = this.Item.DirectoryName; } - this.ScriptingContext.SetValue("tag", collections[MetaDataItemType.Tag]); - this.ScriptingContext.SetValue("property", collections[MetaDataItemType.Property]); - this.ScriptingContext.SetValue("document", collections[MetaDataItemType.Document]); + this.ScriptingContext.SetValue("tag", tags); + this.ScriptingContext.SetValue("property", properties); + this.ScriptingContext.SetValue("document", documents); this.ScriptingContext.SetValue("file", fileName); this.ScriptingContext.SetValue("folder", directoryName); } + public string GetKey(MetaDataItem item) + { + var name = item.Name; + switch (item.Type) + { + case MetaDataItemType.Document: + var parts = name.Split(new[] { ':' }, 2); + if (parts.Length != 2) + { + //Expected MimeType:Data. + } + else + { + name = parts[1]; + } + break; + } + return name.ToLower(); + } + public object GetValue(MetaDataItem item) { if (string.IsNullOrEmpty(item.Value)) diff --git a/FoxTunes.Core/Signal/CommonSignals.cs b/FoxTunes.Core/Signal/CommonSignals.cs index 6551787ef..f62ecd131 100644 --- a/FoxTunes.Core/Signal/CommonSignals.cs +++ b/FoxTunes.Core/Signal/CommonSignals.cs @@ -12,6 +12,8 @@ public static class CommonSignals public const string MetaDataUpdated = "MetaDataUpdated"; + public const string MetaDataProvidersUpdated = "MetaDataProvidersUpdated"; + public const string SettingsUpdated = "SettingsUpdated"; public const string ImagesUpdated = "ImagesUpdated"; diff --git a/FoxTunes.Core/StandardComponents.cs b/FoxTunes.Core/StandardComponents.cs index 50a9f275f..1e829d5ba 100644 --- a/FoxTunes.Core/StandardComponents.cs +++ b/FoxTunes.Core/StandardComponents.cs @@ -129,6 +129,14 @@ public IMetaDataCache MetaDataCache } } + public IMetaDataProviderCache MetaDataProviderCache + { + get + { + return ComponentRegistry.Instance.GetComponent(); + } + } + public IMetaDataSynchronizer MetaDataSynchronizer { get diff --git a/FoxTunes.Core/StandardFactories.cs b/FoxTunes.Core/StandardFactories.cs index a0dd0ea3b..1a9e0505f 100644 --- a/FoxTunes.Core/StandardFactories.cs +++ b/FoxTunes.Core/StandardFactories.cs @@ -20,6 +20,14 @@ public IMetaDataSourceFactory MetaDataSource } } + public IMetaDataDecoratorFactory MetaDataDecorator + { + get + { + return ComponentRegistry.Instance.GetComponent(); + } + } + public static readonly IStandardFactories Instance = new StandardFactories(); } } diff --git a/FoxTunes.Core/StandardManagers.cs b/FoxTunes.Core/StandardManagers.cs index b1c56c0a3..6a5cb6998 100644 --- a/FoxTunes.Core/StandardManagers.cs +++ b/FoxTunes.Core/StandardManagers.cs @@ -57,6 +57,14 @@ public IMetaDataManager MetaData } } + public IMetaDataProviderManager MetaDataProvider + { + get + { + return ComponentRegistry.Instance.GetComponent(); + } + } + public IFileActionHandlerManager FileActionHandler { get diff --git a/FoxTunes.DB.SQLite/Resources/Database.sql b/FoxTunes.DB.SQLite/Resources/Database.sql index 330f606ef..a434d7d78 100644 --- a/FoxTunes.DB.SQLite/Resources/Database.sql +++ b/FoxTunes.DB.SQLite/Resources/Database.sql @@ -85,6 +85,14 @@ CREATE TABLE [LibraryHierarchyItem_LibraryItem] ( [LibraryItem_Id] INTEGER NOT NULL ); +CREATE TABLE [MetaDataProviders] ( + [Id] INTEGER PRIMARY KEY NOT NULL, + [Name] TEXT NOT NULL COLLATE NOCASE, + [Type] bigint NOT NULL, + [Script] text NULL COLLATE NOCASE, + [Enabled] bit NOT NULL +); + CREATE INDEX [IDX_LibraryHierarchies_1] ON [LibraryHierarchies]( [Enabled] ); diff --git a/FoxTunes.DB.SqlServer/Resources.Designer.cs b/FoxTunes.DB.SqlServer/Resources.Designer.cs index a3a178086..8483a78be 100644 --- a/FoxTunes.DB.SqlServer/Resources.Designer.cs +++ b/FoxTunes.DB.SqlServer/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace FoxTunes { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { diff --git a/FoxTunes.DB.SqlServer/Resources/Database.sql b/FoxTunes.DB.SqlServer/Resources/Database.sql index 78cf3b1ef..937c55ebc 100644 --- a/FoxTunes.DB.SqlServer/Resources/Database.sql +++ b/FoxTunes.DB.SqlServer/Resources/Database.sql @@ -84,4 +84,12 @@ CREATE TABLE [LibraryHierarchyItem_LibraryItem] ( [Id] INTEGER IDENTITY(1,1) PRIMARY KEY NOT NULL, [LibraryHierarchyItem_Id] INTEGER NOT NULL REFERENCES LibraryHierarchyItems([Id]), [LibraryItem_Id] INTEGER NOT NULL REFERENCES LibraryItems([Id]) +); + +CREATE TABLE [MetaDataProviders] ( + [Id] INTEGER IDENTITY(1,1) PRIMARY KEY NOT NULL, + [Name] nvarchar(4000) NOT NULL, + [Type] bigint NOT NULL, + [Script] nvarchar(max) NULL, + [Enabled] bit NOT NULL ); \ No newline at end of file diff --git a/FoxTunes.DB/DatabaseTables.cs b/FoxTunes.DB/DatabaseTables.cs index 361997dc4..b694e5aa1 100644 --- a/FoxTunes.DB/DatabaseTables.cs +++ b/FoxTunes.DB/DatabaseTables.cs @@ -30,6 +30,8 @@ public DatabaseTables(IDatabaseComponent database) public ITableConfig LibraryHierarchyNode { get; private set; } + public ITableConfig MetaDataProvider { get; private set; } + public override void InitializeComponent(ICore core) { this.MetaDataItem = this.Database.Config.Table(); @@ -40,6 +42,7 @@ public override void InitializeComponent(ICore core) this.LibraryHierarchy = this.Database.Config.Table(); this.LibraryHierarchyLevel = this.Database.Config.Table(); this.LibraryHierarchyNode = this.Database.Config.Table(); + this.MetaDataProvider = this.Database.Config.Table(); base.InitializeComponent(core); } } diff --git a/FoxTunes.MetaData.FileName/FileNameMetaDataSourceFactory.cs b/FoxTunes.MetaData.FileName/FileNameMetaDataSourceFactory.cs index 7045e5970..d94d2781f 100644 --- a/FoxTunes.MetaData.FileName/FileNameMetaDataSourceFactory.cs +++ b/FoxTunes.MetaData.FileName/FileNameMetaDataSourceFactory.cs @@ -1,7 +1,6 @@ using FoxTunes.Interfaces; using System; using System.Collections.Generic; -using System.Linq; namespace FoxTunes { @@ -53,7 +52,7 @@ public override IEnumerable> Supported } } - public override IMetaDataSource Create() + public override IMetaDataSource OnCreate() { var source = new FileNameMetaDataSource(this.Extractors); source.InitializeComponent(this.Core); diff --git a/FoxTunes.MetaData.TagLib/Managers/DocumentManager.cs b/FoxTunes.MetaData.TagLib/Managers/DocumentManager.cs index 0c487695e..dfcf81174 100644 --- a/FoxTunes.MetaData.TagLib/Managers/DocumentManager.cs +++ b/FoxTunes.MetaData.TagLib/Managers/DocumentManager.cs @@ -9,6 +9,8 @@ namespace FoxTunes { public static class DocumentManager { + const string PREFIX = "document"; + const string MIME_TYPE_JSON = "application/json"; static readonly Regex Base64 = new Regex("^([a-z0-9+/]{4})*([a-z0-9+/]{3}=|[a-z0-9+/]{2}==)?$", RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -43,9 +45,11 @@ public static void Read(TagLibMetaDataSource source, IList metaDat { if (string.Equals(picture.MimeType, MIME_TYPE_JSON, StringComparison.OrdinalIgnoreCase)) { - metaDatas.Add(new MetaDataItem(picture.Description, MetaDataItemType.Document) + var name = string.Concat(PREFIX, ":", picture.Description); + var value = string.Concat(MIME_TYPE_JSON, ":", ReadJsonDocument(picture.Data.Data)); + metaDatas.Add(new MetaDataItem(name, MetaDataItemType.Document) { - Value = string.Concat(MIME_TYPE_JSON, ":", ReadJsonDocument(picture.Data.Data)) + Value = value }); } } diff --git a/FoxTunes.MetaData.TagLib/TagLibMetaDataSourceFactory.cs b/FoxTunes.MetaData.TagLib/TagLibMetaDataSourceFactory.cs index fe1b29c3c..2bd4a0144 100644 --- a/FoxTunes.MetaData.TagLib/TagLibMetaDataSourceFactory.cs +++ b/FoxTunes.MetaData.TagLib/TagLibMetaDataSourceFactory.cs @@ -6,7 +6,6 @@ namespace FoxTunes [Component("679D9459-BBCE-4D95-BB65-DD20C335719C", ComponentSlots.MetaData, @default: true)] public class TagLibMetaDataSourceFactory : MetaDataSourceFactory { - public BooleanConfigurationElement Extended { get; private set; } public BooleanConfigurationElement MusicBrainz { get; private set; } @@ -111,7 +110,7 @@ public override IEnumerable> Supported } } - public override IMetaDataSource Create() + public override IMetaDataSource OnCreate() { var source = new TagLibMetaDataSource(); source.InitializeComponent(this.Core); diff --git a/FoxTunes.MetaData/MetaDataDecorator.cs b/FoxTunes.MetaData/MetaDataDecorator.cs new file mode 100644 index 000000000..914849507 --- /dev/null +++ b/FoxTunes.MetaData/MetaDataDecorator.cs @@ -0,0 +1,91 @@ +using FoxTunes.Interfaces; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace FoxTunes +{ + public class MetaDataDecorator : BaseComponent, IMetaDataDecorator + { + public MetaDataDecorator() + { + this.Warnings = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); + } + + public ConcurrentDictionary> Warnings { get; private set; } + + public IEnumerable GetWarnings(string fileName) + { + var warnings = default(IList); + if (!this.Warnings.TryGetValue(fileName, out warnings)) + { + return Enumerable.Empty(); + } + return warnings; + } + + public void AddWarning(string fileName, string warning) + { + this.Warnings.GetOrAdd(fileName, key => new List()).Add(warning); + } + + public IMetaDataProviderManager MetaDataProviderManager { get; private set; } + + public override void InitializeComponent(ICore core) + { + this.MetaDataProviderManager = core.Managers.MetaDataProvider; + base.InitializeComponent(core); + } + + public void Decorate(string fileName, IList metaDataItems, ISet names = null) + { + var providers = this.MetaDataProviderManager.GetProviders(); + foreach (var provider in providers) + { + try + { + var factory = this.MetaDataProviderManager.GetProvider(provider); + if (factory == null) + { + //No such IMetaDataProvider implementation for MetaDataProviderType. + continue; + } + if (factory.AddOrUpdate(fileName, metaDataItems, provider) && names != null) + { + names.Add(provider.Name); + } + } + catch (Exception e) + { + this.AddWarning(fileName, e.Message); + } + } + } + + public void Decorate(IFileAbstraction fileAbstraction, IList metaDataItems, ISet names = null) + { + var providers = this.MetaDataProviderManager.GetProviders(); + foreach (var provider in providers) + { + try + { + var factory = this.MetaDataProviderManager.GetProvider(provider); + if (factory == null) + { + //No such IMetaDataProvider implementation for MetaDataProviderType. + continue; + } + if (factory.AddOrUpdate(fileAbstraction, metaDataItems, provider) && names != null) + { + names.Add(provider.Name); + } + } + catch (Exception e) + { + this.AddWarning(fileAbstraction.FileName, e.Message); + } + } + } + } +} diff --git a/FoxTunes.MetaData/MetaDataDecoratorFactory.cs b/FoxTunes.MetaData/MetaDataDecoratorFactory.cs new file mode 100644 index 000000000..2012aca00 --- /dev/null +++ b/FoxTunes.MetaData/MetaDataDecoratorFactory.cs @@ -0,0 +1,48 @@ +using FoxTunes.Interfaces; +using System.Collections.Generic; +using System.Linq; + +namespace FoxTunes +{ + public class MetaDataDecoratorFactory : StandardFactory, IMetaDataDecoratorFactory + { + public ICore Core { get; private set; } + + public IMetaDataProviderManager MetaDataProviderManager { get; private set; } + + public override void InitializeComponent(ICore core) + { + this.Core = core; + this.MetaDataProviderManager = core.Managers.MetaDataProvider; + base.InitializeComponent(core); + } + + public IEnumerable> Supported + { + get + { + var providers = this.MetaDataProviderManager.GetProviders(); + foreach (var provider in providers) + { + yield return new KeyValuePair(provider.Name, MetaDataItemType.CustomTag); + } + } + } + + public bool CanCreate + { + get + { + var providers = this.MetaDataProviderManager.GetProviders(); + return providers.Any(); + } + } + + public IMetaDataDecorator Create() + { + var decorator = new MetaDataDecorator(); + decorator.InitializeComponent(this.Core); + return decorator; + } + } +} diff --git a/FoxTunes.MetaData/MetaDataSourceFactory.cs b/FoxTunes.MetaData/MetaDataSourceFactory.cs index d2f67a8f6..468d3d966 100644 --- a/FoxTunes.MetaData/MetaDataSourceFactory.cs +++ b/FoxTunes.MetaData/MetaDataSourceFactory.cs @@ -7,6 +7,8 @@ public abstract class MetaDataSourceFactory : StandardFactory, IMetaDataSourceFa { public ICore Core { get; private set; } + public IMetaDataDecoratorFactory MetaDataDecoratorFactory { get; private set; } + public IConfiguration Configuration { get; private set; } public abstract IEnumerable> Supported { get; } @@ -14,10 +16,23 @@ public abstract class MetaDataSourceFactory : StandardFactory, IMetaDataSourceFa public override void InitializeComponent(ICore core) { this.Core = core; + this.MetaDataDecoratorFactory = core.Factories.MetaDataDecorator; this.Configuration = core.Components.Configuration; base.InitializeComponent(core); } - public abstract IMetaDataSource Create(); + public IMetaDataSource Create() + { + var metaDataSource = this.OnCreate(); + if (this.MetaDataDecoratorFactory.CanCreate) + { + var metaDataDecorator = this.MetaDataDecoratorFactory.Create(); + metaDataSource = new MetaDataSourceWrapper(metaDataSource, metaDataDecorator); + metaDataSource.InitializeComponent(this.Core); + } + return metaDataSource; + } + + public abstract IMetaDataSource OnCreate(); } } diff --git a/FoxTunes.MetaData/MetaDataSourceWrapper.cs b/FoxTunes.MetaData/MetaDataSourceWrapper.cs new file mode 100644 index 000000000..5368ba66c --- /dev/null +++ b/FoxTunes.MetaData/MetaDataSourceWrapper.cs @@ -0,0 +1,47 @@ +using FoxTunes.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace FoxTunes +{ + public class MetaDataSourceWrapper : BaseComponent, IMetaDataSource + { + public MetaDataSourceWrapper(IMetaDataSource metaDataSource, IMetaDataDecorator metaDataDecorator) + { + this.MetaDataSource = metaDataSource; + this.MetaDataDecorator = metaDataDecorator; + } + + public IMetaDataSource MetaDataSource { get; private set; } + + public IMetaDataDecorator MetaDataDecorator { get; private set; } + + public async Task> GetMetaData(string fileName) + { + var metaDataItems = (await this.MetaDataSource.GetMetaData(fileName).ConfigureAwait(false)).AsList(); + this.MetaDataDecorator.Decorate(fileName, metaDataItems); + return metaDataItems; + } + + public async Task> GetMetaData(IFileAbstraction fileAbstraction) + { + var metaDataItems = (await this.MetaDataSource.GetMetaData(fileAbstraction).ConfigureAwait(false)).AsList(); + this.MetaDataDecorator.Decorate(fileAbstraction, metaDataItems); + return metaDataItems; + } + + public IEnumerable GetWarnings(string fileName) + { + return this.MetaDataSource.GetWarnings(fileName) + .Concat(this.MetaDataDecorator.GetWarnings(fileName)) + .ToArray(); + } + + public Task SetMetaData(string fileName, IEnumerable metaDataItems, Func predicate) + { + return this.MetaDataSource.SetMetaData(fileName, metaDataItems, predicate); + } + } +} diff --git a/FoxTunes.MetaData/Providers/ScriptMetaDataProvider.cs b/FoxTunes.MetaData/Providers/ScriptMetaDataProvider.cs new file mode 100644 index 000000000..3dd7c988d --- /dev/null +++ b/FoxTunes.MetaData/Providers/ScriptMetaDataProvider.cs @@ -0,0 +1,162 @@ +using FoxTunes.Interfaces; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; + +namespace FoxTunes +{ + public class ScriptMetaDataProvider : StandardComponent, IMetaDataProvider, IDisposable + { + public IScriptingRuntime ScriptingRuntime { get; private set; } + +#if NET40 + private TrackingThreadLocal Contexts { get; set; } +#else + private ThreadLocal Contexts { get; set; } +#endif + + public override void InitializeComponent(ICore core) + { + this.ScriptingRuntime = core.Components.ScriptingRuntime; +#if NET40 + this.Contexts = new TrackingThreadLocal(); +#else + this.Contexts = new ThreadLocal(true); +#endif + base.InitializeComponent(core); + } + + public MetaDataProviderType Type + { + get + { + return MetaDataProviderType.Script; + } + } + + public bool AddOrUpdate(string fileName, IList metaDataItems, MetaDataProvider provider) + { + var item = new FileData(fileName, metaDataItems); + var runner = new ScriptRunner( + this.GetOrAddContext(), + item, + provider.Script + ); + runner.Prepare(); + var value = Convert.ToString(runner.Run()); + return this.AddOrUpdate(metaDataItems, provider.Name, value); + } + + public bool AddOrUpdate(IFileAbstraction fileAbstraction, IList metaDataItems, MetaDataProvider provider) + { + var item = new FileData(fileAbstraction.FileName, metaDataItems); + var runner = new ScriptRunner( + this.GetOrAddContext(), + item, + provider.Script + ); + runner.Prepare(); + var value = Convert.ToString(runner.Run()); + return this.AddOrUpdate(metaDataItems, provider.Name, value); + } + + protected virtual bool AddOrUpdate(IList metaDataItems, string name, string value) + { + foreach (var metaDataItem in metaDataItems) + { + if (string.Equals(metaDataItem.Name, name, StringComparison.OrdinalIgnoreCase) && metaDataItem.Type == MetaDataItemType.CustomTag) + { + if (string.Equals(metaDataItem.Value, value)) + { + return false; + } + metaDataItem.Value = value; + return true; + } + } + metaDataItems.Add(new MetaDataItem(name, MetaDataItemType.CustomTag) + { + Value = value + }); + return true; + } + + private IScriptingContext GetOrAddContext() + { + if (this.Contexts.IsValueCreated) + { + return this.Contexts.Value; + } + return this.Contexts.Value = this.ScriptingRuntime.CreateContext(); + } + + public bool IsDisposed { get; private set; } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (this.IsDisposed || !disposing) + { + return; + } + this.OnDisposing(); + this.IsDisposed = true; + } + + protected virtual void OnDisposing() + { + if (this.Contexts != null) + { + foreach (var context in this.Contexts.Values) + { + context.Dispose(); + } + this.Contexts.Dispose(); + } + } + + ~ScriptMetaDataProvider() + { + Logger.Write(this, LogLevel.Error, "Component was not disposed: {0}", this.GetType().Name); + try + { + this.Dispose(true); + } + catch + { + //Nothing can be done, never throw on GC thread. + } + } + + private class FileData : PersistableComponent, IFileData + { + public FileData(string fileName, IList metaDatas) + { + this.DirectoryName = Path.GetDirectoryName(fileName); + this.FileName = fileName; + this.MetaDatas = metaDatas; + } + + public string DirectoryName { get; set; } + + public string FileName { get; set; } + + public IList MetaDatas { get; set; } + + } + + private class ScriptRunner : ScriptRunner + { + public ScriptRunner(IScriptingContext scriptingContext, FileData item, string script) : base(scriptingContext, item, script) + { + } + } + } +} diff --git a/FoxTunes.UI.Windows.LibraryBrowser/ViewModel/LibraryBrowser.cs b/FoxTunes.UI.Windows.LibraryBrowser/ViewModel/LibraryBrowser.cs index e7643ef20..9e4aa9e08 100644 --- a/FoxTunes.UI.Windows.LibraryBrowser/ViewModel/LibraryBrowser.cs +++ b/FoxTunes.UI.Windows.LibraryBrowser/ViewModel/LibraryBrowser.cs @@ -238,6 +238,10 @@ await this.Synchronize(new List() protected virtual bool Validate(LibraryBrowserFrame frame) { + if (frame.Items == null) + { + return false; + } if (object.ReferenceEquals(frame.ItemsSource, LibraryHierarchyNode.Empty)) { //Root frame. diff --git a/FoxTunes.UI.Windows/MetaDataProvidersSettingsDialog.xaml b/FoxTunes.UI.Windows/MetaDataProvidersSettingsDialog.xaml new file mode 100644 index 000000000..26c1b1abc --- /dev/null +++ b/FoxTunes.UI.Windows/MetaDataProvidersSettingsDialog.xaml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FoxTunes.UI.Windows/MetaDataProvidersSettingsDialog.xaml.cs b/FoxTunes.UI.Windows/MetaDataProvidersSettingsDialog.xaml.cs new file mode 100644 index 000000000..25e8d3977 --- /dev/null +++ b/FoxTunes.UI.Windows/MetaDataProvidersSettingsDialog.xaml.cs @@ -0,0 +1,15 @@ +using System.Windows.Controls; + +namespace FoxTunes +{ + /// + /// Interaction logic for MetaDataProvidersSettingsDialog.xaml + /// + public partial class MetaDataProvidersSettingsDialog : UserControl + { + public MetaDataProvidersSettingsDialog() + { + this.InitializeComponent(); + } + } +} diff --git a/FoxTunes.UI.Windows/PlaylistSettingsDialog.xaml b/FoxTunes.UI.Windows/PlaylistSettingsDialog.xaml index 508be7f7e..ccbd92b77 100644 --- a/FoxTunes.UI.Windows/PlaylistSettingsDialog.xaml +++ b/FoxTunes.UI.Windows/PlaylistSettingsDialog.xaml @@ -20,6 +20,7 @@ + diff --git a/FoxTunes.UI.Windows/Properties/Strings.Designer.cs b/FoxTunes.UI.Windows/Properties/Strings.Designer.cs index 59b46fb49..e3971c8c5 100644 --- a/FoxTunes.UI.Windows/Properties/Strings.Designer.cs +++ b/FoxTunes.UI.Windows/Properties/Strings.Designer.cs @@ -573,6 +573,60 @@ internal static string LibraryTree_Name { } } + /// + /// Looks up a localized string similar to Tag (Custom). + /// + internal static string MetaDataItemType_CustomTag { + get { + return ResourceManager.GetString("MetaDataItemType.CustomTag", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Document. + /// + internal static string MetaDataItemType_Document { + get { + return ResourceManager.GetString("MetaDataItemType.Document", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Image. + /// + internal static string MetaDataItemType_Image { + get { + return ResourceManager.GetString("MetaDataItemType.Image", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property. + /// + internal static string MetaDataItemType_Property { + get { + return ResourceManager.GetString("MetaDataItemType.Property", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Statistic. + /// + internal static string MetaDataItemType_Statistic { + get { + return ResourceManager.GetString("MetaDataItemType.Statistic", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tag. + /// + internal static string MetaDataItemType_Tag { + get { + return ResourceManager.GetString("MetaDataItemType.Tag", resourceCulture); + } + } + /// /// Looks up a localized string similar to Compilation. /// @@ -582,6 +636,96 @@ internal static string MetaDataName_IsCompilation { } } + /// + /// Looks up a localized string similar to Cancel. + /// + internal static string MetaDataProvidersSettingsDialog_Cancel { + get { + return ResourceManager.GetString("MetaDataProvidersSettingsDialog.Cancel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Delete. + /// + internal static string MetaDataProvidersSettingsDialog_Delete { + get { + return ResourceManager.GetString("MetaDataProvidersSettingsDialog.Delete", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enabled. + /// + internal static string MetaDataProvidersSettingsDialog_Enabled { + get { + return ResourceManager.GetString("MetaDataProvidersSettingsDialog.Enabled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Meta Data. + /// + internal static string MetaDataProvidersSettingsDialog_GroupHeader { + get { + return ResourceManager.GetString("MetaDataProvidersSettingsDialog.GroupHeader", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Name. + /// + internal static string MetaDataProvidersSettingsDialog_Name { + get { + return ResourceManager.GetString("MetaDataProvidersSettingsDialog.Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to New. + /// + internal static string MetaDataProvidersSettingsDialog_New { + get { + return ResourceManager.GetString("MetaDataProvidersSettingsDialog.New", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reset. + /// + internal static string MetaDataProvidersSettingsDialog_Reset { + get { + return ResourceManager.GetString("MetaDataProvidersSettingsDialog.Reset", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Save. + /// + internal static string MetaDataProvidersSettingsDialog_Save { + get { + return ResourceManager.GetString("MetaDataProvidersSettingsDialog.Save", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Script. + /// + internal static string MetaDataProvidersSettingsDialog_Script { + get { + return ResourceManager.GetString("MetaDataProvidersSettingsDialog.Script", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type. + /// + internal static string MetaDataProvidersSettingsDialog_Type { + get { + return ResourceManager.GetString("MetaDataProvidersSettingsDialog.Type", resourceCulture); + } + } + /// /// Looks up a localized string similar to . /// diff --git a/FoxTunes.UI.Windows/Properties/Strings.resx b/FoxTunes.UI.Windows/Properties/Strings.resx index 70b7c7088..ff7aae2f8 100644 --- a/FoxTunes.UI.Windows/Properties/Strings.resx +++ b/FoxTunes.UI.Windows/Properties/Strings.resx @@ -288,9 +288,57 @@ Library Tree + + Tag (Custom) + + + Document + + + Image + + + Property + + + Statistic + + + Tag + Compilation + + Cancel + + + Delete + + + Enabled + + + Meta Data + + + Name + + + New + + + Reset + + + Save + + + Script + + + Type + diff --git a/FoxTunes.UI.Windows/SettingsWindow.xaml b/FoxTunes.UI.Windows/SettingsWindow.xaml index a379853e0..4b8c94eca 100644 --- a/FoxTunes.UI.Windows/SettingsWindow.xaml +++ b/FoxTunes.UI.Windows/SettingsWindow.xaml @@ -29,5 +29,8 @@ + + + diff --git a/FoxTunes.UI.Windows/ViewModel/MetaDataProvidersSettings.cs b/FoxTunes.UI.Windows/ViewModel/MetaDataProvidersSettings.cs new file mode 100644 index 000000000..454eadc5d --- /dev/null +++ b/FoxTunes.UI.Windows/ViewModel/MetaDataProvidersSettings.cs @@ -0,0 +1,227 @@ +using FoxDb; +using FoxTunes.Interfaces; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; + +namespace FoxTunes.ViewModel +{ + public class MetaDataProvidersSettings : ViewModelBase + { + public IMetaDataProviderManager MetaDataProviderManager { get; private set; } + + public IDatabaseFactory DatabaseFactory { get; private set; } + + public ISignalEmitter SignalEmitter { get; private set; } + + private CollectionManager _MetaDataProviders { get; set; } + + public CollectionManager MetaDataProviders + { + get + { + return this._MetaDataProviders; + } + set + { + this._MetaDataProviders = value; + this.OnMetaDataProvidersChanged(); + } + } + + protected virtual void OnMetaDataProvidersChanged() + { + this.OnPropertyChanged("MetaDataProviders"); + } + + public bool IsSaving + { + get + { + return global::FoxTunes.BackgroundTask.Active + .Any(task => task is PlaylistTaskBase || task is LibraryTaskBase); + } + } + + protected virtual void OnIsSavingChanged() + { + if (this.IsSavingChanged != null) + { + this.IsSavingChanged(this, EventArgs.Empty); + } + this.OnPropertyChanged("IsSaving"); + } + + public event EventHandler IsSavingChanged; + + public ICommand SaveCommand + { + get + { + return CommandFactory.Instance.CreateCommand(this.Save); + } + } + + public async Task Save() + { + var exception = default(Exception); + try + { + using (var database = this.DatabaseFactory.Create()) + { + using (var task = new SingletonReentrantTask(CancellationToken.None, ComponentSlots.Database, SingletonReentrantTask.PRIORITY_HIGH, cancellationToken => + { + using (var transaction = database.BeginTransaction(database.PreferredIsolationLevel)) + { + var metaDataProviders = database.Set(transaction); + metaDataProviders.Remove(metaDataProviders.Except(this.MetaDataProviders.ItemsSource)); + metaDataProviders.AddOrUpdate(this.MetaDataProviders.ItemsSource); + transaction.Commit(); + } +#if NET40 + return TaskEx.FromResult(false); +#else + return Task.CompletedTask; +#endif + })) + { + await task.Run().ConfigureAwait(false); + } + } + await this.SignalEmitter.Send(new Signal(this, CommonSignals.MetaDataProvidersUpdated)).ConfigureAwait(false); + return; + } + catch (Exception e) + { + exception = e; + } + await this.OnError("Save", exception).ConfigureAwait(false); + throw exception; + } + + public ICommand CancelCommand + { + get + { + return new Command(this.Cancel); + } + } + + public void Cancel() + { + this.Dispatch(this.Refresh); + } + + public ICommand ResetCommand + { + get + { + return CommandFactory.Instance.CreateCommand(this.Reset); + } + } + + public async Task Reset() + { + var core = default(ICore); + await Windows.Invoke(() => core = this.Core).ConfigureAwait(false); + using (var database = this.DatabaseFactory.Create()) + { + using (var task = new SingletonReentrantTask(CancellationToken.None, ComponentSlots.Database, SingletonReentrantTask.PRIORITY_HIGH, cancellationToken => + { + core.InitializeDatabase(database, DatabaseInitializeType.MetaData); +#if NET40 + return TaskEx.FromResult(false); +#else + return Task.CompletedTask; +#endif + })) + { + await task.Run().ConfigureAwait(false); + } + } + await this.SignalEmitter.Send(new Signal(this, CommonSignals.MetaDataProvidersUpdated)).ConfigureAwait(false); + } + + public override void InitializeComponent(ICore core) + { + global::FoxTunes.BackgroundTask.ActiveChanged += this.OnActiveChanged; + this.MetaDataProviderManager = core.Managers.MetaDataProvider; + this.DatabaseFactory = this.Core.Factories.Database; + this.SignalEmitter = this.Core.Components.SignalEmitter; + this.SignalEmitter.Signal += this.OnSignal; + this.MetaDataProviders = new CollectionManager(CollectionManagerFlags.AllowEmptyCollection) + { + ItemFactory = () => + { + using (var database = this.DatabaseFactory.Create()) + { + return database.Set().Create().With(metaDataProvider => + { + metaDataProvider.Name = "New"; + metaDataProvider.Type = MetaDataProviderType.Script; + metaDataProvider.Script = "'New'"; + metaDataProvider.Enabled = true; + }); + } + } + }; + this.Dispatch(this.Refresh); + base.InitializeComponent(core); + } + + protected virtual async void OnActiveChanged(object sender, EventArgs e) + { + await Windows.Invoke(() => this.OnIsSavingChanged()).ConfigureAwait(false); + } + + protected virtual Task OnSignal(object sender, ISignal signal) + { + switch (signal.Name) + { + case CommonSignals.SettingsUpdated: + return this.Refresh(); + case CommonSignals.MetaDataProvidersUpdated: + var metaDataProviders = signal.State as IEnumerable; + if (metaDataProviders != null && metaDataProviders.Any()) + { + this.MetaDataProviders.Refresh(); + } + else + { + return this.Refresh(); + } + break; + } +#if NET40 + return TaskEx.FromResult(false); +#else + return Task.CompletedTask; +#endif + } + + protected virtual Task Refresh() + { + return Windows.Invoke(() => + { + this.MetaDataProviders.ItemsSource = new ObservableCollection( + this.MetaDataProviderManager.GetProviders() + ); + }); + } + + protected override void OnDisposing() + { + global::FoxTunes.BackgroundTask.ActiveChanged -= this.OnActiveChanged; + base.OnDisposing(); + } + + protected override Freezable CreateInstanceCore() + { + return new MetaDataProvidersSettings(); + } + } +} diff --git a/FoxTunes.UI.Windows/ViewModel/MetaDataSelector.cs b/FoxTunes.UI.Windows/ViewModel/MetaDataSelector.cs index bf61c5e0a..a9decc7c0 100644 --- a/FoxTunes.UI.Windows/ViewModel/MetaDataSelector.cs +++ b/FoxTunes.UI.Windows/ViewModel/MetaDataSelector.cs @@ -1,35 +1,30 @@ using FoxTunes.Interfaces; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Windows.Data; namespace FoxTunes.ViewModel { public static class MetaDataSelector { - public static readonly IMetaDataSourceFactory Factory = ComponentRegistry.Instance.GetComponent(); + public static readonly IMetaDataSourceFactory SourceFactory = ComponentRegistry.Instance.GetComponent(); - public static readonly IEnumerable Elements = GetElements(); + public static readonly IMetaDataDecoratorFactory DecoratorFactory = ComponentRegistry.Instance.GetComponent(); - private static IEnumerable GetElements() - { - var supported = Factory.Supported.ToArray(); - var elements = Enumerable.Empty(); - elements = elements.Concat(GetElements(CommonMetaData.Lookup.Keys, MetaDataItemType.Tag, supported)); - elements = elements.Concat(GetElements(CommonProperties.Lookup.Keys, MetaDataItemType.Property, supported)); - elements = elements.Concat(GetElements(CommonStatistics.Lookup.Keys, MetaDataItemType.Statistic, supported)); - return elements.ToArray(); - } + public static readonly IValueConverter Converter = new EnumConverter(); - private static IEnumerable GetElements(IEnumerable names, MetaDataItemType type, IEnumerable> supported) + public static IEnumerable Elements { - foreach (var name in names) + get { - if (!supported.Any(element => string.Equals(element.Key, name, StringComparison.OrdinalIgnoreCase) && element.Value == type)) + var supported = SourceFactory.Supported; + if (DecoratorFactory.CanCreate) { - continue; + supported = supported.Concat(DecoratorFactory.Supported); } - yield return new MetaDataElement(name, type); + return supported.Select(element => new MetaDataElement(element.Key, element.Value)).ToArray(); } } @@ -52,7 +47,9 @@ public class MetaDataElement public MetaDataElement(string name, MetaDataItemType type) { this.Name = name; - this.Type = Enum.GetName(typeof(MetaDataItemType), type); + this.Type = Convert.ToString( + Converter.Convert(type, typeof(string), null, CultureInfo.InvariantCulture) + ); } public string Name { get; private set; } diff --git a/FoxTunes.UI.Windows/ViewModel/PlaylistSettings.cs b/FoxTunes.UI.Windows/ViewModel/PlaylistSettings.cs index 8f9235a70..4c4872a51 100644 --- a/FoxTunes.UI.Windows/ViewModel/PlaylistSettings.cs +++ b/FoxTunes.UI.Windows/ViewModel/PlaylistSettings.cs @@ -12,7 +12,7 @@ namespace FoxTunes.ViewModel { public class PlaylistSettings : ViewModelBase { - public PlaylistColumnManager PlaylistColumnProviderManager { get; private set; } + public IPlaylistColumnManager PlaylistColumnProviderManager { get; private set; } public IPlaylistBrowser PlaylistBrowser { get; private set; } @@ -215,7 +215,7 @@ protected virtual async void OnActiveChanged(object sender, EventArgs e) await Windows.Invoke(() => this.OnIsSavingChanged()).ConfigureAwait(false); } - private Task OnSignal(object sender, ISignal signal) + protected virtual Task OnSignal(object sender, ISignal signal) { switch (signal.Name) { diff --git a/FoxTunes.UI.Windows/ViewModel/StringResources.cs b/FoxTunes.UI.Windows/ViewModel/StringResources.cs index ef4fd4577..fdaac1219 100644 --- a/FoxTunes.UI.Windows/ViewModel/StringResources.cs +++ b/FoxTunes.UI.Windows/ViewModel/StringResources.cs @@ -245,5 +245,85 @@ public static string LibraryRootsDialog_Delete return Strings.LibraryRootsDialog_Delete; } } + + public static string MetaDataProvidersSettingsDialog_GroupHeader + { + get + { + return Strings.MetaDataProvidersSettingsDialog_GroupHeader; + } + } + + public static string MetaDataProvidersSettingsDialog_New + { + get + { + return Strings.MetaDataProvidersSettingsDialog_New; + } + } + + public static string MetaDataProvidersSettingsDialog_Delete + { + get + { + return Strings.MetaDataProvidersSettingsDialog_Delete; + } + } + + public static string MetaDataProvidersSettingsDialog_Name + { + get + { + return Strings.MetaDataProvidersSettingsDialog_Name; + } + } + + public static string MetaDataProvidersSettingsDialog_Type + { + get + { + return Strings.MetaDataProvidersSettingsDialog_Type; + } + } + + public static string MetaDataProvidersSettingsDialog_Enabled + { + get + { + return Strings.MetaDataProvidersSettingsDialog_Enabled; + } + } + + public static string MetaDataProvidersSettingsDialog_Script + { + get + { + return Strings.MetaDataProvidersSettingsDialog_Script; + } + } + + public static string MetaDataProvidersSettingsDialog_Save + { + get + { + return Strings.MetaDataProvidersSettingsDialog_Save; + } + } + + public static string MetaDataProvidersSettingsDialog_Cancel + { + get + { + return Strings.MetaDataProvidersSettingsDialog_Cancel; + } + } + + public static string MetaDataProvidersSettingsDialog_Reset + { + get + { + return Strings.MetaDataProvidersSettingsDialog_Reset; + } + } } } diff --git a/FoxTunes.UI.Windows/WindowsUserInterface.cs b/FoxTunes.UI.Windows/WindowsUserInterface.cs index e9106402e..5dad4edd8 100644 --- a/FoxTunes.UI.Windows/WindowsUserInterface.cs +++ b/FoxTunes.UI.Windows/WindowsUserInterface.cs @@ -114,11 +114,13 @@ public override void Fatal(Exception exception) public override bool Confirm(string message) { var result = default(bool); - if (ActiveWindow != null) + //TODO: Bad .Result(). + var window = GetActiveWindow().Result; + if (window != null) { //TODO: This is the only MessageBox provided with a Window. //TODO: Bad .Wait(). - global::FoxTunes.Windows.Invoke(() => result = MessageBox.Show(ActiveWindow, message, "Confirm", MessageBoxButton.OKCancel, MessageBoxImage.Question) == MessageBoxResult.OK).Wait(); + global::FoxTunes.Windows.Invoke(() => result = MessageBox.Show(window, message, "Confirm", MessageBoxButton.OKCancel, MessageBoxImage.Question) == MessageBoxResult.OK).Wait(); } else { @@ -231,15 +233,17 @@ protected virtual void OnDisposing() } } - public static Window ActiveWindow + public async Task GetActiveWindow() { - get + var activeWindow = default(Window); + await global::FoxTunes.Windows.Invoke(() => { //Try and get the focused window, fall back to one of the "main" windows. - return Application.Current.Windows.OfType().FirstOrDefault( + activeWindow = Application.Current.Windows.OfType().FirstOrDefault( window => window.IsActive ) ?? global::FoxTunes.Windows.ActiveWindow; - } + }).ConfigureAwait(false); + return activeWindow; } } }