diff --git a/FoxTunes.Core/Interfaces/MetaData/IFileAbstraction.cs b/FoxTunes.Core/Interfaces/MetaData/IFileAbstraction.cs new file mode 100644 index 000000000..7c434f3d2 --- /dev/null +++ b/FoxTunes.Core/Interfaces/MetaData/IFileAbstraction.cs @@ -0,0 +1,16 @@ +using System; +using System.IO; + +namespace FoxTunes.Interfaces +{ + public interface IFileAbstraction : IDisposable + { + string FileName { get; } + + Stream ReadStream { get; } + + Stream WriteStream { get; } + + void CloseStream(Stream stream); + } +} diff --git a/FoxTunes.Core/Interfaces/MetaData/IMetaDataSource.cs b/FoxTunes.Core/Interfaces/MetaData/IMetaDataSource.cs index df2304565..b87e0276b 100644 --- a/FoxTunes.Core/Interfaces/MetaData/IMetaDataSource.cs +++ b/FoxTunes.Core/Interfaces/MetaData/IMetaDataSource.cs @@ -10,6 +10,8 @@ public interface IMetaDataSource : IBaseComponent Task> GetMetaData(string fileName); + Task> GetMetaData(IFileAbstraction fileAbstraction); + Task SetMetaData(string fileName, IEnumerable metaDataItems, Func predicate); } diff --git a/FoxTunes.MetaData.FileName/FileNameMetaDataSource.cs b/FoxTunes.MetaData.FileName/FileNameMetaDataSource.cs index 7dff11862..48009f397 100644 --- a/FoxTunes.MetaData.FileName/FileNameMetaDataSource.cs +++ b/FoxTunes.MetaData.FileName/FileNameMetaDataSource.cs @@ -102,6 +102,11 @@ public async Task> GetMetaData(string fileName) return result; } + public Task> GetMetaData(IFileAbstraction fileAbstraction) + { + throw new NotImplementedException(); + } + protected virtual MetaDataItem GetMetaData(string name, string value) { return new MetaDataItem(name, MetaDataItemType.Tag) diff --git a/FoxTunes.MetaData.TagLib/Mpeg4/File.cs b/FoxTunes.MetaData.TagLib/Mpeg4/File.cs index 61afb1322..a814c4e25 100644 --- a/FoxTunes.MetaData.TagLib/Mpeg4/File.cs +++ b/FoxTunes.MetaData.TagLib/Mpeg4/File.cs @@ -49,6 +49,11 @@ public Task> GetMetaData(string fileName) #endif } + public Task> GetMetaData(global::FoxTunes.Interfaces.IFileAbstraction fileAbstraction) + { + throw new NotImplementedException(); + } + public Task SetMetaData(string fileName, IEnumerable metaDataItems, Func predicate) { var box = this.GetXtraBox(); diff --git a/FoxTunes.MetaData.TagLib/TagLibFileAbstraction.cs b/FoxTunes.MetaData.TagLib/TagLibFileAbstraction.cs new file mode 100644 index 000000000..8befef793 --- /dev/null +++ b/FoxTunes.MetaData.TagLib/TagLibFileAbstraction.cs @@ -0,0 +1,53 @@ +using FoxTunes.Interfaces; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace FoxTunes +{ + public class TagLibFileAbstraction : global::TagLib.File.IFileAbstraction + { + public TagLibFileAbstraction(IFileAbstraction fileAbstraction) + { + this.FileAbstraction = fileAbstraction; + } + + public IFileAbstraction FileAbstraction { get; private set; } + + public string Name + { + get + { + return this.FileAbstraction.FileName; + } + } + + public Stream ReadStream + { + get + { + return this.FileAbstraction.ReadStream; + } + } + + public Stream WriteStream + { + get + { + return this.FileAbstraction.WriteStream; + } + } + + public void CloseStream(Stream stream) + { + this.FileAbstraction.CloseStream(stream); + } + + public static TagLibFileAbstraction Create(IFileAbstraction fileAbstraction) + { + return new TagLibFileAbstraction(fileAbstraction); + } + } +} diff --git a/FoxTunes.MetaData.TagLib/TagLibFileFactory.cs b/FoxTunes.MetaData.TagLib/TagLibFileFactory.cs index 951dc28d0..328821cf4 100644 --- a/FoxTunes.MetaData.TagLib/TagLibFileFactory.cs +++ b/FoxTunes.MetaData.TagLib/TagLibFileFactory.cs @@ -36,6 +36,11 @@ public File Create(string fileName) return file; } + public File Create(IFileAbstraction fileAbstraction) + { + return File.Create(TagLibFileAbstraction.Create(fileAbstraction)); + } + public IEnumerable GetConfigurationSections() { return TagLibFileFactoryConfiguration.GetConfigurationSections(); diff --git a/FoxTunes.MetaData.TagLib/TagLibMetaDataSource.cs b/FoxTunes.MetaData.TagLib/TagLibMetaDataSource.cs index e23db9edf..6ee867cb0 100644 --- a/FoxTunes.MetaData.TagLib/TagLibMetaDataSource.cs +++ b/FoxTunes.MetaData.TagLib/TagLibMetaDataSource.cs @@ -126,7 +126,17 @@ public override void InitializeComponent(ICore core) base.InitializeComponent(core); } - public async Task> GetMetaData(string fileName) + public Task> GetMetaData(string fileName) + { + return this.GetMetaData(fileName, () => this.FileFactory.Create(fileName)); + } + + public Task> GetMetaData(IFileAbstraction fileAbstraction) + { + return this.GetMetaData(fileAbstraction.FileName, () => this.FileFactory.Create(fileAbstraction)); + } + + public async Task> GetMetaData(string fileName, Func factory) { if (!this.IsSupported(fileName)) { @@ -139,7 +149,7 @@ public async Task> GetMetaData(string fileName) Logger.Write(this, LogLevel.Trace, "Reading meta data for file: {0}", fileName); try { - using (var file = this.FileFactory.Create(fileName)) + using (var file = factory()) { if (file.PossiblyCorrupt) { diff --git a/FoxTunes.Output.Bass.Archive/BassArchiveStreamProvider.cs b/FoxTunes.Output.Bass.Archive/BassArchiveStreamProvider.cs new file mode 100644 index 000000000..1cace9266 --- /dev/null +++ b/FoxTunes.Output.Bass.Archive/BassArchiveStreamProvider.cs @@ -0,0 +1,68 @@ +using FoxTunes.Interfaces; +using ManagedBass; +using ManagedBass.ZipStream; +using System.Collections.Generic; + +namespace FoxTunes +{ + public class BassArchiveStreamProvider : BassStreamProvider + { + public BassArchiveStreamProviderBehaviour Behaviour { get; private set; } + + public override void InitializeComponent(ICore core) + { + this.Behaviour = ComponentRegistry.Instance.GetComponent(); + base.InitializeComponent(core); + } + + public override bool CanCreateStream(PlaylistItem playlistItem) + { + //The behaviour is not loaded for utilities so we can't check whether it's enabled. + //if (this.Behaviour == null || !this.Behaviour.Enabled) + //{ + // return false; + //} + var fileName = default(string); + var entryName = default(string); + return ArchiveUtils.ParseUrl(playlistItem.FileName, out fileName, out entryName); + } + + public override IBassStream CreateBasicStream(PlaylistItem playlistItem, IEnumerable advice, BassFlags flags) + { + var fileName = default(string); + var entryName = default(string); + if (!ArchiveUtils.ParseUrl(playlistItem.FileName, out fileName, out entryName)) + { + //This shouldn't happen as CanCreateStream would have returned false. + return BassStream.Empty; + } + var index = default(int); + if (!ArchiveUtils.GetEntryIndex(fileName, entryName, out index)) + { + //The associated entry was not found. + return BassStream.Empty; + } + var channelHandle = BassZipStream.CreateStream(fileName, index, Flags: flags); + return this.CreateBasicStream(channelHandle, advice, flags); + } + + public override IBassStream CreateInteractiveStream(PlaylistItem playlistItem, IEnumerable advice, BassFlags flags) + { + var fileName = default(string); + var entryName = default(string); + if (!ArchiveUtils.ParseUrl(playlistItem.FileName, out fileName, out entryName)) + { + //This shouldn't happen as CanCreateStream would have returned false. + return BassStream.Empty; + } + var index = default(int); + if (!ArchiveUtils.GetEntryIndex(fileName, entryName, out index)) + { + //The associated entry was not found. + return BassStream.Empty; + } + var channelHandle = BassZipStream.CreateStream(fileName, index, Flags: flags); + return this.CreateInteractiveStream(channelHandle, advice, flags); + } + } +} diff --git a/FoxTunes.Output.Bass.Archive/BassArchiveStreamProviderBehaviour.cs b/FoxTunes.Output.Bass.Archive/BassArchiveStreamProviderBehaviour.cs new file mode 100644 index 000000000..d1dc711f3 --- /dev/null +++ b/FoxTunes.Output.Bass.Archive/BassArchiveStreamProviderBehaviour.cs @@ -0,0 +1,265 @@ +using FoxDb; +using FoxDb.Interfaces; +using FoxTunes.Interfaces; +using ManagedBass; +using ManagedBass.ZipStream; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace FoxTunes +{ + [Component("5E1331EE-37F9-41BB-BD5E-82E9B4995B8A", null, priority: ComponentAttribute.PRIORITY_LOW)] + [ComponentDependency(Slot = ComponentSlots.Output)] + public class BassArchiveStreamProviderBehaviour : StandardBehaviour, IConfigurableComponent, IBackgroundTaskSource, IInvocableComponent, IDisposable + { + public const string OPEN_ARCHIVE = "FGGG"; + + public ICore Core { get; private set; } + + public IPlaylistManager PlaylistManager { get; private set; } + + public IFileSystemBrowser FileSystemBrowser { get; private set; } + + public IConfiguration Configuration { get; private set; } + + public IBassOutput Output { get; private set; } + + new public bool IsInitialized { get; private set; } + + private bool _Enabled { get; set; } + + public bool Enabled + { + get + { + return this._Enabled; + } + set + { + this._Enabled = value; + Logger.Write(this, LogLevel.Debug, "Enabled = {0}", this.Enabled); + } + } + + public override void InitializeComponent(ICore core) + { + this.Core = core; + this.Output = core.Components.Output as IBassOutput; + this.Output.Init += this.OnInit; + this.Output.Free += this.OnFree; + this.PlaylistManager = core.Managers.Playlist; + this.FileSystemBrowser = core.Components.FileSystemBrowser; + this.Configuration = core.Components.Configuration; + this.Configuration.GetElement( + BassArchiveStreamProviderBehaviourConfiguration.SECTION, + BassArchiveStreamProviderBehaviourConfiguration.ENABLED_ELEMENT + ).ConnectValue(value => this.Enabled = value); + base.InitializeComponent(core); + } + + protected virtual void OnInit(object sender, EventArgs e) + { + if (!this.Enabled) + { + return; + } + var flags = BassFlags.Decode; + if (this.Output.Float) + { + flags |= BassFlags.Float; + } + BassUtils.OK(BassZipStream.Init()); + this.IsInitialized = true; + Logger.Write(this, LogLevel.Debug, "BASS ZIPSTREAM Initialized."); + } + + protected virtual void OnFree(object sender, EventArgs e) + { + if (!this.IsInitialized) + { + return; + } + BassZipStream.Free(); + this.IsInitialized = false; + } + + public IEnumerable GetConfigurationSections() + { + return BassArchiveStreamProviderBehaviourConfiguration.GetConfigurationSections(); + } + + public IEnumerable Invocations + { + get + { + if (this.Enabled) + { + yield return new InvocationComponent(InvocationComponent.CATEGORY_PLAYLIST, OPEN_ARCHIVE, "Open Archive"); + } + } + } + + public Task InvokeAsync(IInvocationComponent component) + { + switch (component.Id) + { + case OPEN_ARCHIVE: + return this.OpenArchive(); + } +#if NET40 + return TaskEx.FromResult(false); +#else + return Task.CompletedTask; +#endif + } + + public Task OpenArchive() + { + var options = new BrowseOptions( + "Open", + string.Empty, + new[] + { + new BrowseFilter("Archives", ArchiveUtils.Extensions) + }, + BrowseFlags.File + ); + var result = this.FileSystemBrowser.Browse(options); + if (!result.Success) + { +#if NET40 + return TaskEx.FromResult(false); +#else + return Task.CompletedTask; +#endif + } + return this.OpenArchive(this.PlaylistManager.SelectedPlaylist, result.Paths.FirstOrDefault()); + } + + public async Task OpenArchive(Playlist playlist, string fileName) + { + using (var task = new AddArchiveToPlaylistTask(playlist, fileName)) + { + task.InitializeComponent(this.Core); + this.OnBackgroundTask(task); + await task.Run().ConfigureAwait(false); + } + } + + protected virtual void OnBackgroundTask(IBackgroundTask backgroundTask) + { + if (this.BackgroundTask == null) + { + return; + } + this.BackgroundTask(this, new BackgroundTaskEventArgs(backgroundTask)); + } + + public event BackgroundTaskEventHandler BackgroundTask; + + 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.Output != null) + { + this.Output.Init -= this.OnInit; + this.Output.Free -= this.OnFree; + } + } + + ~BassArchiveStreamProviderBehaviour() + { + 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 AddArchiveToPlaylistTask : PlaylistTaskBase + { + public AddArchiveToPlaylistTask(Playlist playlist, string fileName) : base(playlist) + { + this.FileName = fileName; + } + + public string FileName { get; private set; } + + public IPlaylistBrowser PlaylistBrowser { get; private set; } + + public ArchivePlaylistItemFactory Factory { get; private set; } + + public override void InitializeComponent(ICore core) + { + this.PlaylistBrowser = core.Components.PlaylistBrowser; + this.Factory = new ArchivePlaylistItemFactory(this.FileName); + this.Factory.InitializeComponent(core); + base.InitializeComponent(core); + } + + protected override async Task OnRun() + { + this.Name = "Opening archive"; + var playlistItems = await this.Factory.Create().ConfigureAwait(false); + using (var task = new SingletonReentrantTask(this, ComponentSlots.Database, SingletonReentrantTask.PRIORITY_HIGH, async cancellationToken => + { + //Always append for now. + this.Sequence = this.PlaylistBrowser.GetInsertIndex(this.Playlist); + await this.AddPlaylistItems(this.Playlist, playlistItems).ConfigureAwait(false); + await this.ShiftItems(QueryOperator.GreaterOrEqual, this.Sequence, this.Offset).ConfigureAwait(false); + await this.SetPlaylistItemsStatus(PlaylistItemStatus.None).ConfigureAwait(false); + })) + { + await task.Run().ConfigureAwait(false); + } + await this.SignalEmitter.Send(new Signal(this, CommonSignals.PlaylistUpdated, new[] { this.Playlist })).ConfigureAwait(false); + } + + private async Task AddPlaylistItems(Playlist playlist, IEnumerable playlistItems) + { + using (var transaction = this.Database.BeginTransaction(this.Database.PreferredIsolationLevel)) + { + var set = this.Database.Set(transaction); + var position = 0; + foreach (var playlistItem in playlistItems) + { + Logger.Write(this, LogLevel.Debug, "Adding file to playlist: {0}", playlistItem.FileName); + playlistItem.Playlist_Id = playlist.Id; + playlistItem.Sequence = this.Sequence + position; + playlistItem.Status = PlaylistItemStatus.Import; + await set.AddAsync(playlistItem).ConfigureAwait(false); + position++; + } + this.Offset += position; + if (transaction.HasTransaction) + { + transaction.Commit(); + } + } + } + } + } +} \ No newline at end of file diff --git a/FoxTunes.Output.Bass.Archive/BassArchiveStreamProviderBehaviourConfiguration.cs b/FoxTunes.Output.Bass.Archive/BassArchiveStreamProviderBehaviourConfiguration.cs new file mode 100644 index 000000000..035d3a5e0 --- /dev/null +++ b/FoxTunes.Output.Bass.Archive/BassArchiveStreamProviderBehaviourConfiguration.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace FoxTunes +{ + public static class BassArchiveStreamProviderBehaviourConfiguration + { + public const string SECTION = "B593BE99-CCA4-42D2-A129-7F65A96FD302"; + + public const string ENABLED_ELEMENT = "AAAA73E0-DAAD-4B73-A375-D6D820AFAE93"; + + public static IEnumerable GetConfigurationSections() + { + yield return new ConfigurationSection(SECTION, "Archive") + .WithElement(new BooleanConfigurationElement(ENABLED_ELEMENT, "Enabled").WithValue(false)); + } + } +} diff --git a/FoxTunes.Output.Bass.Archive/FoxTunes.Output.Bass.Archive.csproj b/FoxTunes.Output.Bass.Archive/FoxTunes.Output.Bass.Archive.csproj new file mode 100644 index 000000000..c0d4c56df --- /dev/null +++ b/FoxTunes.Output.Bass.Archive/FoxTunes.Output.Bass.Archive.csproj @@ -0,0 +1,65 @@ + + + + net40;net461 + FoxTunes.Output.Bass.Archive + ..\distribution\$(Platform)\ + true + true + 2.2.7.0 + x86;x64 + + + + + ..\lib\net40\ManagedBass.dll + + + ..\lib\net40\ManagedBass.ZipStream.dll + + + + + + ..\lib\ManagedBass.dll + + + ..\lib\ManagedBass.ZipStream.dll + + + + + + + + + + PreserveNewest + + + + + + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/FoxTunes.Output.Bass.Archive/Utilities/ArchiveEntryStream.cs b/FoxTunes.Output.Bass.Archive/Utilities/ArchiveEntryStream.cs new file mode 100644 index 000000000..6eb8bbc70 --- /dev/null +++ b/FoxTunes.Output.Bass.Archive/Utilities/ArchiveEntryStream.cs @@ -0,0 +1,104 @@ +using ManagedBass.ZipStream; +using System; +using System.IO; + +namespace FoxTunes +{ + public class ArchiveEntryStream : Stream + { + private IntPtr Entry; + + public ArchiveEntryStream(IntPtr entry) + { + this.Entry = entry; + } + + public override bool CanRead + { + get + { + return true; + } + } + + public override bool CanSeek + { + get + { + return true; + } + } + + public override bool CanWrite + { + get + { + return false; + } + } + + public override long Length + { + get + { + return ArchiveEntry.GetEntryLength(this.Entry); + } + } + + public override long Position + { + get + { + return ArchiveEntry.GetEntryPosition(this.Entry); + } + set + { + this.Seek(value, SeekOrigin.Begin); + } + } + + public override void Flush() + { + throw new NotImplementedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (offset != 0) + { + throw new NotImplementedException(); + } + return ArchiveEntry.ReadEntry(this.Entry, buffer, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + switch (origin) + { + case SeekOrigin.Begin: + break; + case SeekOrigin.Current: + offset = this.Position + offset; + break; + case SeekOrigin.End: + offset = this.Length + offset; + break; + } + if (!ArchiveEntry.SeekEntry(this.Entry, offset)) + { + return 0; + } + return offset; + } + + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + } +} diff --git a/FoxTunes.Output.Bass.Archive/Utilities/ArchiveFileAbstraction.cs b/FoxTunes.Output.Bass.Archive/Utilities/ArchiveFileAbstraction.cs new file mode 100644 index 000000000..d256485ba --- /dev/null +++ b/FoxTunes.Output.Bass.Archive/Utilities/ArchiveFileAbstraction.cs @@ -0,0 +1,117 @@ +using FoxTunes.Interfaces; +using ManagedBass.ZipStream; +using System; +using System.IO; + +namespace FoxTunes +{ + public class ArchiveFileAbstraction : BaseComponent, IFileAbstraction + { + private IntPtr Entry; + + private ArchiveFileAbstraction() + { + this.Entry = IntPtr.Zero; + } + + public ArchiveFileAbstraction(string fileName, string entryName, int index) : this() + { + this.FileName = fileName; + this.EntryName = entryName; + this.Index = index; + } + + string IFileAbstraction.FileName + { + get + { + return this.EntryName; + } + } + + public string FileName { get; private set; } + + public string EntryName { get; private set; } + + public int Index { get; private set; } + + public Stream ReadStream { get; private set; } + + public Stream WriteStream { get; private set; } + + public bool IsOpen { get; private set; } + + public void Open() + { + if (!ArchiveEntry.OpenEntry(this.FileName, this.Index, BassZipStream.Overwrite, out this.Entry)) + { + return; + } + this.ReadStream = new ArchiveEntryStream(this.Entry); + this.IsOpen = true; + } + + public void Close() + { + if (this.ReadStream != null) + { + this.ReadStream.Dispose(); + this.ReadStream = null; + } + if (!IntPtr.Zero.Equals(this.Entry)) + { + ArchiveEntry.CloseEntry(this.Entry); + this.Entry = IntPtr.Zero; + } + this.IsOpen = false; + } + + public void CloseStream(Stream stream) + { + stream.Close(); + } + + 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() + { + this.Close(); + } + + ~ArchiveFileAbstraction() + { + Logger.Write(this.GetType(), LogLevel.Error, "Component was not disposed: {0}", this.GetType().Name); + try + { + this.Dispose(true); + } + catch + { + //Nothing can be done, never throw on GC thread. + } + } + + public static ArchiveFileAbstraction Create(string fileName, string entryName, int index) + { + var fileAbstraction = new ArchiveFileAbstraction(fileName, entryName, index); + fileAbstraction.Open(); + return fileAbstraction; + } + } +} diff --git a/FoxTunes.Output.Bass.Archive/Utilities/ArchivePlaylistItemFactory.cs b/FoxTunes.Output.Bass.Archive/Utilities/ArchivePlaylistItemFactory.cs new file mode 100644 index 000000000..d6695b33c --- /dev/null +++ b/FoxTunes.Output.Bass.Archive/Utilities/ArchivePlaylistItemFactory.cs @@ -0,0 +1,147 @@ +using FoxTunes.Interfaces; +using ManagedBass.ZipStream; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace FoxTunes +{ + public class ArchivePlaylistItemFactory : BaseComponent + { + public ArchivePlaylistItemFactory(string fileName) + { + this.FileName = fileName; + } + + public string FileName { get; private set; } + + public IOutput Output { get; private set; } + + public IMetaDataSourceFactory MetaDataSourceFactory { get; private set; } + + public override void InitializeComponent(ICore core) + { + this.Output = core.Components.Output; + this.MetaDataSourceFactory = core.Factories.MetaDataSource; + base.InitializeComponent(core); + } + + public async Task Create() + { + var playlistItems = new List(); + var archive = default(IntPtr); + if (Archive.Create(out archive)) + { + try + { + if (Archive.Open(archive, this.FileName)) + { + await this.Create(archive, playlistItems).ConfigureAwait(false); + } + else + { + //TODO: Warn. + } + } + finally + { + Archive.Release(archive); + } + } + else + { + //TODO: Warn. + } + return playlistItems.ToArray(); + } + + protected virtual async Task Create(IntPtr archive, List playlistItems) + { + var count = default(int); + var directoryName = Path.GetDirectoryName(this.FileName); + var metaDataSource = this.MetaDataSourceFactory.Create(); + if (Archive.GetEntryCount(archive, out count)) + { + for (var a = 0; a < count; a++) + { + var entry = default(Archive.ArchiveEntry); + if (Archive.GetEntry(archive, out entry, a)) + { + if (!this.Output.IsSupported(entry.path)) + { + continue; + } + var fileName = ArchiveUtils.CreateUrl(this.FileName, entry.path); + var playlistItem = new PlaylistItem() + { + DirectoryName = directoryName, + FileName = fileName + }; + try + { + using (var fileAbstraction = ArchiveFileAbstraction.Create(this.FileName, entry.path, a)) + { + if (fileAbstraction.IsOpen) + { + playlistItem.MetaDatas = ( + await metaDataSource.GetMetaData(fileAbstraction).ConfigureAwait(false) + ).ToList(); + } + } + } + catch (Exception e) + { + Logger.Write(this, LogLevel.Debug, "Failed to read meta data from file \"{0}\": {1}", this.FileName, e.Message); + } + this.EnsureMetaData(a, entry, playlistItem); + playlistItems.Add(playlistItem); + } + else + { + //TODO: Warn. + } + } + } + else + { + //TODO: Warn. + } + } + + protected virtual void EnsureMetaData(int index, Archive.ArchiveEntry entry, PlaylistItem playlistItem) + { + var hasTrack = false; + var hasTitle = false; + if (playlistItem.MetaDatas != null) + { + var metaData = playlistItem.MetaDatas.ToDictionary( + metaDataItem => metaDataItem.Name, + metaDataItem => metaDataItem.Value, + StringComparer.OrdinalIgnoreCase + ); + hasTrack = metaData.ContainsKey(CommonMetaData.Track); + hasTitle = metaData.ContainsKey(CommonMetaData.Title); + } + else + { + playlistItem.MetaDatas = new List(); + } + if (!hasTrack) + { + playlistItem.MetaDatas.Add(new MetaDataItem(CommonMetaData.Track, MetaDataItemType.Tag) + { + Value = Convert.ToString(index) + }); + } + if (!hasTitle) + { + playlistItem.MetaDatas.Add(new MetaDataItem(CommonMetaData.Title, MetaDataItemType.Tag) + { + Value = entry.path.GetName() + }); + } + } + } +} \ No newline at end of file diff --git a/FoxTunes.Output.Bass.Archive/Utilities/ArchiveUtils.cs b/FoxTunes.Output.Bass.Archive/Utilities/ArchiveUtils.cs new file mode 100644 index 000000000..6c1ec4dbb --- /dev/null +++ b/FoxTunes.Output.Bass.Archive/Utilities/ArchiveUtils.cs @@ -0,0 +1,159 @@ +using ManagedBass.ZipStream; +using System; +using System.Collections.Generic; +using System.IO; + +namespace FoxTunes +{ + public static class ArchiveUtils + { + public const string SCHEME = "archive"; + + private static readonly Lazy _Extensions = new Lazy(() => + { + var instance = default(IntPtr); + var extensions = new List(); + if (Archive.Create(out instance)) + { + try + { + var count = default(int); + if (Archive.GetFormatCount(instance, out count)) + { + for (var a = 0; a < count; a++) + { + var format = default(Archive.ArchiveFormat); + if (Archive.GetFormat(instance, out format, a)) + { + if (!string.IsNullOrEmpty(format.extensions)) + { + foreach (var extension in format.extensions.Split(',')) + { + extensions.Add(extension.Trim()); + } + } + } + else + { + //TODO: Warn. + } + } + } + else + { + //TODO: Warn. + } + } + finally + { + Archive.Release(instance); + } + } + else + { + //TODO: Warn. + } + return extensions.ToArray(); + }); + + public static string[] Extensions + { + get + { + return _Extensions.Value; + } + } + + public static bool GetEntryIndex(string fileName, string entryName, out int index) + { + var archive = default(IntPtr); + if (Archive.Create(out archive)) + { + try + { + if (Archive.Open(archive, fileName)) + { + return GetEntryIndex(archive, entryName, out index); + } + else + { + //TODO: Warn. + } + } + finally + { + Archive.Release(archive); + } + } + else + { + //TODO: Warn. + } + index = -1; + return false; + } + + public static bool GetEntryIndex(IntPtr archive, string entryName, out int index) + { + var count = default(int); + if (Archive.GetEntryCount(archive, out count)) + { + for (index = 0; index < count; index++) + { + var entry = default(Archive.ArchiveEntry); + if (Archive.GetEntry(archive, out entry, index)) + { + if (string.Equals(entry.path, entryName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + else + { + //TODO: Warn. + } + } + } + else + { + //TODO: Warn. + } + index = -1; + return false; + } + + public static string CreateUrl(string fileName, string entryName) + { + return string.Format( + "{0}://{1}?entry={2}", + SCHEME, + fileName.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), + Uri.EscapeDataString( + entryName + ).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + ); + } + + public static bool ParseUrl(string url, out string fileName, out string entryName) + { + return ParseUrl(new Uri(url), out fileName, out entryName); + } + + public static bool ParseUrl(Uri url, out string fileName, out string entryName) + { + fileName = default(string); + entryName = default(string); + if (!string.Equals(url.Scheme, SCHEME, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + fileName = Uri.UnescapeDataString( + url.AbsolutePath + ).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + entryName = Uri.UnescapeDataString( + url.GetQueryParameter("entry") + ).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + return true; + } + } +} diff --git a/FoxTunes.Output.Bass.Cd/BassCdMetaDataSource.cs b/FoxTunes.Output.Bass.Cd/BassCdMetaDataSource.cs index bc2e14116..258af6e58 100644 --- a/FoxTunes.Output.Bass.Cd/BassCdMetaDataSource.cs +++ b/FoxTunes.Output.Bass.Cd/BassCdMetaDataSource.cs @@ -44,6 +44,11 @@ public Task> GetMetaData(string fileName) #endif } + public Task> GetMetaData(IFileAbstraction fileAbstraction) + { + throw new NotImplementedException(); + } + public Task SetMetaData(string fileName, IEnumerable metaDataItems, Func predicate) { throw new NotImplementedException(); diff --git a/FoxTunes.sln b/FoxTunes.sln index 218ccb9ea..956f64f62 100644 --- a/FoxTunes.sln +++ b/FoxTunes.sln @@ -121,7 +121,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FoxTunes.UI.Windows.Lyrics" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FoxTunes.UI.Windows.Snapping", "FoxTunes.UI.Windows.Snapping\FoxTunes.UI.Windows.Snapping.csproj", "{5F4775F6-B042-4068-B4E6-5B49A247C256}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FoxTunes.Output.Bass.Tempo", "FoxTunes.Output.Bass.Tempo\FoxTunes.Output.Bass.Tempo.csproj", "{52B51207-89D3-47C6-80EA-637E71D8FFF0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FoxTunes.Output.Bass.Tempo", "FoxTunes.Output.Bass.Tempo\FoxTunes.Output.Bass.Tempo.csproj", "{52B51207-89D3-47C6-80EA-637E71D8FFF0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FoxTunes.Output.Bass.Archive", "FoxTunes.Output.Bass.Archive\FoxTunes.Output.Bass.Archive.csproj", "{72E8C729-6E04-4005-AAF6-97A2DCA05C49}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -563,6 +565,14 @@ Global {52B51207-89D3-47C6-80EA-637E71D8FFF0}.Release|x64.Build.0 = Release|x64 {52B51207-89D3-47C6-80EA-637E71D8FFF0}.Release|x86.ActiveCfg = Release|x86 {52B51207-89D3-47C6-80EA-637E71D8FFF0}.Release|x86.Build.0 = Release|x86 + {72E8C729-6E04-4005-AAF6-97A2DCA05C49}.Debug|x64.ActiveCfg = Debug|x64 + {72E8C729-6E04-4005-AAF6-97A2DCA05C49}.Debug|x64.Build.0 = Debug|x64 + {72E8C729-6E04-4005-AAF6-97A2DCA05C49}.Debug|x86.ActiveCfg = Debug|x86 + {72E8C729-6E04-4005-AAF6-97A2DCA05C49}.Debug|x86.Build.0 = Debug|x86 + {72E8C729-6E04-4005-AAF6-97A2DCA05C49}.Release|x64.ActiveCfg = Release|x64 + {72E8C729-6E04-4005-AAF6-97A2DCA05C49}.Release|x64.Build.0 = Release|x64 + {72E8C729-6E04-4005-AAF6-97A2DCA05C49}.Release|x86.ActiveCfg = Release|x86 + {72E8C729-6E04-4005-AAF6-97A2DCA05C49}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/lib/ManagedBass.ZipStream.dll b/lib/ManagedBass.ZipStream.dll new file mode 100644 index 000000000..cf2811e96 Binary files /dev/null and b/lib/ManagedBass.ZipStream.dll differ diff --git a/lib/net40/ManagedBass.ZipStream.dll b/lib/net40/ManagedBass.ZipStream.dll new file mode 100644 index 000000000..fe9a90fcd Binary files /dev/null and b/lib/net40/ManagedBass.ZipStream.dll differ diff --git a/lib/x64/bass_zipstream.dll b/lib/x64/bass_zipstream.dll new file mode 100644 index 000000000..16b0d0ac5 Binary files /dev/null and b/lib/x64/bass_zipstream.dll differ diff --git a/lib/x86/bass_zipstream.dll b/lib/x86/bass_zipstream.dll new file mode 100644 index 000000000..344f2b252 Binary files /dev/null and b/lib/x86/bass_zipstream.dll differ