diff --git a/Jellyfin.Plugin.Reports/Api/Data/ExcelExport.cs b/Jellyfin.Plugin.Reports/Api/Data/ExcelExport.cs
deleted file mode 100644
index c3398b9..0000000
--- a/Jellyfin.Plugin.Reports/Api/Data/ExcelExport.cs
+++ /dev/null
@@ -1,244 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.IO.Compression;
-using System.Linq;
-using System.Security;
-using System.Text;
-using Jellyfin.Plugin.Reports.Api.Model;
-
-namespace Jellyfin.Plugin.Reports.Api.Data
-{
- /// Build XML files and generate XLSX archives from them.
- public static class ExcelExport
- {
- /// Creates an XLSX file from
- /// The results of a client query
- /// A MemoryStream containing a XLSX file.
- public static MemoryStream GenerateXlsx(ReportResult reportResult)
- {
- // Constant XML files to place into the XLSX archive
- const string rootRels = "";
- const string rootContentTypes = "";
- const string workbookRels = "";
- const string workbookXml = "";
- const string styleXml = "";
-
- static void AddStringToArchive(ZipArchive archive, string fileName, string content)
- {
- ZipArchiveEntry file = archive.CreateEntry(fileName, CompressionLevel.Optimal);
- using (Stream entryStream = file.Open())
- using (StreamWriter streamWriter = new StreamWriter(entryStream))
- streamWriter.Write(content);
- }
- static void AddXmlToArchive(ZipArchive archive, string fileName, ExcelXmlBuilder content)
- {
- ZipArchiveEntry file = archive.CreateEntry(fileName, CompressionLevel.Optimal);
- using (Stream entryStream = file.Open())
- using (StreamWriter streamWriter = new StreamWriter(entryStream))
- content.WriteXml(streamWriter);
- }
-
- ExcelSharedString sharedString = new ExcelSharedString();
- ExcelSheet sheetObj = new ExcelSheet(reportResult, sharedString);
-
- // Add reportResult to ExcelSheet object
- if (reportResult.IsGrouped)
- {
- reportResult.Groups.ForEach(group =>
- {
- sheetObj.addGroupHeader(group.Name);
- sheetObj.AddRows(group.Rows);
- });
- }
- else
- {
- sheetObj.AddRows(reportResult.Rows);
- }
-
- // Write XLSX file
- MemoryStream memoryStream = new MemoryStream();
- using (ZipArchive archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
- {
- AddStringToArchive(archive, "_rels/.rels", rootRels);
- AddStringToArchive(archive, "[Content_Types].xml", rootContentTypes);
- AddStringToArchive(archive, "xl/_rels/workbook.xml.rels", workbookRels);
- AddStringToArchive(archive, "xl/workbook.xml", workbookXml);
- AddStringToArchive(archive, "xl/styles.xml", styleXml);
- AddXmlToArchive(archive, "xl/sharedStrings.xml", sharedString);
- AddXmlToArchive(archive, "xl/worksheets/sheet1.xml", sheetObj);
- }
- memoryStream.Position = 0;
- return memoryStream;
- }
-
-
- /// Abstract superclass to enforce WriteXml method
- abstract private class ExcelXmlBuilder
- {
- /// Write ExcelXmlBuilder object contents to a StreamWriter in a XML format
- /// The StreamWriter to write the XML content to
- abstract public void WriteXml(StreamWriter writer);
- }
-
- /// XML builder for SharedStrings.xml file
- private class ExcelSharedString : ExcelXmlBuilder
- {
- private int wordCount;
- private readonly List wordList = new();
- private const string sharedStringXmlHeader = " Add a string to the workbook's shared string list (if not already in list), returning its index
- /// The string to be added to the shared string list
- /// Index of string in shared string list
- public int AddString(string text)
- {
- int strPos = wordList.IndexOf(text);
- wordCount++;
- if (strPos == -1)
- {
- wordList.Add(text);
- return wordList.Count - 1;
- }
- return strPos;
- }
-
- public override void WriteXml(StreamWriter writer)
- {
- writer.Write(sharedStringXmlHeader);
- writer.Write($"{wordCount}\" uniqueCount=\"{wordList.Count}\">");
- writer.Write(string.Join(null, wordList.Select(word => $"{SecurityElement.Escape(word)}")));
- writer.Write("");
- }
- }
-
- /// XML builder for sheet XML files (e.g. sheet1.xml)
- private class ExcelSheet : ExcelXmlBuilder
- {
- private readonly int numCols;
- private int rowCount;
- private readonly bool isGrouped;
- private readonly int[] colWidths;
- private readonly List groupHeaderRows = new();
- private readonly ExcelSharedString sharedString;
- private readonly StringBuilder sheetXml;
- private const int minColWidth = 10;
- private const int maxColWidth = 50;
- private const string sheetXmlHeader = "";
- private enum RowType
- {
- sheetHeader,
- groupHeader,
- standard
- }
-
- public ExcelSheet(ReportResult reportResult, ExcelSharedString sharedString)
- {
- this.sharedString = sharedString;
- isGrouped = reportResult.IsGrouped;
- numCols = reportResult.Headers.Count;
- colWidths = Enumerable.Repeat(minColWidth, numCols).ToArray();
- sheetXml = new StringBuilder();
- AddRow(reportResult.Headers.Select(s => s.Name).ToArray(), RowType.sheetHeader);
- }
-
- /// Add rows to the Excel sheet
- /// A list of ReportRows to be added
- public void AddRows(List rows)
- {
- rows.ForEach(row =>
- {
- AddRow(row.Columns.Select(s => s.Name).ToArray(), RowType.standard);
- });
- }
-
- /// Add a group header row to the Excel sheet
- /// The title to use for the header row
- public void addGroupHeader(string header)
- {
- string[] groupHeaderRow = new string[numCols];
- groupHeaderRow[0] = header;
- AddRow(groupHeaderRow, RowType.groupHeader);
- groupHeaderRows.Add(rowCount);
- }
-
- public override void WriteXml(StreamWriter writer)
- {
- writer.Write(sheetXmlHeader);
- writer.Write("");
- writer.Write(string.Join(null, colWidths.Select((width,idx) => $"")));
- writer.Write($"");
- writer.Write(sheetXml);
- writer.Write("");
- if (isGrouped)
- {
- string lastCol = ColIdxToColRef(numCols - 1);
- writer.Write($"");
- writer.Write(string.Join(null, groupHeaderRows.Select(groupRow => $"")));
- writer.Write("");
- }
- writer.Write("");
- }
-
- /// Add a row to the Excel sheet
- /// String array of values to fill row with. One per cell, starting with column A
- /// Type of row to add. Whether it's a title, group header, or standard row
- private void AddRow(string[] rowVals, RowType rowType)
- {
- int rowStyle = rowType.Equals(RowType.standard) ? 2 : 1;
- sheetXml.Append(CultureInfo.InvariantCulture, $"');
- for (int colIdx = 0; colIdx < rowVals.Length; colIdx++)
- {
- string cellStr = rowVals[colIdx];
- sheetXml.Append(CultureInfo.InvariantCulture, $"");
- }
- else
- {
- sheetXml.Append(CultureInfo.InvariantCulture, $"\" t=\"s\">{sharedString.AddString(cellStr)}");
- if (!rowType.Equals(RowType.groupHeader))
- {
- int colWidth = (int)Math.Ceiling(cellStr.Length * (rowType.Equals(RowType.standard) ? 1 : 1.2));
- colWidths[colIdx] = Math.Max(colWidths[colIdx], Math.Min(colWidth, maxColWidth));
- }
- }
- }
- sheetXml.Append("
");
- }
-
- ///
- /// Converts a column index to a Excel Column Ref.
- /// For example ColIdxToColRef(12)
returns "M"
- /// and ColIdxToColRef(30)
returns "AE".
- ///
- /// The column index to be converted, should use zero-based indexing
- /// Excel Column referecnce in terms of base-26 alphabetic string
- private static string ColIdxToColRef(int colIdx)
- {
- LinkedList colRef = new();
- colIdx++;
- while (colIdx > 0)
- {
- int rem = (colIdx - 1) % 26;
- colRef.AddFirst(rem);
- colIdx = (colIdx - rem) / 26;
- }
- return new string(colRef.Select(rem => Convert.ToChar('A' + rem)).ToArray());
- }
- }
-
-
- }
-}
diff --git a/Jellyfin.Plugin.Reports/Api/Data/ReportExport.cs b/Jellyfin.Plugin.Reports/Api/Data/ReportExport.cs
index 628a9c4..093168c 100644
--- a/Jellyfin.Plugin.Reports/Api/Data/ReportExport.cs
+++ b/Jellyfin.Plugin.Reports/Api/Data/ReportExport.cs
@@ -4,7 +4,9 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
+using System.Reflection;
using Jellyfin.Plugin.Reports.Api.Model;
+using ClosedXML.Excel;
namespace Jellyfin.Plugin.Reports.Api.Data
{
@@ -137,7 +139,68 @@ namespace Jellyfin.Plugin.Reports.Api.Data
/// A MemoryStream containing a XLSX file.
public static MemoryStream ExportToExcel(ReportResult reportResult)
{
- return ExcelExport.GenerateXlsx(reportResult);
+ static void AddHeaderStyle(IXLRange range)
+ {
+ range.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
+ range.Style.Font.Bold = true;
+ range.Style.Fill.BackgroundColor = XLColor.FromArgb(222, 222, 222);
+ }
+
+ static void AddReportRows(IXLWorksheet worksheet, List reportRows, ref int nextRow)
+ {
+ IEnumerable rows = reportRows.Select(r => r.Columns.Select(s => s.Name).ToArray());
+ worksheet.Cell(nextRow, 1).InsertData(rows);
+ nextRow += rows.Count();
+ }
+
+ IXLWorkbook workbook = new XLWorkbook(XLEventTracking.Disabled);
+ IXLWorksheet worksheet = workbook.Worksheets.Add("ReportExport");
+
+ // Add report rows
+ int nextRow = 1;
+ IEnumerable headers = reportResult.Headers.Select(s => s.Name);
+ IXLRange headerRange = worksheet.Cell(nextRow++, 1).InsertData(headers, true);
+ AddHeaderStyle(headerRange);
+ if (reportResult.IsGrouped)
+ {
+ foreach (ReportGroup group in reportResult.Groups)
+ {
+ int groupHeaderRow = nextRow++;
+ worksheet.Cell(groupHeaderRow, 1).Value = group.Name;
+ AddHeaderStyle(worksheet.Cell(groupHeaderRow, 1).AsRange());
+ worksheet.Range(groupHeaderRow, 1, groupHeaderRow, reportResult.Headers.Count).Merge();
+ AddReportRows(worksheet, group.Rows, ref nextRow);
+ worksheet.Rows(groupHeaderRow + 1, nextRow - 1).Group();
+ }
+ }
+ else
+ {
+ AddReportRows(worksheet, reportResult.Rows, ref nextRow);
+ }
+
+ // Sheet properties
+ worksheet.Style.Font.FontColor = XLColor.FromArgb(51, 51, 51);
+ worksheet.Style.Font.FontName = "Arial";
+ worksheet.Style.Font.FontSize = 9;
+ worksheet.ShowGridLines = false;
+ worksheet.SheetView.FreezeRows(1);
+ worksheet.Outline.SummaryVLocation = XLOutlineSummaryVLocation.Top;
+ worksheet.RangeUsed().Style.Border.InsideBorder = XLBorderStyleValues.Thin;
+ worksheet.RangeUsed().Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
+ //worksheet.ColumnsUsed().AdjustToContents(10.0, 50.0);
+
+ // Workbook properties
+ workbook.Properties.Author = "Jellyfin";
+ workbook.Properties.Title = "ReportExport";
+ string pluginVer = Assembly.GetExecutingAssembly().GetName().Version.ToString();
+ workbook.Properties.Comments = $"Produced by Jellyfin Reports Plugin {pluginVer}";
+
+ // Save workbook to stream and return
+ MemoryStream memoryStream = new MemoryStream();
+ workbook.SaveAs(memoryStream);
+ workbook.Dispose();
+ memoryStream.Position = 0;
+ return memoryStream;
}
}
}
diff --git a/Jellyfin.Plugin.Reports/Jellyfin.Plugin.Reports.csproj b/Jellyfin.Plugin.Reports/Jellyfin.Plugin.Reports.csproj
index 36bc4da..fa75f7e 100644
--- a/Jellyfin.Plugin.Reports/Jellyfin.Plugin.Reports.csproj
+++ b/Jellyfin.Plugin.Reports/Jellyfin.Plugin.Reports.csproj
@@ -20,6 +20,7 @@
+
diff --git a/build.yaml b/build.yaml
index 858cdb3..7bca32f 100644
--- a/build.yaml
+++ b/build.yaml
@@ -11,6 +11,15 @@ description: "Generate reports of your media library"
category: "General"
artifacts:
- "Jellyfin.Plugin.Reports.dll"
+ - "ClosedXML.dll"
+ - "DocumentFormat.OpenXml.dll"
+ - "ExcelNumberFormat.dll"
+ - "Microsoft.Win32.SystemEvents.dll"
+ - "System.Drawing.Common.dll"
+ - "System.IO.Packaging.dll"
+ - "runtimes/unix/lib/netcoreapp2.0/System.Drawing.Common.dll"
+ - "runtimes/win/lib/netcoreapp2.0/Microsoft.Win32.SystemEvents.dll"
+ - "runtimes/win/lib/netcoreapp2.0/System.Drawing.Common.dll"
changelog: |2-
### Bug Fixes ###
- Fix CSV Export Format (#73) @mwildgoose