Skip to content

Commit

Permalink
Added CSV and Excel export
Browse files Browse the repository at this point in the history
  • Loading branch information
davewalker5 committed Oct 7, 2023
1 parent a288edd commit d83c958
Show file tree
Hide file tree
Showing 18 changed files with 371 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ public enum CommandLineOptionType
Unknown,
Lookup,
Import,
Export,
}
}
34 changes: 30 additions & 4 deletions src/MusicCatalogue.Entities/DataExchange/FlattenedTrack.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using MusicCatalogue.Entities.Database;
using System.Diagnostics.CodeAnalysis;
using System.Text;

namespace MusicCatalogue.Entities.DataExchange
{
Expand All @@ -19,10 +20,10 @@ public class FlattenedTrack : TrackBase

public string ArtistName{ get; set; } = "";
public string AlbumTitle { get; set; } = "";
public string Genre { get; set; } = "";
public string? Genre { get; set; } = "";
public int? Released { get; set; }
public string? CoverUrl { get; set; } = "";
public int TrackNumber { get; set; }
public int? TrackNumber { get; set; }
public string Title { get; set; } = "";

/// <summary>
Expand All @@ -31,8 +32,16 @@ public class FlattenedTrack : TrackBase
/// <returns></returns>
public string ToCsv()
{
var representation = $"\"{ArtistName}\",\"{AlbumTitle}\",\"{Genre}\",\"{Released}\",\"{CoverUrl}\",\"{Title}\",\"{FormattedDuration()}\"";
return representation;
StringBuilder builder = new StringBuilder();
AppendField(builder, ArtistName);
AppendField(builder, AlbumTitle);
AppendField(builder, Genre);
AppendField(builder, Released);
AppendField(builder, CoverUrl);
AppendField(builder, TrackNumber);
AppendField(builder, Title);
AppendField(builder, FormattedDuration());
return builder.ToString();
}

/// <summary>
Expand Down Expand Up @@ -66,5 +75,22 @@ public static FlattenedTrack FromCsv(string record)
Duration = durationMs
};
}

/// <summary>
/// Append a value to a string builder holding a representation of a flattened track in CSV format
/// </summary>
/// <param name="builder"></param>
/// <param name="value"></param>
private static void AppendField(StringBuilder builder, object? value)
{
if (builder.Length > 0)
{
builder.Append(',');
}

builder.Append('"');
builder.Append(value?.ToString() ?? "");
builder.Append('"');
}
}
}
11 changes: 11 additions & 0 deletions src/MusicCatalogue.Entities/Interfaces/IExporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using MusicCatalogue.Entities.DataExchange;

namespace MusicCatalogue.Entities.Interfaces
{
public interface IExporter
{
event EventHandler<TrackDataExchangeEventArgs>? TrackExport;

Task Export(string file);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace MusicCatalogue.Entities.Interfaces
{
public interface ICsvImporter
public interface IImporter
{
event EventHandler<TrackDataExchangeEventArgs>? TrackImport;
Task Import(string file);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ public interface IMusicCatalogueFactory
IArtistManager Artists { get; }
ITrackManager Tracks { get; }
IUserManager Users { get; }
ICsvImporter Importer { get; }
IImporter Importer { get; }
IExporter CsvExporter { get; }
IExporter XlsxExporter { get; }
}
}
54 changes: 54 additions & 0 deletions src/MusicCatalogue.Logic/DataExchange/CsvExporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using MusicCatalogue.Entities.DataExchange;
using MusicCatalogue.Entities.Interfaces;
using System.Diagnostics.CodeAnalysis;
using System.Text;

namespace MusicCatalogue.Logic.DataExchange
{
[ExcludeFromCodeCoverage]
public class CsvExporter : DataExportBase, IExporter
{
private StreamWriter? _writer = null;

#pragma warning disable CS8618
internal CsvExporter(IMusicCatalogueFactory factory) : base(factory)
{
}
#pragma warning restore CS8618

/// <summary>
/// Export the collection to a CSV file
/// </summary>
/// <param name="sightings"></param>
/// <param name="file"></param>
public async Task Export(string file)
{
// Open the CSV file
using (_writer = new(file, false, Encoding.UTF8))
{
// Iterate over the collection, calling the row addition methods
await IterateOverCollection();
}
}

/// <summary>
/// Add the headers to the CSV file
/// </summary>
/// <param name="headers"></param>
protected override void AddHeaders(IEnumerable<string> headers)
{
var csvHeaders = string.Join(",", headers);
_writer!.WriteLine(csvHeaders);
}

/// <summary>
/// Add a track to the CSV file
/// </summary>
/// <param name="track"></param>
/// <param name="_"></param>
protected override void AddTrack(FlattenedTrack track, int _)

Check warning on line 49 in src/MusicCatalogue.Logic/DataExchange/CsvExporter.cs

View workflow job for this annotation

GitHub Actions / build

Rename parameter '_' to 'recordNumber' to match the base class declaration.

Check warning on line 49 in src/MusicCatalogue.Logic/DataExchange/CsvExporter.cs

View workflow job for this annotation

GitHub Actions / build

Rename parameter '_' to 'recordNumber' to match the base class declaration.
{
_writer!.WriteLine(track.ToCsv());
}
}
}
9 changes: 2 additions & 7 deletions src/MusicCatalogue.Logic/DataExchange/CsvImporter.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
using MusicCatalogue.Entities.DataExchange;
using MusicCatalogue.Entities.Exceptions;
using MusicCatalogue.Entities.Interfaces;
using MusicCatalogue.Logic.Factory;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text.RegularExpressions;

namespace MusicCatalogue.Logic.DataExchange
{
[ExcludeFromCodeCoverage]
public partial class CsvImporter : ICsvImporter
public partial class CsvImporter : DataExchangeBase, IImporter
{
private readonly MusicCatalogueFactory _factory;

public event EventHandler<TrackDataExchangeEventArgs>? TrackImport;

#pragma warning disable CS8618
internal CsvImporter(MusicCatalogueFactory factory)
internal CsvImporter(IMusicCatalogueFactory factory) : base(factory)
{
_factory = factory;
}
#pragma warning restore CS8618

Expand Down Expand Up @@ -74,6 +70,5 @@ public async Task Import(string file)
}
}
}

}
}
16 changes: 16 additions & 0 deletions src/MusicCatalogue.Logic/DataExchange/DataExchangeBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using MusicCatalogue.Entities.Interfaces;
using System.Diagnostics.CodeAnalysis;

namespace MusicCatalogue.Logic.DataExchange
{
[ExcludeFromCodeCoverage]
public abstract class DataExchangeBase
{
protected readonly IMusicCatalogueFactory _factory;

protected DataExchangeBase(IMusicCatalogueFactory factory)
{
_factory = factory;
}
}
}
89 changes: 89 additions & 0 deletions src/MusicCatalogue.Logic/DataExchange/DataExportBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using MusicCatalogue.Entities.DataExchange;
using MusicCatalogue.Entities.Interfaces;
using System.Diagnostics.CodeAnalysis;

namespace MusicCatalogue.Logic.DataExchange
{
[ExcludeFromCodeCoverage]
public abstract class DataExportBase : DataExchangeBase
{
private readonly string[] ColumnHeaders =
{
"Artist",
"Album",
"Genre",
"Released",
"Cover Url",
"Track Number",
"Track",
"Duration",
};

public event EventHandler<TrackDataExchangeEventArgs>? TrackExport;

protected DataExportBase(IMusicCatalogueFactory factory) : base(factory)
{

}

/// <summary>
/// Method to add headers to the output
/// </summary>
/// <param name="headers"></param>
protected abstract void AddHeaders(IEnumerable<string> headers);

/// <summary>
/// Method to add a new flattened track to the output
/// </summary>
/// <param name="track"></param>
/// <param name="recordNumber"></param>
protected abstract void AddTrack(FlattenedTrack track, int recordNumber);

/// <summary>
/// Iterate over the collection calling the methods supplied by the child class to add
/// headers and to add each track to the output
/// </summary>
protected async Task IterateOverCollection()
{
// Call the method, supplied by the child class, to add the headers to the output
AddHeaders(ColumnHeaders);

// Initialise the record count
int count = 0;

// Retrieve a list of artists and their albums then iterate over the artists
// and albums
var artists = await _factory.Artists.ListAsync(x => true);
foreach (var artist in artists.OrderBy(x => x.Name))
{
foreach (var album in artist.Albums.OrderBy(x => x.Title))
{
// Retrieve the track list for this album and iterate over the tracks
var tracks = await _factory.Tracks.ListAsync(x => x.AlbumId == album.Id);
foreach (var track in tracks.OrderBy(x => x.Number))
{
// Construct a flattened track
var flattened = new FlattenedTrack
{
ArtistName = artist.Name,
AlbumTitle = album.Title,
Genre = album.Genre,
Released = album.Released,
CoverUrl = album.CoverUrl,
TrackNumber = track.Number,
Title = track.Title,
Duration = track.Duration
};

// Call the method to add this track to the file
count++;
AddTrack(flattened, count);

// Raise the track exported event
TrackExport?.Invoke(this, new TrackDataExchangeEventArgs { RecordCount = count, Track = flattened });
}
}
}
}
}
}
75 changes: 75 additions & 0 deletions src/MusicCatalogue.Logic/DataExchange/XlsxExporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using ClosedXML.Excel;
using MusicCatalogue.Entities.DataExchange;
using MusicCatalogue.Entities.Interfaces;
using System.Diagnostics.CodeAnalysis;

namespace MusicCatalogue.Logic.DataExchange
{
[ExcludeFromCodeCoverage]
public class XlsxExporter : DataExportBase, IExporter
{
private const string WorksheetName = "Music";

private IXLWorksheet? _worksheet = null;

#pragma warning disable CS8618
internal XlsxExporter(IMusicCatalogueFactory factory) : base(factory)
{
}
#pragma warning restore CS8618

/// <summary>
/// Export the collection to a CSV file
/// </summary>
/// <param name="sightings"></param>
/// <param name="file"></param>
public async Task Export(string file)
{
// Create a new Excel Workbook
using (var workbook = new XLWorkbook())
{
// Add a worksheet to contain the data
_worksheet = workbook.Worksheets.Add(WorksheetName);

// Iterate over the collection, calling the row addition methods. This builds the spreadsheet
// in memory
await IterateOverCollection();

// Save the workbook to the specified file
workbook.SaveAs(file);
}
}

/// <summary>
/// Add the headers to the CSV file
/// </summary>
/// <param name="headers"></param>
protected override void AddHeaders(IEnumerable<string> headers)
{
var columnNumber = 1;
foreach (var header in headers)
{
_worksheet!.Cell(1, columnNumber).Value = header;
columnNumber++;
}
}

/// <summary>
/// Add a track to the CSV file
/// </summary>
/// <param name="track"></param>
/// <param name="recordCount"></param>
protected override void AddTrack(FlattenedTrack track, int recordCount)

Check warning on line 62 in src/MusicCatalogue.Logic/DataExchange/XlsxExporter.cs

View workflow job for this annotation

GitHub Actions / build

Rename parameter 'recordCount' to 'recordNumber' to match the base class declaration.

Check warning on line 62 in src/MusicCatalogue.Logic/DataExchange/XlsxExporter.cs

View workflow job for this annotation

GitHub Actions / build

Rename parameter 'recordCount' to 'recordNumber' to match the base class declaration.
{
var row = recordCount + 1;
_worksheet!.Cell(row, 1).Value = track.ArtistName ?? "";
_worksheet!.Cell(row, 2).Value = track.AlbumTitle ?? "";
_worksheet!.Cell(row, 3).Value = track.Genre ?? "";
_worksheet!.Cell(row, 4).Value = track.Released?.ToString() ?? "";
_worksheet!.Cell(row, 5).Value = track.CoverUrl ?? "";
_worksheet!.Cell(row, 6).Value = track.TrackNumber?.ToString() ?? "";
_worksheet!.Cell(row, 7).Value = track.Title ?? "";
_worksheet!.Cell(row, 8).Value = track.FormattedDuration() ?? "";
}
}
}
2 changes: 1 addition & 1 deletion src/MusicCatalogue.Logic/Database/AlbumManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ public async Task<Album> GetAsync(Expression<Func<Album, bool>> predicate)
/// <returns></returns>
public async Task<List<Album>> ListAsync(Expression<Func<Album, bool>> predicate)
=> await _context.Albums
.Include(x => x.Tracks)
.Where(predicate)
.Include(x => x.Tracks)
.ToListAsync();

/// <summary>
Expand Down
Loading

0 comments on commit d83c958

Please sign in to comment.