├── .editorconfig ├── .github ├── release-drafter.yml ├── renovate.json └── workflows │ ├── build.yaml │ ├── changelog.yaml │ ├── command-dispatch.yaml │ ├── command-rebase.yaml │ ├── publish-unstable.yaml │ ├── publish.yaml │ ├── scan-codeql.yaml │ ├── sync-labels.yaml │ └── test.yaml ├── .gitignore ├── Directory.Build.props ├── Jellyfin.Plugin.Reports.sln ├── Jellyfin.Plugin.Reports ├── Api │ ├── Activities │ │ └── ReportActivitiesBuilder.cs │ ├── Common │ │ ├── HeaderActivitiesMetadata.cs │ │ ├── HeaderMetadata.cs │ │ ├── ItemViewType.cs │ │ ├── ReportBuilderBase.cs │ │ ├── ReportDisplayType.cs │ │ ├── ReportExportType.cs │ │ ├── ReportFieldType.cs │ │ ├── ReportHeaderIdType.cs │ │ ├── ReportHelper.cs │ │ ├── ReportIncludeItemTypes.cs │ │ └── ReportViewType.cs │ ├── Data │ │ ├── ReportBuilder.cs │ │ ├── ReportExport.cs │ │ └── ReportOptions.cs │ ├── Model │ │ ├── ReportGroup.cs │ │ ├── ReportHeader.cs │ │ ├── ReportItem.cs │ │ ├── ReportResult.cs │ │ └── ReportRow.cs │ ├── ReportRequests.cs │ ├── ReportsController.cs │ └── ReportsService.cs ├── Configuration │ └── PluginConfiguration.cs ├── Jellyfin.Plugin.Reports.csproj ├── Plugin.cs └── Web │ ├── reports.html │ └── reports.js ├── LICENSE ├── README.md ├── build.yaml └── jellyfin.ruleset /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>jellyfin/.github//renovate-presets/default" 5 | ] 6 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /.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/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/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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17.0.0.0 4 | 17.0.0.0 5 | 17.0.0.0 6 | 7 | 8 | -------------------------------------------------------------------------------- /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/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.Plugin.Reports.Api.Common; 8 | using Jellyfin.Plugin.Reports.Api.Data; 9 | using Jellyfin.Plugin.Reports.Api.Model; 10 | using MediaBrowser.Controller.Library; 11 | using MediaBrowser.Model.Activity; 12 | using MediaBrowser.Model.Querying; 13 | 14 | namespace Jellyfin.Plugin.Reports.Api.Activities 15 | { 16 | /// A report activities builder. 17 | /// 18 | public class ReportActivitiesBuilder : ReportBuilderBase 19 | { 20 | /// 21 | /// Initializes a new instance of the MediaBrowser.Api.Reports.ReportActivitiesBuilder class. 22 | /// Manager for library. 23 | /// Manager for user. 24 | public ReportActivitiesBuilder(ILibraryManager libraryManager, IUserManager userManager) 25 | : base(libraryManager) 26 | { 27 | _userManager = userManager; 28 | } 29 | 30 | private readonly IUserManager _userManager; ///< Manager for user 31 | 32 | /// Gets a result. 33 | /// The query result. 34 | /// The request. 35 | /// The result. 36 | public ReportResult GetResult(QueryResult queryResult, IReportsQuery request) 37 | { 38 | ReportDisplayType displayType = ReportHelper.GetReportDisplayType(request.DisplayType); 39 | List> options = this.GetReportOptions(request, 40 | () => this.GetDefaultHeaderMetadata(), 41 | (hm) => this.GetOption(hm)).Where(x => this.DisplayTypeVisible(x.Header.DisplayType, displayType)).ToList(); 42 | 43 | var headers = GetHeaders(options); 44 | var rows = GetReportRows(queryResult.Items, options); 45 | 46 | ReportResult result = new ReportResult { Headers = headers }; 47 | HeaderMetadata groupBy = ReportHelper.GetHeaderMetadataType(request.GroupBy); 48 | int i = headers.FindIndex(x => x.FieldName == groupBy); 49 | if (groupBy != HeaderMetadata.None && i >= 0) 50 | { 51 | var rowsGroup = rows.SelectMany(x => x.Columns[i].Name.Split(';'), (x, g) => new { Group = g.Trim(), Rows = x }) 52 | .GroupBy(x => x.Group) 53 | .OrderBy(x => x.Key) 54 | .Select(x => new ReportGroup(x.Key, x.Select(r => r.Rows).ToList())); 55 | 56 | result.Groups = rowsGroup.ToList(); 57 | result.IsGrouped = true; 58 | } 59 | else 60 | { 61 | result.Rows = rows; 62 | result.IsGrouped = false; 63 | } 64 | 65 | return result; 66 | } 67 | 68 | /// Gets the headers. 69 | /// Type of the header. 70 | /// The request. 71 | /// The headers. 72 | /// 73 | protected internal override List GetHeaders(T request) 74 | { 75 | return this.GetHeaders(request, () => this.GetDefaultHeaderMetadata(), (hm) => this.GetOption(hm)); 76 | } 77 | 78 | /// Gets default header metadata. 79 | /// The default header metadata. 80 | private List GetDefaultHeaderMetadata() 81 | { 82 | return new List 83 | { 84 | HeaderMetadata.UserPrimaryImage, 85 | HeaderMetadata.Date, 86 | HeaderMetadata.User, 87 | HeaderMetadata.Type, 88 | HeaderMetadata.Severity, 89 | HeaderMetadata.Name, 90 | HeaderMetadata.ShortOverview, 91 | HeaderMetadata.Overview, 92 | //HeaderMetadata.UserId 93 | //HeaderMetadata.Item, 94 | }; 95 | } 96 | 97 | /// Gets an option. 98 | /// The header. 99 | /// The sort field. 100 | /// The option. 101 | private ReportOptions GetOption(HeaderMetadata header, string sortField = "") 102 | { 103 | HeaderMetadata internalHeader = header; 104 | 105 | ReportOptions option = new ReportOptions() 106 | { 107 | Header = new ReportHeader 108 | { 109 | HeaderFieldType = ReportFieldType.String, 110 | SortField = sortField, 111 | Type = "", 112 | ItemViewType = ItemViewType.None 113 | } 114 | }; 115 | 116 | switch (header) 117 | { 118 | case HeaderMetadata.Name: 119 | option.Column = (i, r) => i.Name; 120 | option.Header.SortField = ""; 121 | break; 122 | case HeaderMetadata.Overview: 123 | option.Column = (i, r) => i.Overview; 124 | option.Header.SortField = ""; 125 | option.Header.CanGroup = false; 126 | break; 127 | 128 | case HeaderMetadata.ShortOverview: 129 | option.Column = (i, r) => i.ShortOverview; 130 | option.Header.SortField = ""; 131 | option.Header.CanGroup = false; 132 | break; 133 | 134 | case HeaderMetadata.Type: 135 | option.Column = (i, r) => i.Type; 136 | option.Header.SortField = ""; 137 | break; 138 | 139 | case HeaderMetadata.Date: 140 | option.Column = (i, r) => i.Date; 141 | option.Header.SortField = ""; 142 | option.Header.HeaderFieldType = ReportFieldType.DateTime; 143 | option.Header.Type = ""; 144 | break; 145 | 146 | case HeaderMetadata.UserPrimaryImage: 147 | //option.Column = (i, r) => i.UserPrimaryImageTag; 148 | option.Header.DisplayType = ReportDisplayType.Screen; 149 | option.Header.ItemViewType = ItemViewType.UserPrimaryImage; 150 | option.Header.ShowHeaderLabel = false; 151 | internalHeader = HeaderMetadata.User; 152 | option.Header.CanGroup = false; 153 | option.Column = (i, r) => 154 | { 155 | if (i.UserId != Guid.Empty) 156 | { 157 | Jellyfin.Data.Entities.User user = _userManager.GetUserById(i.UserId); 158 | if (user != null) 159 | { 160 | var dto = _userManager.GetUserDto(user); 161 | return dto.PrimaryImageTag; 162 | } 163 | } 164 | return string.Empty; 165 | }; 166 | option.Header.SortField = ""; 167 | break; 168 | case HeaderMetadata.Severity: 169 | option.Column = (i, r) => i.Severity; 170 | option.Header.SortField = ""; 171 | break; 172 | case HeaderMetadata.Item: 173 | option.Column = (i, r) => i.ItemId; 174 | option.Header.SortField = ""; 175 | break; 176 | case HeaderMetadata.User: 177 | option.Column = (i, r) => 178 | { 179 | if (i.UserId != Guid.Empty) 180 | { 181 | Jellyfin.Data.Entities.User user = _userManager.GetUserById(i.UserId); 182 | if (user != null) 183 | return user.Username; 184 | } 185 | return string.Empty; 186 | }; 187 | option.Header.SortField = ""; 188 | break; 189 | case HeaderMetadata.UserId: 190 | option.Column = (i, r) => i.UserId; 191 | option.Header.SortField = ""; 192 | break; 193 | } 194 | 195 | option.Header.Name = GetLocalizedHeader(internalHeader); 196 | option.Header.FieldName = header; 197 | 198 | return option; 199 | } 200 | 201 | /// Gets report rows. 202 | /// The items. 203 | /// Options for controlling the operation. 204 | /// The report rows. 205 | private List GetReportRows(IEnumerable items, List> options) 206 | { 207 | var rows = new List(); 208 | 209 | foreach (ActivityLogEntry item in items) 210 | { 211 | ReportRow rRow = GetRow(item); 212 | foreach (ReportOptions option in options) 213 | { 214 | object itemColumn = option.Column != null ? option.Column(item, rRow) : ""; 215 | object itemId = option.ItemID != null ? option.ItemID(item) : ""; 216 | ReportItem rItem = new ReportItem 217 | { 218 | Name = ReportHelper.ConvertToString(itemColumn, option.Header.HeaderFieldType), 219 | Id = ReportHelper.ConvertToString(itemId, ReportFieldType.Object) 220 | }; 221 | rRow.Columns.Add(rItem); 222 | } 223 | 224 | rows.Add(rRow); 225 | } 226 | 227 | return rows; 228 | } 229 | 230 | /// Gets a row. 231 | /// The item. 232 | /// The row. 233 | private ReportRow GetRow(ActivityLogEntry item) 234 | { 235 | ReportRow rRow = new ReportRow 236 | { 237 | Id = item.Id.ToString(CultureInfo.InvariantCulture), 238 | UserId = item.UserId 239 | }; 240 | return rRow; 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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.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 | -------------------------------------------------------------------------------- /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/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