mirror of
https://github.com/jellyfin/jellyfin-plugin-reports.git
synced 2024-11-23 05:39:45 +00:00
Fix CSV Export Format (#73)
* Make CSV export RFC 4180 compliant * Correct CSV MIME type * Remove disabled columns from exports * Auto format so that indentation is consistent * Use string interpolation for string formatting Apply suggestion from @h1dden-da3m0n to use string interpolation to add double quotes to escaped text.
This commit is contained in:
parent
59b4b41c4a
commit
99692ff0fc
@ -1,4 +1,4 @@
|
||||
#nullable disable
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -211,16 +211,11 @@ namespace Jellyfin.Plugin.Reports.Api.Common
|
||||
foreach (ReportHeader header in options.Select(x => x.Header))
|
||||
{
|
||||
|
||||
if (this.DisplayTypeVisible(header.DisplayType, displayType))
|
||||
if ((!DisplayTypeVisible(header.DisplayType, displayType)) || (!headersMetadataFiltered.Contains(header.FieldName) && header.DisplayType != ReportDisplayType.Export)
|
||||
|| (!headersMetadataFiltered.Contains(HeaderMetadata.Status) && header.DisplayType == ReportDisplayType.Export))
|
||||
{
|
||||
|
||||
if (!headersMetadataFiltered.Contains(header.FieldName) && displayType != ReportDisplayType.Export)
|
||||
{
|
||||
header.DisplayType = ReportDisplayType.None;
|
||||
}
|
||||
}
|
||||
else
|
||||
header.DisplayType = ReportDisplayType.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
#nullable disable
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -9,229 +9,234 @@ namespace Jellyfin.Plugin.Reports.Api.Data
|
||||
{
|
||||
/// <summary> A report export. </summary>
|
||||
public static class ReportExport
|
||||
{
|
||||
/// <summary> Export to CSV. </summary>
|
||||
/// <param name="reportResult"> The report result. </param>
|
||||
/// <returns> A string. </returns>
|
||||
public static string ExportToCsv(ReportResult reportResult)
|
||||
{
|
||||
static void AppendRows(StringBuilder builder, List<ReportRow> rows)
|
||||
{
|
||||
foreach (ReportRow row in rows)
|
||||
{
|
||||
builder.AppendJoin(';', row.Columns.Select(s => s.Name.Replace(',', ' '))).AppendLine();
|
||||
}
|
||||
}
|
||||
{
|
||||
/// <summary> Export to CSV. </summary>
|
||||
/// <param name="reportResult"> The report result. </param>
|
||||
/// <returns> A string. </returns>
|
||||
public static string ExportToCsv(ReportResult reportResult)
|
||||
{
|
||||
static string EscapeText(string text)
|
||||
{
|
||||
string escapedText = text.Replace("\"", "\"\"", System.StringComparison.Ordinal);
|
||||
return text.IndexOfAny(new char[4] { '"', ',', '\n', '\r' }) == -1 ? escapedText : $"\"{escapedText}\"";
|
||||
}
|
||||
static void AppendRows(StringBuilder builder, List<ReportRow> rows)
|
||||
{
|
||||
foreach (ReportRow row in rows)
|
||||
{
|
||||
builder.AppendJoin(',', row.Columns.Select(s => EscapeText(s.Name))).AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder returnValue = new StringBuilder();
|
||||
returnValue.AppendJoin(';', reportResult.Headers.Select(s => s.Name.Replace(',', ' '))).AppendLine();
|
||||
StringBuilder returnValue = new StringBuilder();
|
||||
returnValue.AppendJoin(',', reportResult.Headers.Select(s => EscapeText(s.Name))).AppendLine();
|
||||
|
||||
if (reportResult.IsGrouped)
|
||||
{
|
||||
foreach (ReportGroup group in reportResult.Groups)
|
||||
{
|
||||
AppendRows(returnValue, group.Rows);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AppendRows(returnValue, reportResult.Rows);
|
||||
}
|
||||
if (reportResult.IsGrouped)
|
||||
{
|
||||
foreach (ReportGroup group in reportResult.Groups)
|
||||
{
|
||||
AppendRows(returnValue, group.Rows);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AppendRows(returnValue, reportResult.Rows);
|
||||
}
|
||||
|
||||
return returnValue.ToString();
|
||||
}
|
||||
return returnValue.ToString();
|
||||
}
|
||||
|
||||
|
||||
/// <summary> Export to excel. </summary>
|
||||
/// <param name="reportResult"> The report result. </param>
|
||||
/// <returns> A string. </returns>
|
||||
public static string ExportToExcel(ReportResult reportResult)
|
||||
{
|
||||
const string Style = @"<style type='text/css'>
|
||||
BODY {
|
||||
font-family: Arial;
|
||||
font-size: 12px;
|
||||
}
|
||||
/// <summary> Export to excel. </summary>
|
||||
/// <param name="reportResult"> The report result. </param>
|
||||
/// <returns> A string. </returns>
|
||||
public static string ExportToExcel(ReportResult reportResult)
|
||||
{
|
||||
const string Style = @"<style type='text/css'>
|
||||
BODY {
|
||||
font-family: Arial;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
TABLE {
|
||||
font-family: Arial;
|
||||
font-size: 12px;
|
||||
}
|
||||
TABLE {
|
||||
font-family: Arial;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
A {
|
||||
font-family: Arial;
|
||||
color: #144A86;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
DIV {
|
||||
font-family: Arial;
|
||||
font-size: 12px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
P, LI, DIV {
|
||||
font-size: 12px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
A {
|
||||
font-family: Arial;
|
||||
color: #144A86;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
DIV {
|
||||
font-family: Arial;
|
||||
font-size: 12px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
P, LI, DIV {
|
||||
font-size: 12px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
P, UL {
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
P, UL {
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
H1 {
|
||||
font-size: 18pt;
|
||||
}
|
||||
H1 {
|
||||
font-size: 18pt;
|
||||
}
|
||||
|
||||
H2 {
|
||||
font-weight: bold;
|
||||
font-size: 14pt;
|
||||
COLOR: #C0C0C0;
|
||||
}
|
||||
H2 {
|
||||
font-weight: bold;
|
||||
font-size: 14pt;
|
||||
COLOR: #C0C0C0;
|
||||
}
|
||||
|
||||
H3 {
|
||||
font-weight: normal;
|
||||
font-size: 14pt;
|
||||
text-indent: +1em;
|
||||
}
|
||||
H3 {
|
||||
font-weight: normal;
|
||||
font-size: 14pt;
|
||||
text-indent: +1em;
|
||||
}
|
||||
|
||||
H4 {
|
||||
font-size: 10pt;
|
||||
font-weight: normal;
|
||||
}
|
||||
H4 {
|
||||
font-size: 10pt;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
H5 {
|
||||
font-size: 10pt;
|
||||
font-weight: normal;
|
||||
background: #A9A9A9;
|
||||
COLOR: white;
|
||||
display: inline;
|
||||
}
|
||||
H5 {
|
||||
font-size: 10pt;
|
||||
font-weight: normal;
|
||||
background: #A9A9A9;
|
||||
COLOR: white;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
H6 {
|
||||
padding: 2 1 2 5;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
H6 {
|
||||
padding: 2 1 2 5;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
UL {
|
||||
line-height: 1.5em;
|
||||
list-style-type: disc;
|
||||
}
|
||||
UL {
|
||||
line-height: 1.5em;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
OL {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
OL {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
LI {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
LI {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
A IMG {
|
||||
border: 0;
|
||||
}
|
||||
A IMG {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
table.gridtable {
|
||||
color: #333333;
|
||||
border-width: 0.1pt;
|
||||
border-color: #666666;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.gridtable {
|
||||
color: #333333;
|
||||
border-width: 0.1pt;
|
||||
border-color: #666666;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table.gridtable th {
|
||||
border-width: 0.1pt;
|
||||
padding: 8px;
|
||||
border-style: solid;
|
||||
border-color: #666666;
|
||||
background-color: #dedede;
|
||||
}
|
||||
table.gridtable tr {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
table.gridtable td {
|
||||
border-width: 0.1pt;
|
||||
padding: 8px;
|
||||
border-style: solid;
|
||||
border-color: #666666;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
</style>";
|
||||
table.gridtable th {
|
||||
border-width: 0.1pt;
|
||||
padding: 8px;
|
||||
border-style: solid;
|
||||
border-color: #666666;
|
||||
background-color: #dedede;
|
||||
}
|
||||
table.gridtable tr {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
table.gridtable td {
|
||||
border-width: 0.1pt;
|
||||
padding: 8px;
|
||||
border-style: solid;
|
||||
border-color: #666666;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
</style>";
|
||||
|
||||
string Html = @"<!DOCTYPE html>
|
||||
<html xmlns='http://www.w3.org/1999/xhtml'>
|
||||
<head>
|
||||
<meta http-equiv='X-UA-Compatible' content='IE=8, IE=9, IE=10' />
|
||||
<meta charset='utf-8'>
|
||||
<title>Jellyfin Reports Export</title>";
|
||||
Html += "\n" + Style + "\n";
|
||||
Html += "</head>\n";
|
||||
Html += "<body>\n";
|
||||
string Html = @"<!DOCTYPE html>
|
||||
<html xmlns='http://www.w3.org/1999/xhtml'>
|
||||
<head>
|
||||
<meta http-equiv='X-UA-Compatible' content='IE=8, IE=9, IE=10' />
|
||||
<meta charset='utf-8'>
|
||||
<title>Jellyfin Reports Export</title>";
|
||||
Html += "\n" + Style + "\n";
|
||||
Html += "</head>\n";
|
||||
Html += "<body>\n";
|
||||
|
||||
StringBuilder returnValue = new StringBuilder();
|
||||
returnValue.AppendLine("<table class='gridtable'>");
|
||||
returnValue.AppendLine("<tr>");
|
||||
foreach (var x in reportResult.Headers)
|
||||
{
|
||||
returnValue.Append("<th>")
|
||||
.Append(x.Name)
|
||||
.AppendLine("</th>");
|
||||
}
|
||||
StringBuilder returnValue = new StringBuilder();
|
||||
returnValue.AppendLine("<table class='gridtable'>");
|
||||
returnValue.AppendLine("<tr>");
|
||||
foreach (var x in reportResult.Headers)
|
||||
{
|
||||
returnValue.Append("<th>")
|
||||
.Append(x.Name)
|
||||
.AppendLine("</th>");
|
||||
}
|
||||
|
||||
returnValue.AppendLine("</tr>");
|
||||
returnValue.AppendLine("</tr>");
|
||||
|
||||
if (reportResult.IsGrouped)
|
||||
{
|
||||
foreach (ReportGroup group in reportResult.Groups)
|
||||
{
|
||||
returnValue.AppendLine("<tr>");
|
||||
returnValue.Append("<th scope='rowgroup' colspan='")
|
||||
.Append(reportResult.Headers.Count)
|
||||
.Append("'>")
|
||||
.Append(string.IsNullOrEmpty(group.Name) ? " " : group.Name)
|
||||
.AppendLine("</th>");
|
||||
returnValue.AppendLine("</tr>");
|
||||
ExportToExcelRows(returnValue, group.Rows);
|
||||
returnValue.AppendLine("<tr>");
|
||||
returnValue.Append("<th style='background-color: #ffffff;' scope='rowgroup' colspan='")
|
||||
.Append(reportResult.Headers.Count)
|
||||
.AppendLine("'>" + " " + "</th>");
|
||||
returnValue.AppendLine("</tr>");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ExportToExcelRows(returnValue, reportResult.Rows);
|
||||
}
|
||||
if (reportResult.IsGrouped)
|
||||
{
|
||||
foreach (ReportGroup group in reportResult.Groups)
|
||||
{
|
||||
returnValue.AppendLine("<tr>");
|
||||
returnValue.Append("<th scope='rowgroup' colspan='")
|
||||
.Append(reportResult.Headers.Count)
|
||||
.Append("'>")
|
||||
.Append(string.IsNullOrEmpty(group.Name) ? " " : group.Name)
|
||||
.AppendLine("</th>");
|
||||
returnValue.AppendLine("</tr>");
|
||||
ExportToExcelRows(returnValue, group.Rows);
|
||||
returnValue.AppendLine("<tr>");
|
||||
returnValue.Append("<th style='background-color: #ffffff;' scope='rowgroup' colspan='")
|
||||
.Append(reportResult.Headers.Count)
|
||||
.AppendLine("'>" + " " + "</th>");
|
||||
returnValue.AppendLine("</tr>");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ExportToExcelRows(returnValue, reportResult.Rows);
|
||||
}
|
||||
|
||||
returnValue.AppendLine("</table>");
|
||||
returnValue.AppendLine("</table>");
|
||||
|
||||
Html += returnValue.ToString();
|
||||
Html += "</body>";
|
||||
Html += "</html>";
|
||||
return Html;
|
||||
}
|
||||
Html += returnValue.ToString();
|
||||
Html += "</body>";
|
||||
Html += "</html>";
|
||||
return Html;
|
||||
}
|
||||
|
||||
private static void ExportToExcelRows(
|
||||
StringBuilder returnValue,
|
||||
List<ReportRow> rows)
|
||||
{
|
||||
foreach (var row in rows)
|
||||
{
|
||||
returnValue.AppendLine("<tr>");
|
||||
foreach (var x in row.Columns)
|
||||
{
|
||||
returnValue.Append("<td>")
|
||||
.Append(x.Name)
|
||||
.AppendLine("</td>");
|
||||
}
|
||||
private static void ExportToExcelRows(
|
||||
StringBuilder returnValue,
|
||||
List<ReportRow> rows)
|
||||
{
|
||||
foreach (var row in rows)
|
||||
{
|
||||
returnValue.AppendLine("<tr>");
|
||||
foreach (var x in row.Columns)
|
||||
{
|
||||
returnValue.Append("<td>")
|
||||
.Append(x.Name)
|
||||
.AppendLine("</td>");
|
||||
}
|
||||
|
||||
returnValue.AppendLine("</tr>");
|
||||
}
|
||||
}
|
||||
}
|
||||
returnValue.AppendLine("</tr>");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
#nullable disable
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -107,7 +107,7 @@ namespace Jellyfin.Plugin.Reports.Api
|
||||
ReportViewType reportViewType = ReportHelper.GetReportViewType(request.ReportView);
|
||||
var headers = new Dictionary<string, string>();
|
||||
string fileExtension = "csv";
|
||||
string contentType = "text/plain;charset='utf-8'";
|
||||
string contentType = "text/csv;charset='utf-8'";
|
||||
|
||||
switch (request.ExportType)
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user