├── .github ├── renovate.json ├── workflows │ ├── sync-labels.yaml │ ├── command-dispatch.yaml │ ├── test.yaml │ ├── build.yaml │ ├── publish-unstable.yaml │ ├── scan-codeql.yaml │ ├── changelog.yaml │ ├── publish.yaml │ └── command-rebase.yaml └── release-drafter.yml ├── Jellyfin.Plugin.Reports ├── Api │ ├── Common │ │ ├── ReportHeaderIdType.cs │ │ ├── ReportViewType.cs │ │ ├── ReportExportType.cs │ │ ├── ReportDisplayType.cs │ │ ├── ReportFieldType.cs │ │ ├── HeaderActivitiesMetadata.cs │ │ ├── ReportIncludeItemTypes.cs │ │ ├── ItemViewType.cs │ │ ├── HeaderMetadata.cs │ │ ├── ReportHelper.cs │ │ └── ReportBuilderBase.cs │ ├── Model │ │ ├── ReportItem.cs │ │ ├── ReportGroup.cs │ │ ├── ReportResult.cs │ │ ├── ReportHeader.cs │ │ └── ReportRow.cs │ ├── Data │ │ ├── ReportOptions.cs │ │ ├── ReportExport.cs │ │ └── ReportBuilder.cs │ ├── Activities │ │ └── ReportActivitiesBuilder.cs │ ├── ReportsService.cs │ └── ReportsController.cs ├── Configuration │ └── PluginConfiguration.cs ├── Jellyfin.Plugin.Reports.csproj ├── Plugin.cs └── Web │ └── reports.html ├── Directory.Build.props ├── LICENSE ├── Jellyfin.Plugin.Reports.sln ├── README.md ├── .gitignore ├── jellyfin.ruleset └── .editorconfig /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>jellyfin/.github//renovate-presets/default" 5 | ] 6 | } -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/Common/ReportHeaderIdType.cs: -------------------------------------------------------------------------------- 1 | namespace Jellyfin.Plugin.Reports.Api.Common 2 | { 3 | public enum ReportHeaderIdType 4 | { 5 | Row, 6 | Item 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/Common/ReportViewType.cs: -------------------------------------------------------------------------------- 1 | namespace Jellyfin.Plugin.Reports.Api.Common 2 | { 3 | public enum ReportViewType 4 | { 5 | ReportData, 6 | ReportActivities 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/Common/ReportExportType.cs: -------------------------------------------------------------------------------- 1 | namespace Jellyfin.Plugin.Reports.Api.Common 2 | { 3 | public enum ReportExportType 4 | { 5 | CSV, 6 | Excel, 7 | HTML 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18.0.0.0 4 | 18.0.0.0 5 | 18.0.0.0 6 | 7 | 8 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Configuration/PluginConfiguration.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Plugins; 2 | 3 | namespace Jellyfin.Plugin.Reports.Configuration 4 | { 5 | public class PluginConfiguration : BasePluginConfiguration 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/Common/ReportDisplayType.cs: -------------------------------------------------------------------------------- 1 | namespace Jellyfin.Plugin.Reports.Api.Common 2 | { 3 | public enum ReportDisplayType 4 | { 5 | None, 6 | Screen, 7 | Export, 8 | ScreenExport 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/sync-labels.yaml: -------------------------------------------------------------------------------- 1 | name: '🏷️ Sync labels' 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 1 * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | call: 10 | uses: jellyfin/jellyfin-meta-plugins/.github/workflows/sync-labels.yaml@master 11 | secrets: 12 | token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | _extends: jellyfin-meta-plugins 2 | 3 | template: | 4 | 5 | [Plugin build can be downloaded here](https://repo.jellyfin.org/releases/plugin/reports/reports_$NEXT_MAJOR_VERSION.0.0.0.zip). 6 | 7 | ## :sparkles: What's New 8 | 9 | $CHANGES 10 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/Common/ReportFieldType.cs: -------------------------------------------------------------------------------- 1 | namespace Jellyfin.Plugin.Reports.Api.Common 2 | { 3 | public enum ReportFieldType 4 | { 5 | String, 6 | Boolean, 7 | Date, 8 | Time, 9 | DateTime, 10 | Int, 11 | Image, 12 | Object, 13 | Minutes 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/command-dispatch.yaml: -------------------------------------------------------------------------------- 1 | # Allows for the definition of PR and Issue /commands 2 | name: '📟 Slash Command Dispatcher' 3 | 4 | on: 5 | issue_comment: 6 | types: 7 | - created 8 | 9 | jobs: 10 | call: 11 | uses: jellyfin/jellyfin-meta-plugins/.github/workflows/command-dispatch.yaml@master 12 | secrets: 13 | token: ${{ secrets.JF_BOT_TOKEN }} 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: '🧪 Test Plugin' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - '**/*.md' 9 | pull_request: 10 | branches: 11 | - master 12 | paths-ignore: 13 | - '**/*.md' 14 | workflow_dispatch: 15 | 16 | jobs: 17 | call: 18 | uses: jellyfin/jellyfin-meta-plugins/.github/workflows/test.yaml@master 19 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: '🏗️ Build Plugin' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - '**/*.md' 9 | pull_request: 10 | branches: 11 | - master 12 | paths-ignore: 13 | - '**/*.md' 14 | workflow_dispatch: 15 | 16 | jobs: 17 | call: 18 | uses: jellyfin/jellyfin-meta-plugins/.github/workflows/build.yaml@master 19 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/Common/HeaderActivitiesMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Jellyfin.Plugin.Reports.Api.Common 2 | { 3 | public enum HeaderActivitiesMetadata 4 | { 5 | None, 6 | Name, 7 | Overview, 8 | ShortOverview, 9 | Type, 10 | Date, 11 | UserPrimaryImageTag, 12 | Severity, 13 | Item, 14 | User 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/Common/ReportIncludeItemTypes.cs: -------------------------------------------------------------------------------- 1 | namespace Jellyfin.Plugin.Reports.Api.Common 2 | { 3 | public enum ReportIncludeItemTypes 4 | { 5 | MusicArtist, 6 | MusicAlbum, 7 | Book, 8 | BoxSet, 9 | Episode, 10 | Video, 11 | Movie, 12 | MusicVideo, 13 | Trailer, 14 | Season, 15 | Series, 16 | Audio, 17 | BaseItem, 18 | Artist 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/publish-unstable.yaml: -------------------------------------------------------------------------------- 1 | name: '🚀 Publish (Unstable) Plugin' 2 | 3 | on: 4 | push: 5 | branches: 6 | - unstable 7 | workflow_dispatch: 8 | 9 | jobs: 10 | call: 11 | uses: jellyfin/jellyfin-meta-plugins/.github/workflows/publish-unstable.yaml@master 12 | secrets: 13 | deploy-host: ${{ secrets.REPO_HOST }} 14 | deploy-user: ${{ secrets.REPO_USER }} 15 | deploy-key: ${{ secrets.REPO_KEY }} 16 | token: ${{ secrets.JF_BOT_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/scan-codeql.yaml: -------------------------------------------------------------------------------- 1 | name: '🔬 Run CodeQL' 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths-ignore: 7 | - '**/*.md' 8 | pull_request: 9 | branches: [ master ] 10 | paths-ignore: 11 | - '**/*.md' 12 | schedule: 13 | - cron: '24 2 * * 4' 14 | workflow_dispatch: 15 | 16 | jobs: 17 | call: 18 | uses: jellyfin/jellyfin-meta-plugins/.github/workflows/scan-codeql.yaml@master 19 | with: 20 | repository-name: jellyfin/jellyfin-plugin-reports 21 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yaml: -------------------------------------------------------------------------------- 1 | name: '📝 Create/Update Release Draft & Release Bump PR' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - build.yaml 9 | workflow_dispatch: 10 | repository_dispatch: 11 | types: 12 | - update-prep-command 13 | 14 | jobs: 15 | call: 16 | uses: jellyfin/jellyfin-meta-plugins/.github/workflows/changelog.yaml@master 17 | with: 18 | repository-name: jellyfin/jellyfin-plugin-reports 19 | secrets: 20 | token: ${{ secrets.JF_BOT_TOKEN }} 21 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/Common/ItemViewType.cs: -------------------------------------------------------------------------------- 1 | namespace Jellyfin.Plugin.Reports.Api.Common 2 | { 3 | public enum ItemViewType 4 | { 5 | None, 6 | Detail, 7 | Edit, 8 | List, 9 | ItemByNameDetails, 10 | StatusImage, 11 | EmbeddedImage, 12 | SubtitleImage, 13 | TrailersImage, 14 | SpecialsImage, 15 | LockDataImage, 16 | TagsPrimaryImage, 17 | TagsBackdropImage, 18 | TagsLogoImage, 19 | UserPrimaryImage 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: '🚀 Publish Plugin' 2 | 3 | on: 4 | release: 5 | types: 6 | - released 7 | workflow_dispatch: 8 | 9 | jobs: 10 | call: 11 | uses: jellyfin/jellyfin-meta-plugins/.github/workflows/publish.yaml@master 12 | with: 13 | version: ${{ github.event.release.tag_name }} 14 | is-unstable: ${{ github.event.release.prerelease }} 15 | secrets: 16 | deploy-host: ${{ secrets.REPO_HOST }} 17 | deploy-user: ${{ secrets.REPO_USER }} 18 | deploy-key: ${{ secrets.REPO_KEY }} 19 | -------------------------------------------------------------------------------- /.github/workflows/command-rebase.yaml: -------------------------------------------------------------------------------- 1 | name: '🔀 PR Rebase Command' 2 | 3 | on: 4 | repository_dispatch: 5 | types: 6 | - rebase-command 7 | 8 | jobs: 9 | call: 10 | uses: jellyfin/jellyfin-meta-plugins/.github/workflows/command-rebase.yaml@master 11 | with: 12 | rebase-head: ${{ github.event.client_payload.pull_request.head.label }} 13 | repository-full-name: ${{ github.event.client_payload.github.payload.repository.full_name }} 14 | comment-id: ${{ github.event.client_payload.github.payload.comment.id }} 15 | secrets: 16 | token: ${{ secrets.JF_BOT_TOKEN }} 17 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/Model/ReportItem.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | 3 | namespace Jellyfin.Plugin.Reports.Api.Model 4 | { 5 | /// A report item. 6 | public class ReportItem 7 | { 8 | /// Gets or sets the identifier. 9 | /// The identifier. 10 | public string Id { get; set; } 11 | 12 | /// Gets or sets the name. 13 | /// The name. 14 | public string Name { get; set; } 15 | 16 | public string Image { get; set; } 17 | 18 | /// Gets or sets the custom tag. 19 | /// The custom tag. 20 | public string CustomTag { get; set; } 21 | 22 | /// Returns a string that represents the current object. 23 | /// A string that represents the current object. 24 | /// 25 | public override string ToString() 26 | { 27 | return Name; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Jellyfin.Plugin.Reports.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | Jellyfin.Plugin.Reports 6 | true 7 | ../jellyfin.ruleset 8 | enable 9 | 10 | 11 | 12 | Recommended 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/Model/ReportGroup.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Jellyfin.Plugin.Reports.Api.Model 4 | { 5 | 6 | /// A report group. 7 | public class ReportGroup 8 | { 9 | /// 10 | /// Initializes a new instance of the MediaBrowser.Api.Reports.ReportGroup class. 11 | /// The rows. 12 | public ReportGroup(string name, List rows) 13 | { 14 | Name = name; 15 | Rows = rows; 16 | } 17 | 18 | /// Gets the name. 19 | /// The name. 20 | public string Name { get; } 21 | 22 | /// Gets the rows. 23 | /// The rows. 24 | public List Rows { get; } 25 | 26 | /// Returns a string that represents the current object. 27 | /// A string that represents the current object. 28 | /// 29 | public override string ToString() => Name; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) Media Browser http://mediabrowser.tv 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Plugin.Reports", "Jellyfin.Plugin.Reports\Jellyfin.Plugin.Reports.csproj", "{A2217228-D9FD-48E7-827A-B9302212FE38}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {A2217228-D9FD-48E7-827A-B9302212FE38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {A2217228-D9FD-48E7-827A-B9302212FE38}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {A2217228-D9FD-48E7-827A-B9302212FE38}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {A2217228-D9FD-48E7-827A-B9302212FE38}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {FC91FF5C-ADA7-45F9-AB2E-B0BB1CA4BFCF} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/Common/HeaderMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Jellyfin.Plugin.Reports.Api.Common 2 | { 3 | public enum HeaderMetadata 4 | { 5 | None, 6 | Path, 7 | Name, 8 | PremiereDate, 9 | DateAdded, 10 | ReleaseDate, 11 | Runtime, 12 | PlayCount, 13 | Season, 14 | SeasonNumber, 15 | Series, 16 | Network, 17 | Year, 18 | ParentalRating, 19 | CommunityRating, 20 | Trailers, 21 | Specials, 22 | AlbumArtist, 23 | Album, 24 | Disc, 25 | Track, 26 | Audio, 27 | EmbeddedImage, 28 | Video, 29 | Resolution, 30 | Subtitles, 31 | Genres, 32 | Countries, 33 | Status, 34 | Tracks, 35 | EpisodeSeries, 36 | EpisodeSeason, 37 | EpisodeNumber, 38 | AudioAlbumArtist, 39 | MusicArtist, 40 | AudioAlbum, 41 | Locked, 42 | ImagePrimary, 43 | ImageBackdrop, 44 | ImageLogo, 45 | Actor, 46 | Studios, 47 | Composer, 48 | Director, 49 | GuestStar, 50 | Producer, 51 | Writer, 52 | Artist, 53 | Years, 54 | ParentalRatings, 55 | CommunityRatings, 56 | 57 | //Activity logs 58 | Overview, 59 | ShortOverview, 60 | Type, 61 | Date, 62 | UserPrimaryImage, 63 | Severity, 64 | Item, 65 | User, 66 | UserId 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Plugin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Jellyfin.Plugin.Reports.Configuration; 4 | using MediaBrowser.Common.Configuration; 5 | using MediaBrowser.Common.Plugins; 6 | using MediaBrowser.Model.Plugins; 7 | using MediaBrowser.Model.Serialization; 8 | 9 | namespace Jellyfin.Plugin.Reports 10 | { 11 | public class Plugin : BasePlugin, IHasWebPages 12 | { 13 | public Plugin(IApplicationPaths appPaths, IXmlSerializer xmlSerializer) 14 | : base(appPaths, xmlSerializer) 15 | { 16 | } 17 | 18 | public override string Name => "Reports"; 19 | 20 | public override string Description => "Generate Reports"; 21 | 22 | public PluginConfiguration PluginConfiguration => Configuration; 23 | 24 | public override Guid Id => new Guid("d4312cd9-5c90-4f38-82e8-51da566790e8"); 25 | 26 | public IEnumerable GetPages() 27 | { 28 | return new PluginPageInfo[] 29 | { 30 | new PluginPageInfo 31 | { 32 | Name = "reports", 33 | EmbeddedResourcePath = GetType().Namespace + ".Web.reports.html", 34 | EnableInMainMenu = true 35 | }, 36 | new PluginPageInfo 37 | { 38 | Name = "reportsjs", 39 | EmbeddedResourcePath = GetType().Namespace + ".Web.reports.js" 40 | } 41 | }; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/Data/ReportOptions.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | 3 | using System; 4 | using Jellyfin.Plugin.Reports.Api.Model; 5 | 6 | namespace Jellyfin.Plugin.Reports.Api.Data 7 | { 8 | 9 | /// A report options. 10 | public class ReportOptions 11 | { 12 | /// Initializes a new instance of the ReportOptions class. 13 | public ReportOptions() 14 | { 15 | } 16 | 17 | /// Initializes a new instance of the ReportOptions class. 18 | /// . 19 | /// . 20 | public ReportOptions(ReportHeader header, Func column) 21 | { 22 | Header = header; 23 | Column = column; 24 | } 25 | 26 | /// 27 | /// Initializes a new instance of the ReportOptions class. 28 | /// 29 | /// 30 | /// 31 | /// 32 | public ReportOptions(ReportHeader header, Func column, Func itemID) 33 | { 34 | Header = header; 35 | Column = column; 36 | ItemID = itemID; 37 | } 38 | 39 | /// Gets or sets the header. 40 | /// The header. 41 | public ReportHeader Header { get; set; } 42 | 43 | /// Gets or sets the column. 44 | /// The column. 45 | public Func Column { get; set; } 46 | 47 | /// Gets or sets the identifier of the item. 48 | /// The identifier of the item. 49 | public Func ItemID { get; set; } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/Model/ReportResult.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | 3 | using System.Collections.Generic; 4 | 5 | namespace Jellyfin.Plugin.Reports.Api.Model 6 | { 7 | 8 | /// Encapsulates the result of a report. 9 | public class ReportResult 10 | { 11 | /// 12 | /// Initializes a new instance of the MediaBrowser.Api.Reports.ReportResult class. 13 | public ReportResult() 14 | { 15 | Rows = new List(); 16 | Headers = new List(); 17 | Groups = new List(); 18 | TotalRecordCount = 0; 19 | IsGrouped = false; 20 | } 21 | 22 | /// 23 | /// Initializes a new instance of the MediaBrowser.Api.Reports.ReportResult class. 24 | /// The headers. 25 | /// The rows. 26 | public ReportResult(List headers, List rows) 27 | { 28 | Rows = rows; 29 | Headers = headers; 30 | TotalRecordCount = 0; 31 | } 32 | 33 | /// Gets or sets the rows. 34 | /// The rows. 35 | public List Rows { get; set; } 36 | 37 | /// Gets or sets the headers. 38 | /// The headers. 39 | public List Headers { get; set; } 40 | 41 | /// Gets or sets the groups. 42 | /// The groups. 43 | public List Groups { get; set; } 44 | 45 | 46 | /// Gets or sets the number of total records. 47 | /// The total number of record count. 48 | public int TotalRecordCount { get; set; } 49 | 50 | /// Gets or sets the is grouped. 51 | /// The is grouped. 52 | public bool IsGrouped { get; set; } 53 | 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Jellyfin Reports Plugin

2 |

Part of the Jellyfin Project

3 | 4 |

5 | Plugin Banner 6 |
7 |
8 | 9 | GitHub Workflow Status 10 | 11 | 12 | MIT License 13 | 14 | 15 | Current Release 16 | 17 |

18 | 19 | ## About 20 | 21 | This plugin generates activity and media reports for your library. 22 | 23 | These reports can be exported to Excel and CSV formats. 24 | 25 | ## Installation 26 | 27 | [See the official documentation for install instructions](https://jellyfin.org/docs/general/server/plugins/index.html#installing). 28 | 29 | ## Build 30 | 31 | 1. To build this plugin you will need [.Net 8.x](https://dotnet.microsoft.com/download/dotnet/8.0). 32 | 33 | 2. Build plugin with following command 34 | ``` 35 | dotnet publish --configuration Release --output bin 36 | ``` 37 | 38 | 3. Place the dll-file in the `plugins/reports` folder (you might need to create the folders) of your JF install 39 | 40 | ## Releasing 41 | 42 | To release the plugin we recommend [JPRM](https://github.com/oddstr13/jellyfin-plugin-repository-manager) that will build and package the plugin. 43 | For additional context and for how to add the packaged plugin zip to a plugin manifest see the [JPRM documentation](https://github.com/oddstr13/jellyfin-plugin-repository-manager) for more info. 44 | 45 | ## Contributing 46 | 47 | We welcome all contributions and pull requests! If you have a larger feature in mind please open an issue so we can discuss the implementation before you start. 48 | In general refer to our [contributing guidelines](https://github.com/jellyfin/.github/blob/master/CONTRIBUTING.md) for further information. 49 | 50 | ## Licence 51 | 52 | This plugins code and packages are distributed under the MIT License. See [LICENSE](./LICENSE) for more information. 53 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/Model/ReportHeader.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | 3 | using Jellyfin.Plugin.Reports.Api.Common; 4 | 5 | namespace Jellyfin.Plugin.Reports.Api.Model 6 | { 7 | /// A report header. 8 | public class ReportHeader 9 | { 10 | /// Initializes a new instance of the ReportHeader class. 11 | public ReportHeader() 12 | { 13 | ItemViewType = ItemViewType.None; 14 | Visible = true; 15 | CanGroup = true; 16 | ShowHeaderLabel = true; 17 | DisplayType = ReportDisplayType.ScreenExport; 18 | } 19 | 20 | /// Gets or sets the type of the header field. 21 | /// The type of the header field. 22 | public ReportFieldType HeaderFieldType { get; set; } 23 | 24 | /// Gets or sets the name of the header. 25 | /// The name of the header. 26 | public string Name { get; set; } 27 | 28 | /// Gets or sets the name of the field. 29 | /// The name of the field. 30 | public HeaderMetadata FieldName { get; set; } 31 | 32 | /// Gets or sets the sort field. 33 | /// The sort field. 34 | public string SortField { get; set; } 35 | 36 | /// Gets or sets the type. 37 | /// The type. 38 | public string Type { get; set; } 39 | 40 | /// Gets or sets the type of the item view. 41 | /// The type of the item view. 42 | public ItemViewType ItemViewType { get; set; } 43 | 44 | /// Gets or sets a value indicating whether this object is visible. 45 | /// true if visible, false if not. 46 | public bool Visible { get; set; } 47 | 48 | /// Gets or sets the type of the display. 49 | /// The type of the display. 50 | public ReportDisplayType DisplayType { get; set; } 51 | 52 | /// Gets or sets a value indicating whether the header label is shown. 53 | /// true if show header label, false if not. 54 | public bool ShowHeaderLabel { get; set; } 55 | 56 | /// Gets or sets a value indicating whether we can group. 57 | /// true if we can group, false if not. 58 | public bool CanGroup { get; set; } 59 | 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/Model/ReportRow.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | 3 | using System.Collections.Generic; 4 | using System; 5 | using Jellyfin.Plugin.Reports.Api.Common; 6 | 7 | namespace Jellyfin.Plugin.Reports.Api.Model 8 | { 9 | public class ReportRow 10 | { 11 | /// 12 | /// Initializes a new instance of the ReportRow class. 13 | /// 14 | public ReportRow() 15 | { 16 | Columns = new List(); 17 | } 18 | 19 | /// Gets or sets the identifier. 20 | /// The identifier. 21 | public string Id { get; set; } 22 | 23 | /// 24 | /// Gets or sets a value indicating whether this object has backdrop image. 25 | /// true if this object has backdrop image, false if not. 26 | public bool HasImageTagsBackdrop { get; set; } 27 | 28 | /// Gets or sets a value indicating whether this object has image tags. 29 | /// true if this object has image tags, false if not. 30 | public bool HasImageTagsPrimary { get; set; } 31 | 32 | /// 33 | /// Gets or sets a value indicating whether this object has image tags logo. 34 | /// true if this object has image tags logo, false if not. 35 | public bool HasImageTagsLogo { get; set; } 36 | 37 | /// 38 | /// Gets or sets a value indicating whether this object has local trailer. 39 | /// true if this object has local trailer, false if not. 40 | public bool HasLocalTrailer { get; set; } 41 | 42 | /// Gets or sets a value indicating whether this object has lock data. 43 | /// true if this object has lock data, false if not. 44 | public bool HasLockData { get; set; } 45 | 46 | /// 47 | /// Gets or sets a value indicating whether this object has embedded image. 48 | /// true if this object has embedded image, false if not. 49 | public bool HasEmbeddedImage { get; set; } 50 | 51 | /// Gets or sets a value indicating whether this object has subtitles. 52 | /// true if this object has subtitles, false if not. 53 | public bool HasSubtitles { get; set; } 54 | 55 | /// Gets or sets a value indicating whether this object has specials. 56 | /// true if this object has specials, false if not. 57 | public bool HasSpecials { get; set; } 58 | 59 | /// Gets or sets the columns. 60 | /// The columns. 61 | public List Columns { get; set; } 62 | 63 | /// Gets or sets the type. 64 | /// The type. 65 | public ReportIncludeItemTypes RowType { get; set; } 66 | 67 | /// Gets or sets the identifier of the user. 68 | /// The identifier of the user. 69 | public Guid UserId { get; set; } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | ################# 32 | ## Media Browser 33 | ################# 34 | ProgramData*/ 35 | ProgramData-Server*/ 36 | ProgramData-UI*/ 37 | 38 | ################# 39 | ## Visual Studio 40 | ################# 41 | 42 | .vs 43 | 44 | ## Ignore Visual Studio temporary files, build results, and 45 | ## files generated by popular Visual Studio add-ons. 46 | 47 | # User-specific files 48 | *.suo 49 | *.user 50 | *.sln.docstates 51 | 52 | # Build results 53 | 54 | [Dd]ebug/ 55 | [Rr]elease/ 56 | build/ 57 | [Bb]in/ 58 | [Oo]bj/ 59 | 60 | # MSTest test Results 61 | [Tt]est[Rr]esult*/ 62 | [Bb]uild[Ll]og.* 63 | 64 | *_i.c 65 | *_p.c 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.pch 70 | *.pdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.log 86 | *.scc 87 | *.scc 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.orig 92 | *.rej 93 | *.sdf 94 | *.opensdf 95 | *.ipch 96 | 97 | # Visual C++ cache files 98 | ipch/ 99 | *.aps 100 | *.ncb 101 | *.opensdf 102 | *.sdf 103 | *.cachefile 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | 110 | # Guidance Automation Toolkit 111 | *.gpState 112 | 113 | # ReSharper is a .NET coding add-in 114 | _ReSharper*/ 115 | *.[Rr]e[Ss]harper 116 | 117 | # TeamCity is a build add-in 118 | _TeamCity* 119 | 120 | # DotCover is a Code Coverage Tool 121 | *.dotCover 122 | 123 | # NCrunch 124 | *.ncrunch* 125 | .*crunch*.local.xml 126 | 127 | # Installshield output folder 128 | [Ee]xpress/ 129 | 130 | # DocProject is a documentation generator add-in 131 | DocProject/buildhelp/ 132 | DocProject/Help/*.HxT 133 | DocProject/Help/*.HxC 134 | DocProject/Help/*.hhc 135 | DocProject/Help/*.hhk 136 | DocProject/Help/*.hhp 137 | DocProject/Help/Html2 138 | DocProject/Help/html 139 | 140 | # Click-Once directory 141 | publish/ 142 | 143 | # Publish Web Output 144 | *.Publish.xml 145 | *.pubxml 146 | 147 | # NuGet Packages Directory 148 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 149 | packages/ 150 | 151 | # Windows Azure Build Output 152 | csx 153 | *.build.csdef 154 | 155 | # Windows Store app package directory 156 | AppPackages/ 157 | 158 | # Others 159 | sql/ 160 | *.Cache 161 | ClientBin/ 162 | [Ss]tyle[Cc]op.* 163 | ~$* 164 | *~ 165 | *.dbmdl 166 | *.[Pp]ublish.xml 167 | *.publishsettings 168 | 169 | # RIA/Silverlight projects 170 | Generated_Code/ 171 | 172 | # Backup & report files from converting an old project file to a newer 173 | # Visual Studio version. Backup files are not needed, because we have git ;-) 174 | _UpgradeReport_Files/ 175 | Backup*/ 176 | UpgradeLog*.XML 177 | UpgradeLog*.htm 178 | 179 | # SQL Server files 180 | App_Data/*.mdf 181 | App_Data/*.ldf 182 | 183 | ############# 184 | ## Windows detritus 185 | ############# 186 | 187 | # Windows image file caches 188 | Thumbs.db 189 | ehthumbs.db 190 | 191 | # Folder config file 192 | Desktop.ini 193 | 194 | # Recycle Bin used on file shares 195 | $RECYCLE.BIN/ 196 | 197 | # Mac crap 198 | .DS_Store 199 | 200 | 201 | ############# 202 | ## Python 203 | ############# 204 | 205 | *.py[co] 206 | 207 | # Packages 208 | *.egg 209 | *.egg-info 210 | dist/ 211 | build/ 212 | eggs/ 213 | parts/ 214 | var/ 215 | sdist/ 216 | develop-eggs/ 217 | .installed.cfg 218 | 219 | # Installer logs 220 | pip-log.txt 221 | 222 | # Unit test / coverage reports 223 | .coverage 224 | .tox 225 | 226 | #Translations 227 | *.mo 228 | 229 | #Mr Developer 230 | .mr.developer.cfg 231 | 232 | # Rider 233 | .idea 234 | artifacts 235 | .idea 236 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/Common/ReportHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | 6 | namespace Jellyfin.Plugin.Reports.Api.Common 7 | { 8 | /// A report helper. 9 | public class ReportHelper 10 | { 11 | /// Convert field to string. 12 | /// Generic type parameter. 13 | /// The value. 14 | /// Type of the field. 15 | /// The field converted to string. 16 | public static string? ConvertToString(T value, ReportFieldType fieldType) 17 | { 18 | if (value == null) 19 | { 20 | return string.Empty; 21 | } 22 | 23 | return fieldType switch 24 | { 25 | ReportFieldType.Boolean | ReportFieldType.Int | ReportFieldType.String => value.ToString(), 26 | ReportFieldType.Date => string.Format(CultureInfo.InvariantCulture, "{0:d}", value), 27 | ReportFieldType.Time => string.Format(CultureInfo.InvariantCulture, "{0:t}", value), 28 | ReportFieldType.DateTime => string.Format(CultureInfo.InvariantCulture, "{0:g}", value), 29 | ReportFieldType.Minutes => string.Format(CultureInfo.InvariantCulture, "{0}mn", value), 30 | _ when value is Guid guid => guid.ToString("N", CultureInfo.InvariantCulture), 31 | _ => value.ToString() 32 | }; 33 | } 34 | 35 | /// Gets filtered report header metadata. 36 | /// The report columns. 37 | /// The default return value. 38 | /// The filtered report header metadata. 39 | public static List GetFilteredReportHeaderMetadata(string reportColumns, Func>? defaultReturnValue = null) 40 | { 41 | if (!string.IsNullOrEmpty(reportColumns)) 42 | { 43 | var s = reportColumns.Split('|').Select(x => ReportHelper.GetHeaderMetadataType(x)).Where(x => x != HeaderMetadata.None); 44 | return s.ToList(); 45 | } 46 | 47 | if (defaultReturnValue == null) 48 | { 49 | return new List(); 50 | } 51 | 52 | return defaultReturnValue(); 53 | } 54 | 55 | /// Gets header metadata type. 56 | /// The header. 57 | /// The header metadata type. 58 | public static HeaderMetadata GetHeaderMetadataType(string header) 59 | { 60 | if (string.IsNullOrEmpty(header)) 61 | return HeaderMetadata.None; 62 | 63 | HeaderMetadata rType; 64 | 65 | if (!Enum.TryParse(header, out rType)) 66 | return HeaderMetadata.None; 67 | 68 | return rType; 69 | } 70 | 71 | /// Gets report view type. 72 | /// The type. 73 | /// The report view type. 74 | public static ReportViewType GetReportViewType(string rowType) 75 | { 76 | if (string.IsNullOrEmpty(rowType)) 77 | return ReportViewType.ReportData; 78 | 79 | ReportViewType rType; 80 | 81 | if (!Enum.TryParse(rowType, out rType)) 82 | return ReportViewType.ReportData; 83 | 84 | return rType; 85 | } 86 | 87 | /// Gets row type. 88 | /// The type. 89 | /// The row type. 90 | public static ReportIncludeItemTypes GetRowType(string rowType) 91 | { 92 | if (string.IsNullOrEmpty(rowType)) 93 | return ReportIncludeItemTypes.BaseItem; 94 | 95 | ReportIncludeItemTypes rType; 96 | 97 | if (!Enum.TryParse(rowType, out rType)) 98 | return ReportIncludeItemTypes.BaseItem; 99 | 100 | return rType; 101 | } 102 | 103 | /// Gets report display type. 104 | /// Type of the display. 105 | /// The report display type. 106 | public static ReportDisplayType GetReportDisplayType(string displayType) 107 | { 108 | if (string.IsNullOrEmpty(displayType)) 109 | return ReportDisplayType.ScreenExport; 110 | 111 | ReportDisplayType rType; 112 | 113 | if (!Enum.TryParse(displayType, out rType)) 114 | return ReportDisplayType.ScreenExport; 115 | 116 | return rType; 117 | } 118 | 119 | /// Gets core localized string. 120 | /// The phrase. 121 | /// The core localized string. 122 | public static string GetCoreLocalizedString(string phrase) 123 | { 124 | return phrase; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /jellyfin.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/Data/ReportExport.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Net; 7 | using System.Reflection; 8 | using Jellyfin.Plugin.Reports.Api.Model; 9 | using ClosedXML.Excel; 10 | 11 | namespace Jellyfin.Plugin.Reports.Api.Data 12 | { 13 | /// A report export. 14 | public static class ReportExport 15 | { 16 | /// Export to CSV. 17 | /// The report result. 18 | /// A MemoryStream containing a CSV file. 19 | public static MemoryStream ExportToCsv(ReportResult reportResult) 20 | { 21 | static string EscapeText(string text) 22 | { 23 | string escapedText = text.Replace("\"", "\"\"", System.StringComparison.Ordinal); 24 | return text.IndexOfAny(['"', ',', '\n', '\r']) == -1 ? escapedText : $"\"{escapedText}\""; 25 | } 26 | static void AppendRows(StreamWriter writer, List rows) 27 | { 28 | foreach (ReportRow row in rows) 29 | { 30 | writer.WriteLine(string.Join(',', row.Columns.Select(s => EscapeText(s.Name)))); 31 | } 32 | } 33 | 34 | MemoryStream memoryStream = new MemoryStream(); 35 | using (StreamWriter writer = new StreamWriter(memoryStream, leaveOpen:true)) 36 | { 37 | writer.WriteLine(string.Join(',', reportResult.Headers.Select(s => EscapeText(s.Name)))); 38 | 39 | if (reportResult.IsGrouped) 40 | { 41 | foreach (ReportGroup group in reportResult.Groups) 42 | { 43 | AppendRows(writer, group.Rows); 44 | } 45 | } 46 | else 47 | { 48 | AppendRows(writer, reportResult.Rows); 49 | } 50 | } 51 | memoryStream.Position = 0; 52 | return memoryStream; 53 | } 54 | 55 | 56 | /// Export to HTML. 57 | /// The report result. 58 | /// A MemoryStream containing a HTML file. 59 | public static MemoryStream ExportToHtml(ReportResult reportResult) 60 | { 61 | static void ExportToHtmlRows(StreamWriter writer, List rows) 62 | { 63 | foreach (ReportRow row in rows) 64 | { 65 | writer.Write(""); 66 | foreach (ReportItem x in row.Columns) 67 | { 68 | writer.Write($"{WebUtility.HtmlEncode(x.Name)}"); 69 | } 70 | writer.Write(""); 71 | } 72 | } 73 | 74 | const string Html = @" 75 | 76 | 77 | 78 | Jellyfin Reports Export 79 | 103 | 104 | "; 105 | 106 | MemoryStream memoryStream = new MemoryStream(); 107 | using (StreamWriter writer = new StreamWriter(memoryStream, leaveOpen: true)) 108 | { 109 | writer.Write(Html); 110 | writer.Write(""); 111 | foreach (ReportHeader x in reportResult.Headers) 112 | { 113 | writer.Write($""); 114 | } 115 | writer.Write(""); 116 | 117 | if (reportResult.IsGrouped) 118 | { 119 | foreach (ReportGroup group in reportResult.Groups) 120 | { 121 | string groupName = string.IsNullOrEmpty(group.Name) ? " " : WebUtility.HtmlEncode(group.Name); 122 | writer.Write($""); 123 | ExportToHtmlRows(writer, group.Rows); 124 | writer.Write($""); 125 | } 126 | } 127 | else 128 | { 129 | ExportToHtmlRows(writer, reportResult.Rows); 130 | } 131 | writer.Write("
{WebUtility.HtmlEncode(x.Name)}
{groupName}
 
"); 132 | } 133 | memoryStream.Position = 0; 134 | return memoryStream; 135 | } 136 | 137 | /// Export to Excel. 138 | /// The report result. 139 | /// A MemoryStream containing a XLSX file. 140 | public static MemoryStream ExportToExcel(ReportResult reportResult) 141 | { 142 | static void AddHeaderStyle(IXLRange range) 143 | { 144 | range.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; 145 | range.Style.Font.Bold = true; 146 | range.Style.Fill.BackgroundColor = XLColor.FromArgb(222, 222, 222); 147 | } 148 | 149 | static void AddReportRows(IXLWorksheet worksheet, List reportRows, ref int nextRow) 150 | { 151 | IEnumerable rows = reportRows.Select(r => r.Columns.Select(s => s.Name).ToArray()); 152 | worksheet.Cell(nextRow, 1).InsertData(rows); 153 | nextRow += rows.Count(); 154 | } 155 | 156 | using var workbook = new XLWorkbook(); 157 | IXLWorksheet worksheet = workbook.Worksheets.Add("ReportExport"); 158 | 159 | // Add report rows 160 | int nextRow = 1; 161 | IEnumerable headers = reportResult.Headers.Select(s => s.Name); 162 | IXLRange headerRange = worksheet.Cell(nextRow++, 1).InsertData(headers, true); 163 | AddHeaderStyle(headerRange); 164 | if (reportResult.IsGrouped) 165 | { 166 | foreach (ReportGroup group in reportResult.Groups) 167 | { 168 | int groupHeaderRow = nextRow++; 169 | worksheet.Cell(groupHeaderRow, 1).Value = group.Name; 170 | AddHeaderStyle(worksheet.Cell(groupHeaderRow, 1).AsRange()); 171 | worksheet.Range(groupHeaderRow, 1, groupHeaderRow, reportResult.Headers.Count).Merge(); 172 | AddReportRows(worksheet, group.Rows, ref nextRow); 173 | worksheet.Rows(groupHeaderRow + 1, nextRow - 1).Group(); 174 | } 175 | } 176 | else 177 | { 178 | AddReportRows(worksheet, reportResult.Rows, ref nextRow); 179 | } 180 | 181 | // Sheet properties 182 | worksheet.Style.Font.FontColor = XLColor.FromArgb(51, 51, 51); 183 | worksheet.Style.Font.FontName = "Arial"; 184 | worksheet.Style.Font.FontSize = 9; 185 | worksheet.ShowGridLines = false; 186 | worksheet.SheetView.FreezeRows(1); 187 | worksheet.Outline.SummaryVLocation = XLOutlineSummaryVLocation.Top; 188 | worksheet.RangeUsed().Style.Border.InsideBorder = XLBorderStyleValues.Thin; 189 | worksheet.RangeUsed().Style.Border.OutsideBorder = XLBorderStyleValues.Thin; 190 | //worksheet.ColumnsUsed().AdjustToContents(10.0, 50.0); 191 | 192 | // Workbook properties 193 | workbook.Properties.Author = "Jellyfin"; 194 | workbook.Properties.Title = "ReportExport"; 195 | string pluginVer = Assembly.GetExecutingAssembly().GetName().Version.ToString(); 196 | workbook.Properties.Comments = $"Produced by Jellyfin Reports Plugin {pluginVer}"; 197 | 198 | // Save workbook to stream and return 199 | MemoryStream memoryStream = new MemoryStream(); 200 | workbook.SaveAs(memoryStream); 201 | memoryStream.Position = 0; 202 | return memoryStream; 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # With more recent updates Visual Studio 2017 supports EditorConfig files out of the box 2 | # Visual Studio Code needs an extension: https://github.com/editorconfig/editorconfig-vscode 3 | # For emacs, vim, np++ and other editors, see here: https://github.com/editorconfig 4 | ############################### 5 | # Core EditorConfig Options # 6 | ############################### 7 | root = true 8 | # All files 9 | [*] 10 | indent_style = space 11 | indent_size = 4 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | insert_final_newline = true 15 | end_of_line = lf 16 | max_line_length = off 17 | 18 | # YAML indentation 19 | [*.{yml,yaml}] 20 | indent_size = 2 21 | 22 | # XML indentation 23 | [*.{csproj,xml}] 24 | indent_size = 2 25 | 26 | ############################### 27 | # .NET Coding Conventions # 28 | ############################### 29 | [*.{cs,vb}] 30 | # Organize usings 31 | dotnet_sort_system_directives_first = true 32 | # this. preferences 33 | dotnet_style_qualification_for_field = false:silent 34 | dotnet_style_qualification_for_property = false:silent 35 | dotnet_style_qualification_for_method = false:silent 36 | dotnet_style_qualification_for_event = false:silent 37 | # Language keywords vs BCL types preferences 38 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 39 | dotnet_style_predefined_type_for_member_access = true:silent 40 | # Parentheses preferences 41 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 42 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 43 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 44 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 45 | # Modifier preferences 46 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 47 | dotnet_style_readonly_field = true:suggestion 48 | # Expression-level preferences 49 | dotnet_style_object_initializer = true:suggestion 50 | dotnet_style_collection_initializer = true:suggestion 51 | dotnet_style_explicit_tuple_names = true:suggestion 52 | dotnet_style_null_propagation = true:suggestion 53 | dotnet_style_coalesce_expression = true:suggestion 54 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent 55 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 56 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 57 | dotnet_style_prefer_auto_properties = true:silent 58 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 59 | dotnet_style_prefer_conditional_expression_over_return = true:silent 60 | 61 | ############################### 62 | # Naming Conventions # 63 | ############################### 64 | # Style Definitions (From Roslyn) 65 | 66 | # Non-private static fields are PascalCase 67 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion 68 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields 69 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style 70 | 71 | dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field 72 | dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected 73 | dotnet_naming_symbols.non_private_static_fields.required_modifiers = static 74 | 75 | dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case 76 | 77 | # Constants are PascalCase 78 | dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion 79 | dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants 80 | dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style 81 | 82 | dotnet_naming_symbols.constants.applicable_kinds = field, local 83 | dotnet_naming_symbols.constants.required_modifiers = const 84 | 85 | dotnet_naming_style.constant_style.capitalization = pascal_case 86 | 87 | # Static fields are camelCase and start with s_ 88 | dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion 89 | dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields 90 | dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style 91 | 92 | dotnet_naming_symbols.static_fields.applicable_kinds = field 93 | dotnet_naming_symbols.static_fields.required_modifiers = static 94 | 95 | dotnet_naming_style.static_field_style.capitalization = camel_case 96 | dotnet_naming_style.static_field_style.required_prefix = _ 97 | 98 | # Instance fields are camelCase and start with _ 99 | dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion 100 | dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields 101 | dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style 102 | 103 | dotnet_naming_symbols.instance_fields.applicable_kinds = field 104 | 105 | dotnet_naming_style.instance_field_style.capitalization = camel_case 106 | dotnet_naming_style.instance_field_style.required_prefix = _ 107 | 108 | # Locals and parameters are camelCase 109 | dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion 110 | dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters 111 | dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style 112 | 113 | dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local 114 | 115 | dotnet_naming_style.camel_case_style.capitalization = camel_case 116 | 117 | # Local functions are PascalCase 118 | dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion 119 | dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions 120 | dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style 121 | 122 | dotnet_naming_symbols.local_functions.applicable_kinds = local_function 123 | 124 | dotnet_naming_style.local_function_style.capitalization = pascal_case 125 | 126 | # By default, name items with PascalCase 127 | dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion 128 | dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members 129 | dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style 130 | 131 | dotnet_naming_symbols.all_members.applicable_kinds = * 132 | 133 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 134 | 135 | ############################### 136 | # C# Coding Conventions # 137 | ############################### 138 | [*.cs] 139 | # var preferences 140 | csharp_style_var_for_built_in_types = true:silent 141 | csharp_style_var_when_type_is_apparent = true:silent 142 | csharp_style_var_elsewhere = true:silent 143 | # Expression-bodied members 144 | csharp_style_expression_bodied_methods = false:silent 145 | csharp_style_expression_bodied_constructors = false:silent 146 | csharp_style_expression_bodied_operators = false:silent 147 | csharp_style_expression_bodied_properties = true:silent 148 | csharp_style_expression_bodied_indexers = true:silent 149 | csharp_style_expression_bodied_accessors = true:silent 150 | # Pattern matching preferences 151 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 152 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 153 | # Null-checking preferences 154 | csharp_style_throw_expression = true:suggestion 155 | csharp_style_conditional_delegate_call = true:suggestion 156 | # Modifier preferences 157 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 158 | # Expression-level preferences 159 | csharp_prefer_braces = true:silent 160 | csharp_style_deconstructed_variable_declaration = true:suggestion 161 | csharp_prefer_simple_default_expression = true:suggestion 162 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 163 | csharp_style_inlined_variable_declaration = true:suggestion 164 | 165 | ############################### 166 | # C# Formatting Rules # 167 | ############################### 168 | # New line preferences 169 | csharp_new_line_before_open_brace = all 170 | csharp_new_line_before_else = true 171 | csharp_new_line_before_catch = true 172 | csharp_new_line_before_finally = true 173 | csharp_new_line_before_members_in_object_initializers = true 174 | csharp_new_line_before_members_in_anonymous_types = true 175 | csharp_new_line_between_query_expression_clauses = true 176 | # Indentation preferences 177 | csharp_indent_case_contents = true 178 | csharp_indent_switch_labels = true 179 | csharp_indent_labels = flush_left 180 | # Space preferences 181 | csharp_space_after_cast = false 182 | csharp_space_after_keywords_in_control_flow_statements = true 183 | csharp_space_between_method_call_parameter_list_parentheses = false 184 | csharp_space_between_method_declaration_parameter_list_parentheses = false 185 | csharp_space_between_parentheses = false 186 | csharp_space_before_colon_in_inheritance_clause = true 187 | csharp_space_after_colon_in_inheritance_clause = true 188 | csharp_space_around_binary_operators = before_and_after 189 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 190 | csharp_space_between_method_call_name_and_opening_parenthesis = false 191 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 192 | # Wrapping preferences 193 | csharp_preserve_single_line_statements = true 194 | csharp_preserve_single_line_blocks = true 195 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/Activities/ReportActivitiesBuilder.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Globalization; 6 | using System.Linq; 7 | using Jellyfin.Database.Implementations.Entities; 8 | using Jellyfin.Plugin.Reports.Api.Common; 9 | using Jellyfin.Plugin.Reports.Api.Data; 10 | using Jellyfin.Plugin.Reports.Api.Model; 11 | using MediaBrowser.Controller.Library; 12 | using MediaBrowser.Model.Activity; 13 | using MediaBrowser.Model.Querying; 14 | 15 | namespace Jellyfin.Plugin.Reports.Api.Activities 16 | { 17 | /// A report activities builder. 18 | /// 19 | public class ReportActivitiesBuilder : ReportBuilderBase 20 | { 21 | /// 22 | /// Initializes a new instance of the MediaBrowser.Api.Reports.ReportActivitiesBuilder class. 23 | /// Manager for library. 24 | /// Manager for user. 25 | public ReportActivitiesBuilder(ILibraryManager libraryManager, IUserManager userManager) 26 | : base(libraryManager) 27 | { 28 | _userManager = userManager; 29 | } 30 | 31 | private readonly IUserManager _userManager; ///< Manager for user 32 | 33 | /// Gets a result. 34 | /// The query result. 35 | /// The request. 36 | /// The result. 37 | public ReportResult GetResult(QueryResult queryResult, IReportsQuery request) 38 | { 39 | ReportDisplayType displayType = ReportHelper.GetReportDisplayType(request.DisplayType); 40 | List> options = this.GetReportOptions(request, 41 | () => this.GetDefaultHeaderMetadata(), 42 | (hm) => this.GetOption(hm)).Where(x => this.DisplayTypeVisible(x.Header.DisplayType, displayType)).ToList(); 43 | 44 | var headers = GetHeaders(options); 45 | var rows = GetReportRows(queryResult.Items, options); 46 | 47 | ReportResult result = new ReportResult { Headers = headers }; 48 | HeaderMetadata groupBy = ReportHelper.GetHeaderMetadataType(request.GroupBy); 49 | int i = headers.FindIndex(x => x.FieldName == groupBy); 50 | if (groupBy != HeaderMetadata.None && i >= 0) 51 | { 52 | var rowsGroup = rows.SelectMany(x => x.Columns[i].Name.Split(';'), (x, g) => new { Group = g.Trim(), Rows = x }) 53 | .GroupBy(x => x.Group) 54 | .OrderBy(x => x.Key) 55 | .Select(x => new ReportGroup(x.Key, x.Select(r => r.Rows).ToList())); 56 | 57 | result.Groups = rowsGroup.ToList(); 58 | result.IsGrouped = true; 59 | } 60 | else 61 | { 62 | result.Rows = rows; 63 | result.IsGrouped = false; 64 | } 65 | 66 | return result; 67 | } 68 | 69 | /// Gets the headers. 70 | /// Type of the header. 71 | /// The request. 72 | /// The headers. 73 | /// 74 | protected internal override List GetHeaders(T request) 75 | { 76 | return this.GetHeaders(request, () => this.GetDefaultHeaderMetadata(), (hm) => this.GetOption(hm)); 77 | } 78 | 79 | /// Gets default header metadata. 80 | /// The default header metadata. 81 | private List GetDefaultHeaderMetadata() 82 | { 83 | return new List 84 | { 85 | HeaderMetadata.UserPrimaryImage, 86 | HeaderMetadata.Date, 87 | HeaderMetadata.User, 88 | HeaderMetadata.Type, 89 | HeaderMetadata.Severity, 90 | HeaderMetadata.Name, 91 | HeaderMetadata.ShortOverview, 92 | HeaderMetadata.Overview, 93 | //HeaderMetadata.UserId 94 | //HeaderMetadata.Item, 95 | }; 96 | } 97 | 98 | /// Gets an option. 99 | /// The header. 100 | /// The sort field. 101 | /// The option. 102 | private ReportOptions GetOption(HeaderMetadata header, string sortField = "") 103 | { 104 | HeaderMetadata internalHeader = header; 105 | 106 | ReportOptions option = new ReportOptions() 107 | { 108 | Header = new ReportHeader 109 | { 110 | HeaderFieldType = ReportFieldType.String, 111 | SortField = sortField, 112 | Type = "", 113 | ItemViewType = ItemViewType.None 114 | } 115 | }; 116 | 117 | switch (header) 118 | { 119 | case HeaderMetadata.Name: 120 | option.Column = (i, r) => i.Name; 121 | option.Header.SortField = ""; 122 | break; 123 | case HeaderMetadata.Overview: 124 | option.Column = (i, r) => i.Overview; 125 | option.Header.SortField = ""; 126 | option.Header.CanGroup = false; 127 | break; 128 | 129 | case HeaderMetadata.ShortOverview: 130 | option.Column = (i, r) => i.ShortOverview; 131 | option.Header.SortField = ""; 132 | option.Header.CanGroup = false; 133 | break; 134 | 135 | case HeaderMetadata.Type: 136 | option.Column = (i, r) => i.Type; 137 | option.Header.SortField = ""; 138 | break; 139 | 140 | case HeaderMetadata.Date: 141 | option.Column = (i, r) => i.Date; 142 | option.Header.SortField = ""; 143 | option.Header.HeaderFieldType = ReportFieldType.DateTime; 144 | option.Header.Type = ""; 145 | break; 146 | 147 | case HeaderMetadata.UserPrimaryImage: 148 | //option.Column = (i, r) => i.UserPrimaryImageTag; 149 | option.Header.DisplayType = ReportDisplayType.Screen; 150 | option.Header.ItemViewType = ItemViewType.UserPrimaryImage; 151 | option.Header.ShowHeaderLabel = false; 152 | internalHeader = HeaderMetadata.User; 153 | option.Header.CanGroup = false; 154 | option.Column = (i, r) => 155 | { 156 | if (i.UserId != Guid.Empty) 157 | { 158 | User user = _userManager.GetUserById(i.UserId); 159 | if (user != null) 160 | { 161 | var dto = _userManager.GetUserDto(user); 162 | return dto.PrimaryImageTag; 163 | } 164 | } 165 | return string.Empty; 166 | }; 167 | option.Header.SortField = ""; 168 | break; 169 | case HeaderMetadata.Severity: 170 | option.Column = (i, r) => i.Severity; 171 | option.Header.SortField = ""; 172 | break; 173 | case HeaderMetadata.Item: 174 | option.Column = (i, r) => i.ItemId; 175 | option.Header.SortField = ""; 176 | break; 177 | case HeaderMetadata.User: 178 | option.Column = (i, r) => 179 | { 180 | if (i.UserId != Guid.Empty) 181 | { 182 | User user = _userManager.GetUserById(i.UserId); 183 | if (user != null) 184 | return user.Username; 185 | } 186 | return string.Empty; 187 | }; 188 | option.Header.SortField = ""; 189 | break; 190 | case HeaderMetadata.UserId: 191 | option.Column = (i, r) => i.UserId; 192 | option.Header.SortField = ""; 193 | break; 194 | } 195 | 196 | option.Header.Name = GetLocalizedHeader(internalHeader); 197 | option.Header.FieldName = header; 198 | 199 | return option; 200 | } 201 | 202 | /// Gets report rows. 203 | /// The items. 204 | /// Options for controlling the operation. 205 | /// The report rows. 206 | private List GetReportRows(IEnumerable items, List> options) 207 | { 208 | var rows = new List(); 209 | 210 | foreach (ActivityLogEntry item in items) 211 | { 212 | ReportRow rRow = GetRow(item); 213 | foreach (ReportOptions option in options) 214 | { 215 | object itemColumn = option.Column != null ? option.Column(item, rRow) : ""; 216 | object itemId = option.ItemID != null ? option.ItemID(item) : ""; 217 | ReportItem rItem = new ReportItem 218 | { 219 | Name = ReportHelper.ConvertToString(itemColumn, option.Header.HeaderFieldType), 220 | Id = ReportHelper.ConvertToString(itemId, ReportFieldType.Object) 221 | }; 222 | rRow.Columns.Add(rItem); 223 | } 224 | 225 | rows.Add(rRow); 226 | } 227 | 228 | return rows; 229 | } 230 | 231 | /// Gets a row. 232 | /// The item. 233 | /// The row. 234 | private ReportRow GetRow(ActivityLogEntry item) 235 | { 236 | ReportRow rRow = new ReportRow 237 | { 238 | Id = item.Id.ToString(CultureInfo.InvariantCulture), 239 | UserId = item.UserId 240 | }; 241 | return rRow; 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/Common/ReportBuilderBase.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Globalization; 7 | using Jellyfin.Plugin.Reports.Api.Data; 8 | using Jellyfin.Plugin.Reports.Api.Model; 9 | using MediaBrowser.Controller.Entities; 10 | using MediaBrowser.Controller.Entities.TV; 11 | using MediaBrowser.Controller.Library; 12 | using MediaBrowser.Model.Channels; 13 | using MediaBrowser.Model.Dto; 14 | using MediaBrowser.Model.Entities; 15 | 16 | namespace Jellyfin.Plugin.Reports.Api.Common 17 | { 18 | /// A report builder base. 19 | public abstract class ReportBuilderBase 20 | { 21 | /// Manager for library. 22 | private readonly ILibraryManager _libraryManager; 23 | 24 | /// 25 | /// Initializes a new instance of the MediaBrowser.Api.Reports.ReportBuilderBase class. 26 | /// Manager for library. 27 | public ReportBuilderBase(ILibraryManager libraryManager) 28 | { 29 | _libraryManager = libraryManager; 30 | } 31 | 32 | protected Func GetBoolString => s => s == true ? "x" : string.Empty; 33 | 34 | /// Gets the headers. 35 | /// Type of the header. 36 | /// The request. 37 | /// The headers. 38 | protected internal abstract List GetHeaders(T request) where T : IReportsHeader; 39 | 40 | /// Gets active headers. 41 | /// Generic type parameter. 42 | /// Options for controlling the operation. 43 | /// The active headers. 44 | protected List GetActiveHeaders(List> options, ReportDisplayType displayType) 45 | => options.Where(x => this.DisplayTypeVisible(x.Header.DisplayType, displayType)).Select(x => x.Header).ToList(); 46 | 47 | /// Gets audio stream. 48 | /// The item. 49 | /// The audio stream. 50 | protected string GetAudioStream(BaseItem item) 51 | { 52 | var stream = GetStream(item, MediaStreamType.Audio); 53 | if (stream == null) 54 | { 55 | return string.Empty; 56 | } 57 | 58 | return string.Equals(stream.Codec, "DCA", StringComparison.OrdinalIgnoreCase) 59 | ? stream.Profile 60 | : stream.Codec.ToUpperInvariant(); 61 | } 62 | 63 | /// Gets an episode. 64 | /// The item. 65 | /// The episode. 66 | protected string GetEpisode(BaseItem item) 67 | { 68 | if (string.Equals(item.GetClientTypeName(), ChannelMediaContentType.Episode.ToString(), StringComparison.Ordinal) 69 | && item.ParentIndexNumber != null) 70 | { 71 | return "Season " + item.ParentIndexNumber; 72 | } 73 | 74 | return item.Name; 75 | } 76 | 77 | /// Gets a genre. 78 | /// The name. 79 | /// The genre. 80 | protected Genre GetGenre(string name) 81 | { 82 | if (string.IsNullOrEmpty(name)) 83 | return null; 84 | return _libraryManager.GetGenre(name); 85 | } 86 | 87 | /// Gets genre identifier. 88 | /// The name. 89 | /// The genre identifier. 90 | protected string GetGenreID(string name) 91 | { 92 | if (string.IsNullOrEmpty(name)) 93 | return string.Empty; 94 | return GetGenre(name).Id.ToString("N", CultureInfo.InvariantCulture); 95 | } 96 | 97 | /// Gets the headers. 98 | /// Generic type parameter. 99 | /// Options for controlling the operation. 100 | /// The headers. 101 | protected List GetHeaders(List> options) 102 | => options.ConvertAll(x => x.Header); 103 | 104 | /// Gets the headers. 105 | /// Generic type parameter. 106 | /// The request. 107 | /// The get headers metadata. 108 | /// Options for controlling the get. 109 | /// The headers. 110 | protected List GetHeaders(IReportsHeader request, Func> getHeadersMetadata, Func> getOptions) 111 | { 112 | List> options = this.GetReportOptions(request, getHeadersMetadata, getOptions); 113 | return this.GetHeaders(options); 114 | } 115 | 116 | /// Gets list as string. 117 | /// The items. 118 | /// The list as string. 119 | protected string GetListAsString(List items) 120 | { 121 | return string.Join("; ", items); 122 | } 123 | 124 | /// Gets localized header. 125 | /// The internal header. 126 | /// The localized header. 127 | protected static string GetLocalizedHeader(HeaderMetadata internalHeader) 128 | { 129 | if (internalHeader == HeaderMetadata.EpisodeNumber) 130 | { 131 | return "Episode"; 132 | } 133 | 134 | string headerName = string.Empty; 135 | if (internalHeader != HeaderMetadata.None) 136 | { 137 | string localHeader = internalHeader.ToString(); 138 | headerName = ReportHelper.GetCoreLocalizedString(localHeader); 139 | } 140 | return headerName; 141 | } 142 | 143 | /// Gets media source information. 144 | /// The item. 145 | /// The media source information. 146 | protected MediaSourceInfo GetMediaSourceInfo(BaseItem item) 147 | { 148 | if (item is IHasMediaSources mediaSource) 149 | return mediaSource.GetMediaSources(false).FirstOrDefault(n => n.Type == MediaSourceType.Default); 150 | 151 | return null; 152 | } 153 | 154 | /// Gets an object. 155 | /// Generic type parameter. 156 | /// Type of the r. 157 | /// The item. 158 | /// The function. 159 | /// The default value. 160 | /// The object. 161 | protected TReturn GetObject(BaseItem item, Func function, TReturn defaultValue = default) 162 | where TItem : class 163 | { 164 | if (item is TItem value && function != null) 165 | return function(value); 166 | else 167 | return defaultValue; 168 | } 169 | 170 | /// Gets a person. 171 | /// The name. 172 | /// The person. 173 | protected Person GetPerson(string name) 174 | { 175 | if (string.IsNullOrEmpty(name)) 176 | return null; 177 | return _libraryManager.GetPerson(name); 178 | } 179 | 180 | /// Gets person identifier. 181 | /// The name. 182 | /// The person identifier. 183 | protected string GetPersonID(string name) 184 | { 185 | if (string.IsNullOrEmpty(name)) 186 | return string.Empty; 187 | return GetPerson(name).Id.ToString("N", CultureInfo.InvariantCulture); 188 | } 189 | 190 | /// Gets report options. 191 | /// Generic type parameter. 192 | /// The request. 193 | /// The get headers metadata. 194 | /// Options for controlling the get. 195 | /// The report options. 196 | protected List> GetReportOptions(IReportsHeader request, Func> getHeadersMetadata, Func> getOptions) 197 | { 198 | List headersMetadata = getHeadersMetadata(); 199 | List> options = new List>(); 200 | ReportDisplayType displayType = ReportHelper.GetReportDisplayType(request.DisplayType); 201 | foreach (HeaderMetadata header in headersMetadata) 202 | { 203 | ReportOptions headerOptions = getOptions(header); 204 | if (this.DisplayTypeVisible(headerOptions.Header.DisplayType, displayType)) 205 | options.Add(headerOptions); 206 | } 207 | 208 | if (request != null && !string.IsNullOrEmpty(request.ReportColumns)) 209 | { 210 | List headersMetadataFiltered = ReportHelper.GetFilteredReportHeaderMetadata(request.ReportColumns, () => headersMetadata); 211 | foreach (ReportHeader header in options.Select(x => x.Header)) 212 | { 213 | 214 | if ((!DisplayTypeVisible(header.DisplayType, displayType)) || (!headersMetadataFiltered.Contains(header.FieldName) && header.DisplayType != ReportDisplayType.Export) 215 | || (!headersMetadataFiltered.Contains(HeaderMetadata.Status) && header.DisplayType == ReportDisplayType.Export)) 216 | { 217 | header.DisplayType = ReportDisplayType.None; 218 | } 219 | } 220 | } 221 | 222 | return options; 223 | } 224 | 225 | /// Gets runtime date time. 226 | /// The runtime. 227 | /// The runtime date time. 228 | protected double? GetRuntimeDateTime(long? runtime) 229 | { 230 | if (runtime.HasValue) 231 | return Math.Ceiling(new TimeSpan(runtime.Value).TotalMinutes); 232 | return null; 233 | } 234 | 235 | /// Gets series production year. 236 | /// The item. 237 | /// The series production year. 238 | protected string GetSeriesProductionYear(BaseItem item) 239 | { 240 | 241 | string productionYear = item.ProductionYear?.ToString(CultureInfo.InvariantCulture); 242 | if (item is not Series series) 243 | { 244 | if (item.ProductionYear == null || item.ProductionYear == 0) 245 | return string.Empty; 246 | return productionYear; 247 | } 248 | 249 | if (series.Status == SeriesStatus.Continuing) 250 | return productionYear + "-Present"; 251 | 252 | if (series.EndDate != null && series.EndDate.Value.Year != series.ProductionYear) 253 | return productionYear + "-" + series.EndDate.Value.Year; 254 | 255 | return productionYear; 256 | } 257 | 258 | /// Gets a stream. 259 | /// The item. 260 | /// Type of the stream. 261 | /// The stream. 262 | protected MediaStream GetStream(BaseItem item, MediaStreamType streamType) 263 | { 264 | var itemInfo = GetMediaSourceInfo(item); 265 | if (itemInfo != null) 266 | return itemInfo.MediaStreams.FirstOrDefault(n => n.Type == streamType); 267 | 268 | return null; 269 | } 270 | 271 | /// Gets a studio. 272 | /// The name. 273 | /// The studio. 274 | protected Studio GetStudio(string name) 275 | { 276 | if (string.IsNullOrEmpty(name)) 277 | return null; 278 | return _libraryManager.GetStudio(name); 279 | } 280 | 281 | /// Gets studio identifier. 282 | /// The name. 283 | /// The studio identifier. 284 | protected string GetStudioID(string name) 285 | { 286 | if (string.IsNullOrEmpty(name)) 287 | return string.Empty; 288 | return GetStudio(name).Id.ToString("N", CultureInfo.InvariantCulture); 289 | } 290 | 291 | /// Gets video resolution. 292 | /// The item. 293 | /// The video resolution. 294 | protected string GetVideoResolution(BaseItem item) 295 | { 296 | var stream = GetStream(item, 297 | MediaStreamType.Video); 298 | if (stream != null && stream.Width != null) 299 | return string.Format(CultureInfo.InvariantCulture, "{0} * {1}", 300 | stream.Width, 301 | stream.Height?.ToString(CultureInfo.InvariantCulture) ?? "-"); 302 | 303 | return string.Empty; 304 | } 305 | 306 | /// Gets video stream. 307 | /// The item. 308 | /// The video stream. 309 | protected string GetVideoStream(BaseItem item) 310 | { 311 | var stream = GetStream(item, MediaStreamType.Video); 312 | if (stream != null) 313 | return stream.Codec.ToUpperInvariant(); 314 | 315 | return string.Empty; 316 | } 317 | 318 | /// Displays a type visible. 319 | /// Type of the header display. 320 | /// Type of the display. 321 | /// true if it succeeds, false if it fails. 322 | protected bool DisplayTypeVisible(ReportDisplayType headerDisplayType, ReportDisplayType displayType) 323 | { 324 | if (headerDisplayType == ReportDisplayType.None) 325 | return false; 326 | 327 | bool rval = headerDisplayType == displayType || headerDisplayType == ReportDisplayType.ScreenExport && (displayType == ReportDisplayType.Screen || displayType == ReportDisplayType.Export); 328 | return rval; 329 | } 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/ReportsService.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel; 6 | using System.Globalization; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | using MediaBrowser.Controller.Entities; 11 | using MediaBrowser.Controller.Library; 12 | using MediaBrowser.Model.Entities; 13 | using MediaBrowser.Model.Activity; 14 | using MediaBrowser.Model.Globalization; 15 | using MediaBrowser.Model.Querying; 16 | using Jellyfin.Data.Queries; 17 | using Jellyfin.Database.Implementations.Entities; 18 | using Jellyfin.Plugin.Reports.Api.Activities; 19 | using Jellyfin.Plugin.Reports.Api.Common; 20 | using Jellyfin.Plugin.Reports.Api.Data; 21 | using Jellyfin.Plugin.Reports.Api.Model; 22 | 23 | namespace Jellyfin.Plugin.Reports.Api 24 | { 25 | /// The reports service. 26 | /// 27 | public class ReportsService 28 | { 29 | /// 30 | /// Initializes a new instance of the MediaBrowser.Api.Reports.ReportsService class. 31 | /// Manager for user. 32 | /// Manager for library. 33 | /// The localization. 34 | /// Manager for activity. 35 | public ReportsService(IUserManager userManager, ILibraryManager libraryManager, ILocalizationManager localization, IActivityManager activityManager) 36 | { 37 | _userManager = userManager; 38 | _libraryManager = libraryManager; 39 | _localization = localization; 40 | _activityManager = activityManager; 41 | } 42 | 43 | private readonly IActivityManager _activityManager; 44 | 45 | /// Manager for library. 46 | private readonly ILibraryManager _libraryManager; 47 | 48 | private readonly ILocalizationManager _localization; 49 | 50 | /// Manager for user. 51 | private readonly IUserManager _userManager; 52 | 53 | /// Gets the given request. 54 | /// The request. 55 | /// A Task<object> 56 | public async Task Get(GetActivityLogs request) 57 | { 58 | request.DisplayType = "Screen"; 59 | ReportResult result = await GetReportActivities(request).ConfigureAwait(false); 60 | return result; 61 | } 62 | 63 | /// Gets the given request. 64 | /// The request. 65 | /// A Task<object> 66 | public object Get(GetReportHeaders request) 67 | { 68 | if (string.IsNullOrEmpty(request.IncludeItemTypes)) 69 | return null; 70 | 71 | request.DisplayType = "Screen"; 72 | ReportViewType reportViewType = ReportHelper.GetReportViewType(request.ReportView); 73 | 74 | List result = reportViewType switch 75 | { 76 | ReportViewType.ReportData => new ReportBuilder(_libraryManager).GetHeaders(request), 77 | ReportViewType.ReportActivities => new ReportActivitiesBuilder(_libraryManager, _userManager).GetHeaders(request), 78 | _ => throw new InvalidEnumArgumentException() 79 | }; 80 | 81 | return result; 82 | } 83 | 84 | /// Gets the given request. 85 | /// The request. 86 | /// A Task<object> 87 | public object Get(GetItemReport request) 88 | { 89 | if (string.IsNullOrEmpty(request.IncludeItemTypes)) 90 | return null; 91 | 92 | request.DisplayType = "Screen"; 93 | ReportResult reportResult = GetReportResult(request); 94 | 95 | return reportResult; 96 | } 97 | 98 | /// Gets the given request. 99 | /// The request. 100 | /// A Task<object> 101 | public async Task<(MemoryStream content, string contentType, Dictionary headers)> Get(GetReportDownload request) 102 | { 103 | if (string.IsNullOrEmpty(request.IncludeItemTypes)) 104 | return (null, null, null); 105 | 106 | request.DisplayType = "Export"; 107 | string filename = "ReportExport"; 108 | ReportViewType reportViewType = ReportHelper.GetReportViewType(request.ReportView); 109 | 110 | ReportResult result = null; 111 | switch (reportViewType) 112 | { 113 | case ReportViewType.ReportData: 114 | result = GetReportResult(request); 115 | break; 116 | case ReportViewType.ReportActivities: 117 | result = await GetReportActivities(request).ConfigureAwait(false); 118 | break; 119 | } 120 | 121 | MemoryStream returnResult = null; 122 | Dictionary headers = new Dictionary(); 123 | string contentType = "text/csv;charset='utf-8'"; 124 | string fileExtension = "csv"; 125 | switch (request.ExportType) 126 | { 127 | case ReportExportType.CSV: 128 | returnResult = ReportExport.ExportToCsv(result); 129 | break; 130 | case ReportExportType.HTML: 131 | contentType = "text/html;charset='utf-8'"; 132 | fileExtension = "html"; 133 | returnResult = ReportExport.ExportToHtml(result); 134 | break; 135 | case ReportExportType.Excel: 136 | contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; 137 | fileExtension = "xlsx"; 138 | returnResult = ReportExport.ExportToExcel(result); 139 | break; 140 | } 141 | headers["Content-Disposition"] = $"attachment; filename=\"{filename}.{fileExtension}\""; 142 | headers["Content-Encoding"] = "UTF-8"; 143 | 144 | return (returnResult, contentType, headers); 145 | } 146 | 147 | private InternalItemsQuery GetItemsQuery(BaseReportRequest request, User user) 148 | { 149 | var query = new InternalItemsQuery(user) 150 | { 151 | IsPlayed = request.IsPlayed, 152 | MediaTypes = request.GetMediaTypes(), 153 | IncludeItemTypes = request.GetIncludeItemTypes(), 154 | ExcludeItemTypes = request.GetExcludeItemTypes(), 155 | Recursive = request.Recursive, 156 | OrderBy = request.GetOrderBy(), 157 | IsFavorite = request.IsFavorite, 158 | StartIndex = request.StartIndex, 159 | IsMissing = request.IsMissing, 160 | IsUnaired = request.IsUnaired, 161 | CollapseBoxSetItems = request.CollapseBoxSetItems, 162 | NameLessThan = request.NameLessThan, 163 | NameStartsWith = request.NameStartsWith, 164 | NameStartsWithOrGreater = request.NameStartsWithOrGreater, 165 | HasImdbId = request.HasImdbId, 166 | IsPlaceHolder = request.IsPlaceHolder, 167 | IsLocked = request.IsLocked, 168 | IsHD = request.IsHD, 169 | Is3D = request.Is3D, 170 | HasTvdbId = request.HasTvdbId, 171 | HasTmdbId = request.HasTmdbId, 172 | HasOverview = request.HasOverview, 173 | HasOfficialRating = request.HasOfficialRating, 174 | HasParentalRating = request.HasParentalRating, 175 | HasSpecialFeature = request.HasSpecialFeature, 176 | HasSubtitles = request.HasSubtitles, 177 | HasThemeSong = request.HasThemeSong, 178 | HasThemeVideo = request.HasThemeVideo, 179 | HasTrailer = request.HasTrailer, 180 | Tags = request.GetTags(), 181 | OfficialRatings = request.GetOfficialRatings(), 182 | Genres = request.GetGenres(), 183 | GenreIds = request.GetGenreIds(), 184 | StudioIds = request.GetStudioIds(), 185 | Person = request.Person, 186 | PersonIds = request.GetPersonIds(), 187 | PersonTypes = request.GetPersonTypes(), 188 | Years = request.GetYears(), 189 | ImageTypes = request.GetImageTypes().ToArray(), 190 | VideoTypes = request.GetVideoTypes().ToArray(), 191 | AdjacentTo = request.AdjacentTo, 192 | ItemIds = request.GetItemIds(), 193 | MinCommunityRating = request.MinCommunityRating, 194 | MinCriticRating = request.MinCriticRating, 195 | ParentId = string.IsNullOrWhiteSpace(request.ParentId) ? Guid.Empty : new Guid(request.ParentId), 196 | ParentIndexNumber = request.ParentIndexNumber, 197 | EnableTotalRecordCount = request.EnableTotalRecordCount 198 | }; 199 | 200 | if (request.Limit == -1) 201 | { 202 | query.Limit = null; 203 | } 204 | 205 | if (!string.IsNullOrWhiteSpace(request.Ids)) 206 | { 207 | query.CollapseBoxSetItems = false; 208 | } 209 | 210 | query.IsFavorite = null; 211 | if (request.IsFavorite) 212 | { 213 | query.IsFavorite = true; 214 | } 215 | else if (request.IsNotFavorite) 216 | { 217 | query.IsFavorite = false; 218 | } 219 | 220 | foreach (var filter in request.GetFilters()) 221 | { 222 | switch (filter) 223 | { 224 | case ItemFilter.Dislikes: 225 | query.IsLiked = false; 226 | break; 227 | case ItemFilter.IsFavoriteOrLikes: 228 | query.IsFavoriteOrLiked = true; 229 | break; 230 | case ItemFilter.IsFolder: 231 | query.IsFolder = true; 232 | break; 233 | case ItemFilter.IsNotFolder: 234 | query.IsFolder = false; 235 | break; 236 | case ItemFilter.IsPlayed: 237 | query.IsPlayed = true; 238 | break; 239 | case ItemFilter.IsResumable: 240 | query.IsResumable = true; 241 | break; 242 | case ItemFilter.IsUnplayed: 243 | query.IsPlayed = false; 244 | break; 245 | case ItemFilter.Likes: 246 | query.IsLiked = true; 247 | break; 248 | } 249 | } 250 | 251 | if (!string.IsNullOrEmpty(request.MinPremiereDate)) 252 | { 253 | query.MinPremiereDate = DateTime.Parse(request.MinPremiereDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); 254 | } 255 | 256 | if (!string.IsNullOrEmpty(request.MaxPremiereDate)) 257 | { 258 | query.MaxPremiereDate = DateTime.Parse(request.MaxPremiereDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); 259 | } 260 | 261 | // Filter by Series Status 262 | if (!string.IsNullOrEmpty(request.SeriesStatus)) 263 | { 264 | query.SeriesStatuses = request.SeriesStatus.Split(',').Select(d => Enum.Parse(d, true)).ToArray(); 265 | } 266 | 267 | // ExcludeLocationTypes 268 | if (!string.IsNullOrEmpty(request.ExcludeLocationTypes)) 269 | { 270 | var excludeLocationTypes = request.ExcludeLocationTypes.Split(',').Select(d => Enum.Parse(d, true)).ToArray(); 271 | if (excludeLocationTypes.Contains(LocationType.Virtual)) 272 | { 273 | query.IsVirtualItem = false; 274 | } 275 | } 276 | 277 | // Min official rating 278 | if (!string.IsNullOrWhiteSpace(request.MinOfficialRating)) 279 | { 280 | query.MinParentalRating = _localization.GetRatingScore(request.MinOfficialRating); 281 | } 282 | 283 | // Max official rating 284 | if (!string.IsNullOrWhiteSpace(request.MaxOfficialRating)) 285 | { 286 | query.MaxParentalRating = _localization.GetRatingScore(request.MaxOfficialRating); 287 | } 288 | 289 | query.CollapseBoxSetItems = false; 290 | 291 | if (request.Limit > -1 && request.Limit < int.MaxValue) 292 | { 293 | query.Limit = request.Limit; 294 | } 295 | 296 | return query; 297 | } 298 | 299 | private QueryResult GetQueryResult(BaseReportRequest request, User user) 300 | { 301 | // all report queries currently need this because it's not being specified 302 | request.Recursive = true; 303 | 304 | BaseItem item = null; 305 | 306 | if (!string.IsNullOrEmpty(request.ParentId)) 307 | { 308 | item = _libraryManager.GetItemById(request.ParentId); 309 | } 310 | 311 | if (string.Equals(request.IncludeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase)) 312 | { 313 | //item = user == null ? _libraryManager.RootFolder : user.RootFolder; 314 | } 315 | else if (string.Equals(request.IncludeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase)) 316 | { 317 | item = _libraryManager.GetUserRootFolder(); 318 | } 319 | 320 | // Default list type = children 321 | 322 | Folder folder = item as Folder; 323 | if (folder is null) 324 | { 325 | folder = _libraryManager.GetUserRootFolder(); 326 | } 327 | 328 | if (!string.IsNullOrEmpty(request.Ids)) 329 | { 330 | request.Recursive = true; 331 | var query = GetItemsQuery(request, user); 332 | var result = folder.GetItems(query); 333 | 334 | if (string.IsNullOrWhiteSpace(request.SortBy)) 335 | { 336 | var ids = query.ItemIds.ToList(); 337 | 338 | // Try to preserve order 339 | result.Items = result.Items.OrderBy(i => ids.IndexOf(i.Id)).ToArray(); 340 | } 341 | 342 | return result; 343 | } 344 | 345 | if (request.Recursive) 346 | { 347 | return folder.GetItems(GetItemsQuery(request, user)); 348 | } 349 | 350 | if (user == null) 351 | { 352 | return folder.GetItems(GetItemsQuery(request, null)); 353 | } 354 | 355 | var userRoot = item as UserRootFolder; 356 | 357 | if (userRoot == null) 358 | { 359 | return folder.GetItems(GetItemsQuery(request, user)); 360 | } 361 | 362 | IEnumerable items = folder.GetChildren(user, true); 363 | 364 | var itemsArray = items.ToArray(); 365 | 366 | return new QueryResult 367 | { 368 | Items = itemsArray, 369 | TotalRecordCount = itemsArray.Length 370 | }; 371 | } 372 | 373 | /// Gets report activities. 374 | /// The request. 375 | /// The report activities. 376 | private async Task GetReportActivities(IReportsDownload request) 377 | { 378 | var activityLogQuery = new ActivityLogQuery 379 | { 380 | Skip = request.StartIndex, 381 | Limit = request.HasQueryLimit ? request.Limit : null 382 | }; 383 | 384 | var queryResult = await _activityManager.GetPagedResultAsync(activityLogQuery).ConfigureAwait(false); 385 | ReportActivitiesBuilder builder = new ReportActivitiesBuilder(_libraryManager, _userManager); 386 | var result = builder.GetResult(queryResult, request); 387 | result.TotalRecordCount = queryResult.TotalRecordCount; 388 | return result; 389 | } 390 | 391 | /// Gets report result. 392 | /// The request. 393 | /// The report result. 394 | private ReportResult GetReportResult(BaseReportRequest request) 395 | { 396 | User user = !string.IsNullOrWhiteSpace(request.UserId) ? _userManager.GetUserById(new Guid(request.UserId)) : null; 397 | ReportBuilder reportBuilder = new ReportBuilder(_libraryManager); 398 | QueryResult queryResult = GetQueryResult(request, user); 399 | ReportResult reportResult = reportBuilder.GetResult(queryResult.Items, request); 400 | reportResult.TotalRecordCount = queryResult.TotalRecordCount; 401 | 402 | return reportResult; 403 | } 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/ReportsController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Mime; 3 | using System.Threading.Tasks; 4 | using Jellyfin.Plugin.Reports.Api.Common; 5 | using Jellyfin.Plugin.Reports.Api.Model; 6 | using MediaBrowser.Common.Api; 7 | using MediaBrowser.Controller.Library; 8 | using MediaBrowser.Model.Activity; 9 | using MediaBrowser.Model.Globalization; 10 | using Microsoft.AspNetCore.Authorization; 11 | using Microsoft.AspNetCore.Http; 12 | using Microsoft.AspNetCore.Mvc; 13 | 14 | namespace Jellyfin.Plugin.Reports.Api 15 | { 16 | [ApiController] 17 | [Route("[controller]")] 18 | [Authorize(Policy = Policies.RequiresElevation)] 19 | [Produces(MediaTypeNames.Application.Json)] 20 | public class ReportsController : ControllerBase 21 | { 22 | private readonly ReportsService _reportsService; 23 | 24 | public ReportsController( 25 | IUserManager userManager, 26 | ILibraryManager libraryManager, 27 | ILocalizationManager localizationManager, 28 | IActivityManager activityManager) 29 | { 30 | _reportsService = new ReportsService(userManager, libraryManager, localizationManager, activityManager); 31 | } 32 | 33 | /// 34 | /// Gets reports based on library items. 35 | /// 36 | [HttpGet("Items")] 37 | public ActionResult GetItemReport( 38 | [FromQuery] bool? hasThemeSong, 39 | [FromQuery] bool? hasThemeVideo, 40 | [FromQuery] bool? hasSubtitles, 41 | [FromQuery] bool? hasSpecialFeature, 42 | [FromQuery] bool? hasTrailer, 43 | [FromQuery] Guid? adjacentTo, 44 | [FromQuery] int? minIndexNumber, 45 | [FromQuery] int? parentIndexNumber, 46 | [FromQuery] bool? hasParentalRating, 47 | [FromQuery] bool? isHd, 48 | [FromQuery] string? locationTypes, 49 | [FromQuery] string? excludeLocationTypes, 50 | [FromQuery] bool? isMissing, 51 | [FromQuery] bool? isUnaried, 52 | [FromQuery] double? minCommunityRating, 53 | [FromQuery] double? minCriticRating, 54 | [FromQuery] int? airedDuringSeason, 55 | [FromQuery] string? minPremiereDate, 56 | [FromQuery] string? minDateLastSaved, 57 | [FromQuery] string? minDateLastSavedForUser, 58 | [FromQuery] string? maxPremiereDate, 59 | [FromQuery] bool? hasOverview, 60 | [FromQuery] bool? hasImdbId, 61 | [FromQuery] bool? hasTmdbId, 62 | [FromQuery] bool? hasTvdbId, 63 | [FromQuery] bool? isInBoxSet, 64 | [FromQuery] string? excludeItemIds, 65 | [FromQuery] bool? enableTotalRecordCount, 66 | [FromQuery] int? startIndex, 67 | [FromQuery] int? limit, 68 | [FromQuery] bool? recursive, 69 | [FromQuery] string? sortOrder, 70 | [FromQuery] string? parentId, 71 | [FromQuery] string? fields, 72 | [FromQuery] string? excludeItemTypes, 73 | [FromQuery] string? includeItemTypes, 74 | [FromQuery] string? filters, 75 | [FromQuery] bool? isFavorite, 76 | [FromQuery] bool? isNotFavorite, 77 | [FromQuery] string? mediaTypes, 78 | [FromQuery] string? imageTypes, 79 | [FromQuery] string? sortBy, 80 | [FromQuery] bool? isPlayed, 81 | [FromQuery] string? genres, 82 | [FromQuery] string? genreIds, 83 | [FromQuery] string? officialRatings, 84 | [FromQuery] string? tags, 85 | [FromQuery] string? years, 86 | [FromQuery] bool? enableUserData, 87 | [FromQuery] int? imageTypeLimit, 88 | [FromQuery] string? enableImageTypes, 89 | [FromQuery] string? person, 90 | [FromQuery] string? personIds, 91 | [FromQuery] string? personTypes, 92 | [FromQuery] string? studios, 93 | [FromQuery] string? studioIds, 94 | [FromQuery] string? artists, 95 | [FromQuery] string? excludeArtistIds, 96 | [FromQuery] string? artistIds, 97 | [FromQuery] string? albums, 98 | [FromQuery] string? albumIds, 99 | [FromQuery] string? ids, 100 | [FromQuery] string? videoTypes, 101 | [FromQuery] string? userId, 102 | [FromQuery] string? minOfficialRating, 103 | [FromQuery] bool? isLocked, 104 | [FromQuery] bool? isPlaceHolder, 105 | [FromQuery] bool? hasOfficialRating, 106 | [FromQuery] bool? collapseBoxSetItems, 107 | [FromQuery] bool? is3D, 108 | [FromQuery] string? seriesStatus, 109 | [FromQuery] string? nameStartsWithOrGreater, 110 | [FromQuery] string? nameStartsWith, 111 | [FromQuery] string? nameLessThan, 112 | [FromQuery] string? reportView, 113 | [FromQuery] string? displayType, 114 | [FromQuery] bool? hasQueryLimit, 115 | [FromQuery] string? groupBy, 116 | [FromQuery] string? reportColumns, 117 | [FromQuery] bool enableImages = true) 118 | { 119 | var request = new GetItemReport 120 | { 121 | Albums = albums, 122 | AdjacentTo = adjacentTo, 123 | AiredDuringSeason = airedDuringSeason, 124 | AlbumIds = albumIds, 125 | ArtistIds = artistIds, 126 | Artists = artists, 127 | CollapseBoxSetItems = collapseBoxSetItems, 128 | DisplayType = displayType, 129 | EnableImages = enableImages, 130 | EnableImageTypes = enableImageTypes, 131 | Fields = fields, 132 | Filters = filters, 133 | Genres = genres, 134 | Ids = ids, 135 | Limit = limit, 136 | Person = person, 137 | Recursive = recursive ?? true, 138 | Studios = studios, 139 | Tags = tags, 140 | Years = years, 141 | GenreIds = genreIds, 142 | GroupBy = groupBy, 143 | HasOverview = hasOverview, 144 | HasSubtitles = hasSubtitles, 145 | HasTrailer = hasTrailer, 146 | ImageTypes = imageTypes, 147 | Is3D = is3D, 148 | IsFavorite = isFavorite ?? false, 149 | IsLocked = isLocked, 150 | IsMissing = isMissing, 151 | IsPlayed = isPlayed, 152 | IsUnaired = isUnaried, 153 | LocationTypes = locationTypes, 154 | MediaTypes = mediaTypes, 155 | OfficialRatings = officialRatings, 156 | ParentId = parentId, 157 | PersonIds = personIds, 158 | PersonTypes = personTypes, 159 | ReportColumns = reportColumns, 160 | ReportView = reportView, 161 | SeriesStatus = seriesStatus, 162 | SortBy = sortBy, 163 | SortOrder = sortOrder, 164 | StartIndex = startIndex, 165 | StudioIds = studioIds, 166 | UserId = userId, 167 | VideoTypes = videoTypes, 168 | EnableUserData = enableUserData, 169 | ExcludeArtistIds = excludeArtistIds, 170 | ExcludeItemIds = excludeItemIds, 171 | ExcludeItemTypes = excludeItemTypes, 172 | ExcludeLocationTypes = excludeLocationTypes, 173 | HasImdbId = hasImdbId, 174 | HasOfficialRating = hasOfficialRating, 175 | HasParentalRating = hasParentalRating, 176 | HasQueryLimit = hasQueryLimit ?? false, 177 | HasSpecialFeature = hasSpecialFeature, 178 | HasThemeSong = hasThemeSong, 179 | HasThemeVideo = hasThemeVideo, 180 | HasTmdbId = hasTmdbId, 181 | HasTvdbId = hasTvdbId, 182 | ImageTypeLimit = imageTypeLimit, 183 | IncludeItemTypes = includeItemTypes, 184 | IsHD = isHd, 185 | IsNotFavorite = isNotFavorite ?? false, 186 | IsPlaceHolder = isPlaceHolder, 187 | MaxPremiereDate = maxPremiereDate, 188 | MinCommunityRating = minCommunityRating, 189 | MinCriticRating = minCriticRating, 190 | MinIndexNumber = minIndexNumber, 191 | MinOfficialRating = minOfficialRating, 192 | MinPremiereDate = minPremiereDate, 193 | NameLessThan = nameLessThan, 194 | NameStartsWith = nameStartsWith, 195 | ParentIndexNumber = parentIndexNumber, 196 | EnableTotalRecordCount = enableTotalRecordCount ?? true, 197 | IsInBoxSet = isInBoxSet, 198 | MinDateLastSaved = minDateLastSaved, 199 | NameStartsWithOrGreater = nameStartsWithOrGreater, 200 | MinDateLastSavedForUser = minDateLastSavedForUser 201 | }; 202 | return Ok(_reportsService.Get(request)); 203 | } 204 | 205 | /// 206 | /// Gets reports headers based on library items. 207 | /// 208 | /// The report view. Values (ReportData, ReportActivities). 209 | /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted. 210 | /// 211 | [HttpGet("Headers")] 212 | public ActionResult GetReportHeaders( 213 | [FromQuery] string reportView, 214 | [FromQuery] string includeItemTypes) 215 | { 216 | var request = new GetReportHeaders 217 | { 218 | ReportView = reportView, 219 | IncludeItemTypes = includeItemTypes 220 | }; 221 | 222 | return Ok(_reportsService.Get(request)); 223 | } 224 | 225 | /// 226 | /// Gets activities entries. 227 | /// 228 | [HttpGet("Activities")] 229 | public async Task GetActivityLogs( 230 | [FromQuery] string? reportView, 231 | [FromQuery] string? displayType, 232 | [FromQuery] bool? hasQueryLimit, 233 | [FromQuery] string? groupBy, 234 | [FromQuery] string? reportColumns, 235 | [FromQuery] int? startIndex, 236 | [FromQuery] int? limit, 237 | [FromQuery] string? minDate, 238 | [FromQuery] string? includeItemTypes) 239 | { 240 | var request = new GetActivityLogs 241 | { 242 | ReportView = reportView, 243 | Limit = limit, 244 | DisplayType = displayType, 245 | GroupBy = groupBy, 246 | MinDate = minDate, 247 | ReportColumns = reportColumns, 248 | StartIndex = startIndex, 249 | HasQueryLimit = hasQueryLimit ?? false, 250 | IncludeItemTypes = includeItemTypes 251 | }; 252 | 253 | return Ok(await _reportsService.Get(request).ConfigureAwait(false)); 254 | } 255 | 256 | /// 257 | /// Downloads report. 258 | /// 259 | [HttpGet("Items/Download")] 260 | public async Task> GetReportDownload( 261 | [FromQuery] bool? hasThemeSong, 262 | [FromQuery] bool? hasThemeVideo, 263 | [FromQuery] bool? hasSubtitles, 264 | [FromQuery] bool? hasSpecialFeature, 265 | [FromQuery] bool? hasTrailer, 266 | [FromQuery] Guid? adjacentTo, 267 | [FromQuery] int? minIndexNumber, 268 | [FromQuery] int? parentIndexNumber, 269 | [FromQuery] bool? hasParentalRating, 270 | [FromQuery] bool? isHd, 271 | [FromQuery] string? locationTypes, 272 | [FromQuery] string? excludeLocationTypes, 273 | [FromQuery] bool? isMissing, 274 | [FromQuery] bool? isUnaried, 275 | [FromQuery] double? minCommunityRating, 276 | [FromQuery] double? minCriticRating, 277 | [FromQuery] int? airedDuringSeason, 278 | [FromQuery] string? minPremiereDate, 279 | [FromQuery] string? minDateLastSaved, 280 | [FromQuery] string? minDateLastSavedForUser, 281 | [FromQuery] string? maxPremiereDate, 282 | [FromQuery] bool? hasOverview, 283 | [FromQuery] bool? hasImdbId, 284 | [FromQuery] bool? hasTmdbId, 285 | [FromQuery] bool? hasTvdbId, 286 | [FromQuery] bool? isInBoxSet, 287 | [FromQuery] string? excludeItemIds, 288 | [FromQuery] bool? enableTotalRecordCount, 289 | [FromQuery] int? startIndex, 290 | [FromQuery] int? limit, 291 | [FromQuery] bool? recursive, 292 | [FromQuery] string? sortOrder, 293 | [FromQuery] string? parentId, 294 | [FromQuery] string? fields, 295 | [FromQuery] string? excludeItemTypes, 296 | [FromQuery] string? includeItemTypes, 297 | [FromQuery] string? filters, 298 | [FromQuery] bool? isFavorite, 299 | [FromQuery] bool? isNotFavorite, 300 | [FromQuery] string? mediaTypes, 301 | [FromQuery] string? imageTypes, 302 | [FromQuery] string? sortBy, 303 | [FromQuery] bool? isPlayed, 304 | [FromQuery] string? genres, 305 | [FromQuery] string? genreIds, 306 | [FromQuery] string? officialRatings, 307 | [FromQuery] string? tags, 308 | [FromQuery] string? years, 309 | [FromQuery] bool? enableUserData, 310 | [FromQuery] int? imageTypeLimit, 311 | [FromQuery] string? enableImageTypes, 312 | [FromQuery] string? person, 313 | [FromQuery] string? personIds, 314 | [FromQuery] string? personTypes, 315 | [FromQuery] string? studios, 316 | [FromQuery] string? studioIds, 317 | [FromQuery] string? artists, 318 | [FromQuery] string? excludeArtistIds, 319 | [FromQuery] string? artistIds, 320 | [FromQuery] string? albums, 321 | [FromQuery] string? albumIds, 322 | [FromQuery] string? ids, 323 | [FromQuery] string? videoTypes, 324 | [FromQuery] string? userId, 325 | [FromQuery] string? minOfficialRating, 326 | [FromQuery] bool? isLocked, 327 | [FromQuery] bool? isPlaceHolder, 328 | [FromQuery] bool? hasOfficialRating, 329 | [FromQuery] bool? collapseBoxSetItems, 330 | [FromQuery] bool? is3D, 331 | [FromQuery] string? seriesStatus, 332 | [FromQuery] string? nameStartsWithOrGreater, 333 | [FromQuery] string? nameStartsWith, 334 | [FromQuery] string? nameLessThan, 335 | [FromQuery] string? reportView, 336 | [FromQuery] string? displayType, 337 | [FromQuery] bool? hasQueryLimit, 338 | [FromQuery] string? groupBy, 339 | [FromQuery] string? reportColumns, 340 | [FromQuery] string? minDate, 341 | [FromQuery] ReportExportType exportType = ReportExportType.CSV, 342 | [FromQuery] bool enableImages = true) 343 | { 344 | var request = new GetReportDownload 345 | { 346 | Albums = albums, 347 | AdjacentTo = adjacentTo, 348 | AiredDuringSeason = airedDuringSeason, 349 | AlbumIds = albumIds, 350 | ArtistIds = artistIds, 351 | Artists = artists, 352 | CollapseBoxSetItems = collapseBoxSetItems, 353 | DisplayType = displayType, 354 | EnableImages = enableImages, 355 | EnableImageTypes = enableImageTypes, 356 | Fields = fields, 357 | Filters = filters, 358 | Genres = genres, 359 | Ids = ids, 360 | Limit = limit, 361 | Person = person, 362 | Recursive = recursive ?? true, 363 | Studios = studios, 364 | Tags = tags, 365 | Years = years, 366 | GenreIds = genreIds, 367 | GroupBy = groupBy, 368 | HasOverview = hasOverview, 369 | HasSubtitles = hasSubtitles, 370 | HasTrailer = hasTrailer, 371 | ImageTypes = imageTypes, 372 | Is3D = is3D, 373 | IsFavorite = isFavorite ?? false, 374 | IsLocked = isLocked, 375 | IsMissing = isMissing, 376 | IsPlayed = isPlayed, 377 | IsUnaired = isUnaried, 378 | LocationTypes = locationTypes, 379 | MediaTypes = mediaTypes, 380 | OfficialRatings = officialRatings, 381 | ParentId = parentId, 382 | PersonIds = personIds, 383 | PersonTypes = personTypes, 384 | ReportColumns = reportColumns, 385 | ReportView = reportView, 386 | SeriesStatus = seriesStatus, 387 | SortBy = sortBy, 388 | SortOrder = sortOrder, 389 | StartIndex = startIndex, 390 | StudioIds = studioIds, 391 | UserId = userId, 392 | VideoTypes = videoTypes, 393 | EnableUserData = enableUserData, 394 | ExcludeArtistIds = excludeArtistIds, 395 | ExcludeItemIds = excludeItemIds, 396 | ExcludeItemTypes = excludeItemTypes, 397 | ExcludeLocationTypes = excludeLocationTypes, 398 | HasImdbId = hasImdbId, 399 | HasOfficialRating = hasOfficialRating, 400 | HasParentalRating = hasParentalRating, 401 | HasQueryLimit = hasQueryLimit ?? false, 402 | HasSpecialFeature = hasSpecialFeature, 403 | HasThemeSong = hasThemeSong, 404 | HasThemeVideo = hasThemeVideo, 405 | HasTmdbId = hasTmdbId, 406 | HasTvdbId = hasTvdbId, 407 | ImageTypeLimit = imageTypeLimit, 408 | IncludeItemTypes = includeItemTypes, 409 | IsHD = isHd, 410 | IsNotFavorite = isNotFavorite ?? false, 411 | IsPlaceHolder = isPlaceHolder, 412 | MaxPremiereDate = maxPremiereDate, 413 | MinCommunityRating = minCommunityRating, 414 | MinCriticRating = minCriticRating, 415 | MinIndexNumber = minIndexNumber, 416 | MinOfficialRating = minOfficialRating, 417 | MinPremiereDate = minPremiereDate, 418 | NameLessThan = nameLessThan, 419 | NameStartsWith = nameStartsWith, 420 | ParentIndexNumber = parentIndexNumber, 421 | EnableTotalRecordCount = enableTotalRecordCount ?? true, 422 | IsInBoxSet = isInBoxSet, 423 | MinDateLastSaved = minDateLastSaved, 424 | NameStartsWithOrGreater = nameStartsWithOrGreater, 425 | MinDateLastSavedForUser = minDateLastSavedForUser, 426 | ExportType = exportType, 427 | MinDate = minDate 428 | }; 429 | var (content, contentType, headers) = await _reportsService.Get(request).ConfigureAwait(false); 430 | 431 | foreach (var (key, value) in headers) 432 | { 433 | Response.Headers.Append(key, value); 434 | } 435 | 436 | return File(content, contentType); 437 | } 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Reports/Api/Data/ReportBuilder.cs: -------------------------------------------------------------------------------- 1 | #nullable disable 2 | 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using System.Linq; 6 | using Jellyfin.Plugin.Reports.Api.Common; 7 | using Jellyfin.Plugin.Reports.Api.Model; 8 | using MediaBrowser.Controller.Entities; 9 | using MediaBrowser.Controller.Entities.Audio; 10 | using MediaBrowser.Controller.Entities.TV; 11 | using MediaBrowser.Controller.Library; 12 | using MediaBrowser.Model.Entities; 13 | 14 | namespace Jellyfin.Plugin.Reports.Api.Data 15 | { 16 | /// A report builder. 17 | /// 18 | public class ReportBuilder : ReportBuilderBase 19 | { 20 | /// 21 | /// Initializes a new instance of the MediaBrowser.Api.Reports.ReportBuilder class. 22 | /// Manager for library. 23 | public ReportBuilder(ILibraryManager libraryManager) 24 | : base(libraryManager) 25 | { 26 | } 27 | 28 | /// Gets report result. 29 | /// The items. 30 | /// The request. 31 | /// The report result. 32 | public ReportResult GetResult(IReadOnlyList items, IReportsQuery request) 33 | { 34 | ReportIncludeItemTypes reportRowType = ReportHelper.GetRowType(request.IncludeItemTypes); 35 | ReportDisplayType displayType = ReportHelper.GetReportDisplayType(request.DisplayType); 36 | 37 | List> options = this.GetReportOptions(request, 38 | () => this.GetDefaultHeaderMetadata(reportRowType), 39 | (hm) => this.GetOption(hm)).Where(x => this.DisplayTypeVisible(x.Header.DisplayType, displayType)).ToList(); 40 | 41 | var headers = GetHeaders(options); 42 | var rows = GetReportRows(items, options); 43 | 44 | ReportResult result = new ReportResult { Headers = headers }; 45 | HeaderMetadata groupBy = ReportHelper.GetHeaderMetadataType(request.GroupBy); 46 | int i = headers.FindIndex(x => x.FieldName == groupBy); 47 | if (groupBy != HeaderMetadata.None && i >= 0) 48 | { 49 | var rowsGroup = rows.SelectMany(x => x.Columns[i].Name.Split(';'), (x, g) => new { Group = g.Trim(), Rows = x }) 50 | .GroupBy(x => x.Group) 51 | .OrderBy(x => x.Key) 52 | .Select(x => new ReportGroup(x.Key, x.Select(r => r.Rows).ToList())); 53 | 54 | result.Groups = rowsGroup.ToList(); 55 | result.IsGrouped = true; 56 | } 57 | else 58 | { 59 | result.Rows = rows; 60 | result.IsGrouped = false; 61 | } 62 | 63 | return result; 64 | } 65 | 66 | /// Gets the headers. 67 | /// Type of the header. 68 | /// The request. 69 | /// The headers. 70 | /// 71 | protected internal override List GetHeaders(T request) 72 | { 73 | ReportIncludeItemTypes reportRowType = ReportHelper.GetRowType(request.IncludeItemTypes); 74 | return this.GetHeaders(request, () => this.GetDefaultHeaderMetadata(reportRowType), (hm) => this.GetOption(hm)); 75 | } 76 | 77 | /// Gets default report header metadata. 78 | /// Type of the report row. 79 | /// The default report header metadata. 80 | private List GetDefaultHeaderMetadata(ReportIncludeItemTypes reportIncludeItemTypes) 81 | { 82 | switch (reportIncludeItemTypes) 83 | { 84 | case ReportIncludeItemTypes.Season: 85 | return new List 86 | { 87 | HeaderMetadata.Status, 88 | HeaderMetadata.Locked, 89 | HeaderMetadata.ImagePrimary, 90 | HeaderMetadata.ImageBackdrop, 91 | HeaderMetadata.ImageLogo, 92 | HeaderMetadata.Series, 93 | HeaderMetadata.Season, 94 | HeaderMetadata.SeasonNumber, 95 | HeaderMetadata.DateAdded, 96 | HeaderMetadata.Year, 97 | HeaderMetadata.Genres 98 | }; 99 | 100 | case ReportIncludeItemTypes.Series: 101 | return new List 102 | { 103 | HeaderMetadata.Status, 104 | HeaderMetadata.Locked, 105 | HeaderMetadata.ImagePrimary, 106 | HeaderMetadata.ImageBackdrop, 107 | HeaderMetadata.ImageLogo, 108 | HeaderMetadata.Name, 109 | HeaderMetadata.Network, 110 | HeaderMetadata.DateAdded, 111 | HeaderMetadata.Year, 112 | HeaderMetadata.Genres, 113 | HeaderMetadata.ParentalRating, 114 | HeaderMetadata.CommunityRating, 115 | HeaderMetadata.Runtime, 116 | HeaderMetadata.Trailers, 117 | HeaderMetadata.Specials 118 | }; 119 | 120 | case ReportIncludeItemTypes.MusicAlbum: 121 | return new List 122 | { 123 | HeaderMetadata.Status, 124 | HeaderMetadata.Locked, 125 | HeaderMetadata.ImagePrimary, 126 | HeaderMetadata.ImageBackdrop, 127 | HeaderMetadata.ImageLogo, 128 | HeaderMetadata.Name, 129 | HeaderMetadata.AlbumArtist, 130 | HeaderMetadata.DateAdded, 131 | HeaderMetadata.ReleaseDate, 132 | HeaderMetadata.Tracks, 133 | HeaderMetadata.Year, 134 | HeaderMetadata.Genres 135 | }; 136 | 137 | case ReportIncludeItemTypes.MusicArtist: 138 | return new List 139 | { 140 | HeaderMetadata.Status, 141 | HeaderMetadata.Locked, 142 | HeaderMetadata.ImagePrimary, 143 | HeaderMetadata.ImageBackdrop, 144 | HeaderMetadata.ImageLogo, 145 | HeaderMetadata.MusicArtist, 146 | HeaderMetadata.Countries, 147 | HeaderMetadata.DateAdded, 148 | HeaderMetadata.Year, 149 | HeaderMetadata.Genres 150 | }; 151 | 152 | case ReportIncludeItemTypes.Movie: 153 | return new List 154 | { 155 | HeaderMetadata.Status, 156 | HeaderMetadata.Locked, 157 | HeaderMetadata.ImagePrimary, 158 | HeaderMetadata.ImageBackdrop, 159 | HeaderMetadata.ImageLogo, 160 | HeaderMetadata.Name, 161 | HeaderMetadata.DateAdded, 162 | HeaderMetadata.ReleaseDate, 163 | HeaderMetadata.Year, 164 | HeaderMetadata.Genres, 165 | HeaderMetadata.ParentalRating, 166 | HeaderMetadata.CommunityRating, 167 | HeaderMetadata.Runtime, 168 | HeaderMetadata.Video, 169 | HeaderMetadata.Resolution, 170 | HeaderMetadata.Audio, 171 | HeaderMetadata.Subtitles, 172 | HeaderMetadata.Trailers, 173 | HeaderMetadata.Specials, 174 | HeaderMetadata.Path 175 | }; 176 | 177 | case ReportIncludeItemTypes.Book: 178 | return new List 179 | { 180 | HeaderMetadata.Status, 181 | HeaderMetadata.Locked, 182 | HeaderMetadata.ImagePrimary, 183 | HeaderMetadata.ImageBackdrop, 184 | HeaderMetadata.ImageLogo, 185 | HeaderMetadata.Name, 186 | HeaderMetadata.DateAdded, 187 | HeaderMetadata.ReleaseDate, 188 | HeaderMetadata.Year, 189 | HeaderMetadata.Genres, 190 | HeaderMetadata.ParentalRating, 191 | HeaderMetadata.CommunityRating 192 | }; 193 | 194 | case ReportIncludeItemTypes.BoxSet: 195 | return new List 196 | { 197 | HeaderMetadata.Status, 198 | HeaderMetadata.Locked, 199 | HeaderMetadata.ImagePrimary, 200 | HeaderMetadata.ImageBackdrop, 201 | HeaderMetadata.ImageLogo, 202 | HeaderMetadata.Name, 203 | HeaderMetadata.DateAdded, 204 | HeaderMetadata.ReleaseDate, 205 | HeaderMetadata.Year, 206 | HeaderMetadata.Genres, 207 | HeaderMetadata.ParentalRating, 208 | HeaderMetadata.CommunityRating, 209 | HeaderMetadata.Trailers 210 | }; 211 | 212 | case ReportIncludeItemTypes.Audio: 213 | return new List 214 | { 215 | HeaderMetadata.Status, 216 | HeaderMetadata.Locked, 217 | HeaderMetadata.ImagePrimary, 218 | HeaderMetadata.ImageBackdrop, 219 | HeaderMetadata.ImageLogo, 220 | HeaderMetadata.Name, 221 | HeaderMetadata.AudioAlbumArtist, 222 | HeaderMetadata.AudioAlbum, 223 | HeaderMetadata.Disc, 224 | HeaderMetadata.Track, 225 | HeaderMetadata.DateAdded, 226 | HeaderMetadata.ReleaseDate, 227 | HeaderMetadata.Year, 228 | HeaderMetadata.Genres, 229 | HeaderMetadata.ParentalRating, 230 | HeaderMetadata.CommunityRating, 231 | HeaderMetadata.Runtime, 232 | HeaderMetadata.Audio 233 | }; 234 | 235 | case ReportIncludeItemTypes.Episode: 236 | return new List 237 | { 238 | HeaderMetadata.Status, 239 | HeaderMetadata.Locked, 240 | HeaderMetadata.ImagePrimary, 241 | HeaderMetadata.ImageBackdrop, 242 | HeaderMetadata.ImageLogo, 243 | HeaderMetadata.Name, 244 | HeaderMetadata.EpisodeSeries, 245 | HeaderMetadata.Season, 246 | HeaderMetadata.EpisodeNumber, 247 | HeaderMetadata.DateAdded, 248 | HeaderMetadata.ReleaseDate, 249 | HeaderMetadata.Year, 250 | HeaderMetadata.Genres, 251 | HeaderMetadata.ParentalRating, 252 | HeaderMetadata.CommunityRating, 253 | HeaderMetadata.Runtime, 254 | HeaderMetadata.Video, 255 | HeaderMetadata.Resolution, 256 | HeaderMetadata.Audio, 257 | HeaderMetadata.Subtitles, 258 | HeaderMetadata.Trailers, 259 | HeaderMetadata.Specials, 260 | HeaderMetadata.Path 261 | }; 262 | 263 | case ReportIncludeItemTypes.Video: 264 | case ReportIncludeItemTypes.MusicVideo: 265 | case ReportIncludeItemTypes.Trailer: 266 | case ReportIncludeItemTypes.BaseItem: 267 | default: 268 | return new List 269 | { 270 | HeaderMetadata.Status, 271 | HeaderMetadata.Locked, 272 | HeaderMetadata.ImagePrimary, 273 | HeaderMetadata.ImageBackdrop, 274 | HeaderMetadata.ImageLogo, 275 | HeaderMetadata.ImagePrimary, 276 | HeaderMetadata.ImageBackdrop, 277 | HeaderMetadata.ImageLogo, 278 | HeaderMetadata.Name, 279 | HeaderMetadata.DateAdded, 280 | HeaderMetadata.ReleaseDate, 281 | HeaderMetadata.Year, 282 | HeaderMetadata.Genres, 283 | HeaderMetadata.ParentalRating, 284 | HeaderMetadata.CommunityRating, 285 | HeaderMetadata.Runtime, 286 | HeaderMetadata.Video, 287 | HeaderMetadata.Resolution, 288 | HeaderMetadata.Audio, 289 | HeaderMetadata.Subtitles, 290 | HeaderMetadata.Trailers, 291 | HeaderMetadata.Specials 292 | }; 293 | 294 | } 295 | 296 | } 297 | 298 | /// Gets report option. 299 | /// The header. 300 | /// The sort field. 301 | /// The report option. 302 | private ReportOptions GetOption(HeaderMetadata header, string sortField = "") 303 | { 304 | HeaderMetadata internalHeader = header; 305 | 306 | ReportOptions option = new ReportOptions() 307 | { 308 | Header = new ReportHeader 309 | { 310 | HeaderFieldType = ReportFieldType.String, 311 | SortField = sortField, 312 | Type = "", 313 | ItemViewType = ItemViewType.None 314 | } 315 | }; 316 | 317 | switch (header) 318 | { 319 | case HeaderMetadata.Status: 320 | option.Header.ItemViewType = ItemViewType.StatusImage; 321 | internalHeader = HeaderMetadata.Status; 322 | option.Header.CanGroup = false; 323 | option.Header.DisplayType = ReportDisplayType.Screen; 324 | break; 325 | case HeaderMetadata.Locked: 326 | option.Column = (i, r) => this.GetBoolString(r.HasLockData); 327 | option.Header.ItemViewType = ItemViewType.LockDataImage; 328 | option.Header.CanGroup = false; 329 | option.Header.DisplayType = ReportDisplayType.Export; 330 | break; 331 | case HeaderMetadata.ImagePrimary: 332 | option.Column = (i, r) => this.GetBoolString(r.HasImageTagsPrimary); 333 | option.Header.ItemViewType = ItemViewType.TagsPrimaryImage; 334 | option.Header.CanGroup = false; 335 | option.Header.DisplayType = ReportDisplayType.Export; 336 | break; 337 | case HeaderMetadata.ImageBackdrop: 338 | option.Column = (i, r) => this.GetBoolString(r.HasImageTagsBackdrop); 339 | option.Header.ItemViewType = ItemViewType.TagsBackdropImage; 340 | option.Header.CanGroup = false; 341 | option.Header.DisplayType = ReportDisplayType.Export; 342 | break; 343 | case HeaderMetadata.ImageLogo: 344 | option.Column = (i, r) => this.GetBoolString(r.HasImageTagsLogo); 345 | option.Header.ItemViewType = ItemViewType.TagsLogoImage; 346 | option.Header.CanGroup = false; 347 | option.Header.DisplayType = ReportDisplayType.Export; 348 | break; 349 | 350 | case HeaderMetadata.Path: 351 | option.Column = (i, r) => i.Path; 352 | option.Header.SortField = "Path,SortName"; 353 | break; 354 | 355 | case HeaderMetadata.Name: 356 | option.Column = (i, r) => i.Name; 357 | option.Header.ItemViewType = ItemViewType.Detail; 358 | option.Header.SortField = "SortName"; 359 | break; 360 | 361 | case HeaderMetadata.DateAdded: 362 | option.Column = (i, r) => i.DateCreated; 363 | option.Header.SortField = "DateCreated,SortName"; 364 | option.Header.HeaderFieldType = ReportFieldType.DateTime; 365 | option.Header.Type = ""; 366 | break; 367 | 368 | case HeaderMetadata.PremiereDate: 369 | case HeaderMetadata.ReleaseDate: 370 | option.Column = (i, r) => i.PremiereDate; 371 | option.Header.HeaderFieldType = ReportFieldType.DateTime; 372 | option.Header.SortField = "ProductionYear,PremiereDate,SortName"; 373 | break; 374 | 375 | case HeaderMetadata.Runtime: 376 | option.Column = (i, r) => this.GetRuntimeDateTime(i.RunTimeTicks); 377 | option.Header.HeaderFieldType = ReportFieldType.Minutes; 378 | option.Header.SortField = "Runtime,SortName"; 379 | option.Header.CanGroup = false; 380 | break; 381 | 382 | case HeaderMetadata.PlayCount: 383 | option.Header.HeaderFieldType = ReportFieldType.Int; 384 | break; 385 | 386 | case HeaderMetadata.Season: 387 | option.Column = (i, r) => this.GetEpisode(i); 388 | option.Header.ItemViewType = ItemViewType.Detail; 389 | option.Header.SortField = "SortName"; 390 | break; 391 | 392 | case HeaderMetadata.SeasonNumber: 393 | option.Column = (i, r) => this.GetObject(i, (x) => x.IndexNumber == null ? "" : x.IndexNumber?.ToString(CultureInfo.InvariantCulture)); 394 | option.Header.SortField = "IndexNumber"; 395 | option.Header.HeaderFieldType = ReportFieldType.Int; 396 | break; 397 | 398 | case HeaderMetadata.Series: 399 | option.Column = (i, r) => this.GetObject(i, (x) => x.SeriesName); 400 | option.Header.ItemViewType = ItemViewType.Detail; 401 | option.Header.SortField = "SeriesSortName,SortName"; 402 | break; 403 | 404 | case HeaderMetadata.EpisodeSeries: 405 | option.Column = (i, r) => this.GetObject(i, (x) => x.SeriesName); 406 | option.Header.ItemViewType = ItemViewType.Detail; 407 | option.ItemID = (i) => 408 | { 409 | Series series = this.GetObject(i, (x) => x.Series); 410 | if (series == null) 411 | return string.Empty; 412 | return series.Id; 413 | }; 414 | option.Header.SortField = "SeriesSortName,SortName"; 415 | internalHeader = HeaderMetadata.Series; 416 | break; 417 | 418 | case HeaderMetadata.EpisodeSeason: 419 | option.Column = (i, r) => this.GetObject(i, (x) => x.SeriesName); 420 | option.Header.ItemViewType = ItemViewType.Detail; 421 | option.ItemID = (i) => 422 | { 423 | Season season = this.GetObject(i, (x) => x.Season); 424 | if (season == null) 425 | return string.Empty; 426 | return season.Id; 427 | }; 428 | option.Header.SortField = "SortName"; 429 | internalHeader = HeaderMetadata.Season; 430 | break; 431 | 432 | case HeaderMetadata.EpisodeNumber: 433 | option.Column = (i, r) => this.GetObject(i, (x) => x.IndexNumber == null ? "" : x.IndexNumber?.ToString(CultureInfo.InvariantCulture)); 434 | //option.Header.SortField = "IndexNumber"; 435 | //option.Header.HeaderFieldType = ReportFieldType.Int; 436 | break; 437 | 438 | case HeaderMetadata.Network: 439 | option.Column = (i, r) => this.GetListAsString(i.Studios.ToList()); 440 | option.ItemID = (i) => this.GetStudioID(i.Studios.FirstOrDefault()); 441 | option.Header.ItemViewType = ItemViewType.ItemByNameDetails; 442 | option.Header.SortField = "Studio,SortName"; 443 | break; 444 | 445 | case HeaderMetadata.Year: 446 | option.Column = (i, r) => this.GetSeriesProductionYear(i); 447 | option.Header.SortField = "ProductionYear,PremiereDate,SortName"; 448 | break; 449 | 450 | case HeaderMetadata.ParentalRating: 451 | option.Column = (i, r) => i.OfficialRating; 452 | option.Header.SortField = "OfficialRating,SortName"; 453 | break; 454 | 455 | case HeaderMetadata.CommunityRating: 456 | option.Column = (i, r) => i.CommunityRating; 457 | option.Header.SortField = "CommunityRating,SortName"; 458 | break; 459 | 460 | case HeaderMetadata.Trailers: 461 | option.Column = (i, r) => this.GetBoolString(r.HasLocalTrailer); 462 | option.Header.ItemViewType = ItemViewType.TrailersImage; 463 | break; 464 | 465 | case HeaderMetadata.Specials: 466 | option.Column = (i, r) => this.GetBoolString(r.HasSpecials); 467 | option.Header.ItemViewType = ItemViewType.SpecialsImage; 468 | break; 469 | 470 | case HeaderMetadata.AlbumArtist: 471 | option.Column = (i, r) => this.GetObject(i, (x) => x.AlbumArtist); 472 | option.ItemID = (i) => this.GetPersonID(this.GetObject(i, (x) => x.AlbumArtist)); 473 | option.Header.ItemViewType = ItemViewType.Detail; 474 | option.Header.SortField = "AlbumArtist,Album,SortName"; 475 | 476 | break; 477 | case HeaderMetadata.MusicArtist: 478 | option.Column = (i, r) => this.GetObject(i, (x) => x.GetLookupInfo().Name); 479 | option.Header.ItemViewType = ItemViewType.Detail; 480 | option.Header.SortField = "AlbumArtist,Album,SortName"; 481 | internalHeader = HeaderMetadata.AlbumArtist; 482 | break; 483 | case HeaderMetadata.AudioAlbumArtist: 484 | option.Column = (i, r) => this.GetListAsString(this.GetObject>(i, (x) => x.AlbumArtists.ToList())); 485 | option.Header.SortField = "AlbumArtist,Album,SortName"; 486 | internalHeader = HeaderMetadata.AlbumArtist; 487 | break; 488 | 489 | case HeaderMetadata.AudioAlbum: 490 | option.Column = (i, r) => this.GetObject(i, (x) => x.Album); 491 | option.Header.SortField = "Album,SortName"; 492 | internalHeader = HeaderMetadata.Album; 493 | break; 494 | 495 | case HeaderMetadata.Disc: 496 | option.Column = (i, r) => i.ParentIndexNumber; 497 | break; 498 | 499 | case HeaderMetadata.Track: 500 | option.Column = (i, r) => i.IndexNumber; 501 | break; 502 | 503 | case HeaderMetadata.Tracks: 504 | option.Column = (i, r) => this.GetObject>(i, (x) => x.Tracks.ToList(), new List