├── .editorconfig ├── .gitattributes ├── .gitignore ├── .vscode └── tasks.json ├── Jellyfin.Plugin.Enigma2.sln ├── Jellyfin.Plugin.Enigma2 ├── Configuration │ ├── PluginConfiguration.cs │ └── configPage.html ├── Helpers │ └── GeneralHelpers.cs ├── Images │ └── thumb.png ├── Jellyfin.Plugin.Enigma2.csproj ├── LiveTvService.cs ├── Plugin.cs ├── RecordingsChannel.cs └── ServiceRegistrator.cs ├── LICENSE ├── README.md └── build.yaml /.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 | 9 | # All files 10 | [*] 11 | indent_style = space 12 | indent_size = 4 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | end_of_line = lf 17 | max_line_length = 9999 18 | 19 | # YAML indentation 20 | [*.{yml,yaml}] 21 | indent_size = 2 22 | 23 | # XML indentation 24 | [*.{csproj,xml}] 25 | indent_size = 2 26 | 27 | ############################### 28 | # .NET Coding Conventions # 29 | ############################### 30 | 31 | [*.{cs,vb}] 32 | # Organize usings 33 | dotnet_sort_system_directives_first = true 34 | # this. preferences 35 | dotnet_style_qualification_for_field = false:silent 36 | dotnet_style_qualification_for_property = false:silent 37 | dotnet_style_qualification_for_method = false:silent 38 | dotnet_style_qualification_for_event = false:silent 39 | # Language keywords vs BCL types preferences 40 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 41 | dotnet_style_predefined_type_for_member_access = true:silent 42 | # Parentheses preferences 43 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 44 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 45 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 46 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 47 | # Modifier preferences 48 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 49 | dotnet_style_readonly_field = true:suggestion 50 | # Expression-level preferences 51 | dotnet_style_object_initializer = true:suggestion 52 | dotnet_style_collection_initializer = true:suggestion 53 | dotnet_style_explicit_tuple_names = true:suggestion 54 | dotnet_style_null_propagation = true:suggestion 55 | dotnet_style_coalesce_expression = true:suggestion 56 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent 57 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 58 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 59 | dotnet_style_prefer_auto_properties = true:silent 60 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 61 | dotnet_style_prefer_conditional_expression_over_return = true:silent 62 | 63 | ############################### 64 | # Naming Conventions # 65 | ############################### 66 | 67 | # Style Definitions (From Roslyn) 68 | 69 | # Non-private static fields are PascalCase 70 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion 71 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields 72 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style 73 | 74 | dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field 75 | dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected 76 | dotnet_naming_symbols.non_private_static_fields.required_modifiers = static 77 | 78 | dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case 79 | 80 | # Constants are PascalCase 81 | dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion 82 | dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants 83 | dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style 84 | 85 | dotnet_naming_symbols.constants.applicable_kinds = field, local 86 | dotnet_naming_symbols.constants.required_modifiers = const 87 | 88 | dotnet_naming_style.constant_style.capitalization = pascal_case 89 | 90 | # Static fields are camelCase and start with s_ 91 | dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion 92 | dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields 93 | dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style 94 | 95 | dotnet_naming_symbols.static_fields.applicable_kinds = field 96 | dotnet_naming_symbols.static_fields.required_modifiers = static 97 | 98 | dotnet_naming_style.static_field_style.capitalization = camel_case 99 | dotnet_naming_style.static_field_style.required_prefix = _ 100 | 101 | # Instance fields are camelCase and start with _ 102 | dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion 103 | dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields 104 | dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style 105 | 106 | dotnet_naming_symbols.instance_fields.applicable_kinds = field 107 | 108 | dotnet_naming_style.instance_field_style.capitalization = camel_case 109 | dotnet_naming_style.instance_field_style.required_prefix = _ 110 | 111 | # Locals and parameters are camelCase 112 | dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion 113 | dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters 114 | dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style 115 | 116 | dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local 117 | 118 | dotnet_naming_style.camel_case_style.capitalization = camel_case 119 | 120 | # Local functions are PascalCase 121 | dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion 122 | dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions 123 | dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style 124 | 125 | dotnet_naming_symbols.local_functions.applicable_kinds = local_function 126 | 127 | dotnet_naming_style.local_function_style.capitalization = pascal_case 128 | 129 | # By default, name items with PascalCase 130 | dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion 131 | dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members 132 | dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style 133 | 134 | dotnet_naming_symbols.all_members.applicable_kinds = * 135 | 136 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 137 | 138 | ############################### 139 | # C# Coding Conventions # 140 | ############################### 141 | 142 | [*.cs] 143 | # var preferences 144 | csharp_style_var_for_built_in_types = true:silent 145 | csharp_style_var_when_type_is_apparent = true:silent 146 | csharp_style_var_elsewhere = true:silent 147 | # Expression-bodied members 148 | csharp_style_expression_bodied_methods = false:silent 149 | csharp_style_expression_bodied_constructors = false:silent 150 | csharp_style_expression_bodied_operators = false:silent 151 | csharp_style_expression_bodied_properties = true:silent 152 | csharp_style_expression_bodied_indexers = true:silent 153 | csharp_style_expression_bodied_accessors = true:silent 154 | # Pattern matching preferences 155 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 156 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 157 | # Null-checking preferences 158 | csharp_style_throw_expression = true:suggestion 159 | csharp_style_conditional_delegate_call = true:suggestion 160 | # Modifier preferences 161 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 162 | # Expression-level preferences 163 | csharp_prefer_braces = true:silent 164 | csharp_style_deconstructed_variable_declaration = true:suggestion 165 | csharp_prefer_simple_default_expression = true:suggestion 166 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 167 | csharp_style_inlined_variable_declaration = true:suggestion 168 | 169 | ############################### 170 | # C# Formatting Rules # 171 | ############################### 172 | 173 | # New line preferences 174 | csharp_new_line_before_open_brace = all 175 | csharp_new_line_before_else = true 176 | csharp_new_line_before_catch = true 177 | csharp_new_line_before_finally = true 178 | csharp_new_line_before_members_in_object_initializers = true 179 | csharp_new_line_before_members_in_anonymous_types = true 180 | csharp_new_line_between_query_expression_clauses = true 181 | # Indentation preferences 182 | csharp_indent_case_contents = true 183 | csharp_indent_switch_labels = true 184 | csharp_indent_labels = flush_left 185 | # Space preferences 186 | csharp_space_after_cast = false 187 | csharp_space_after_keywords_in_control_flow_statements = true 188 | csharp_space_between_method_call_parameter_list_parentheses = false 189 | csharp_space_between_method_declaration_parameter_list_parentheses = false 190 | csharp_space_between_parentheses = false 191 | csharp_space_before_colon_in_inheritance_clause = true 192 | csharp_space_after_colon_in_inheritance_clause = true 193 | csharp_space_around_binary_operators = before_and_after 194 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 195 | csharp_space_between_method_call_name_and_opening_parenthesis = false 196 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 197 | # Wrapping preferences 198 | csharp_preserve_single_line_statements = true 199 | csharp_preserve_single_line_blocks = true 200 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build Folders (you can keep bin if you'd like, to store dlls and pdbs) 2 | [Bb]in/ 3 | [Oo]bj/ 4 | 5 | # mstest test results 6 | TestResults 7 | 8 | ## Ignore Visual Studio temporary files, build results, and 9 | ## files generated by popular Visual Studio add-ons. 10 | 11 | # User-specific files 12 | *.suo 13 | *.user 14 | *.sln.docstates 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Rr]elease/ 19 | x64/ 20 | *_i.c 21 | *_p.c 22 | *.ilk 23 | *.meta 24 | *.obj 25 | *.pch 26 | *.pdb 27 | *.pgc 28 | *.pgd 29 | *.rsp 30 | *.sbr 31 | *.tlb 32 | *.tli 33 | *.tlh 34 | *.tmp 35 | *.log 36 | *.vspscc 37 | *.vssscc 38 | .builds 39 | 40 | # Visual C++ cache files 41 | ipch/ 42 | *.aps 43 | *.ncb 44 | *.opensdf 45 | *.sdf 46 | 47 | # Visual Studio profiler 48 | *.psess 49 | *.vsp 50 | *.vspx 51 | 52 | # Guidance Automation Toolkit 53 | *.gpState 54 | 55 | # ReSharper is a .NET coding add-in 56 | _ReSharper* 57 | 58 | # NCrunch 59 | *.ncrunch* 60 | .*crunch*.local.xml 61 | 62 | # Installshield output folder 63 | [Ee]xpress 64 | 65 | # DocProject is a documentation generator add-in 66 | DocProject/buildhelp/ 67 | DocProject/Help/*.HxT 68 | DocProject/Help/*.HxC 69 | DocProject/Help/*.hhc 70 | DocProject/Help/*.hhk 71 | DocProject/Help/*.hhp 72 | DocProject/Help/Html2 73 | DocProject/Help/html 74 | 75 | # Click-Once directory 76 | publish 77 | 78 | # Publish Web Output 79 | *.Publish.xml 80 | 81 | # NuGet Packages Directory 82 | packages 83 | 84 | # Windows Azure Build Output 85 | csx 86 | *.build.csdef 87 | 88 | # Windows Store app package directory 89 | AppPackages/ 90 | 91 | # Others 92 | [Bb]in 93 | [Oo]bj 94 | sql 95 | TestResults 96 | [Tt]est[Rr]esult* 97 | *.Cache 98 | ClientBin 99 | [Ss]tyle[Cc]op.* 100 | ~$* 101 | *.dbmdl 102 | Generated_Code #added for RIA/Silverlight projects 103 | 104 | # Backup & report files from converting an old project file to a newer 105 | # Visual Studio version. Backup files are not needed, because we have git ;-) 106 | _UpgradeReport_Files/ 107 | Backup*/ 108 | UpgradeLog*.XML 109 | 110 | .vs -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/Jellyfin.Plugin.Enigma2/Jellyfin.Plugin.Enigma2.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/Jellyfin.Plugin.Enigma2/Jellyfin.Plugin.Enigma2.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/Jellyfin.Plugin.Enigma2/Jellyfin.Plugin.Enigma2.csproj" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /Jellyfin.Plugin.Enigma2.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32616.157 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Plugin.Enigma2", "Jellyfin.Plugin.Enigma2\Jellyfin.Plugin.Enigma2.csproj", "{52B1ABDF-09C1-450E-B808-BBA0A9C19848}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A968FB09-8732-407C-8748-6DB383EB5D91}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | EndProjectSection 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Any CPU = Debug|Any CPU 16 | Release|Any CPU = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {52B1ABDF-09C1-450E-B808-BBA0A9C19848}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {52B1ABDF-09C1-450E-B808-BBA0A9C19848}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {52B1ABDF-09C1-450E-B808-BBA0A9C19848}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {52B1ABDF-09C1-450E-B808-BBA0A9C19848}.Release|Any CPU.Build.0 = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(SolutionProperties) = preSolution 25 | HideSolutionNode = FALSE 26 | EndGlobalSection 27 | GlobalSection(ExtensibilityGlobals) = postSolution 28 | SolutionGuid = {347CA3B3-ECDC-4C93-A92F-946E2BC0D1B9} 29 | EndGlobalSection 30 | EndGlobal 31 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Enigma2/Configuration/PluginConfiguration.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Model.Plugins; 2 | 3 | namespace Jellyfin.Plugin.Enigma2.Configuration 4 | { 5 | /// 6 | /// Class PluginConfiguration 7 | /// 8 | public class PluginConfiguration : BasePluginConfiguration 9 | { 10 | public string HostName { get; set; } 11 | public string StreamingPort { get; set; } 12 | public string WebInterfacePort { get; set; } 13 | public string WebInterfaceUsername { get; set; } 14 | public string WebInterfacePassword { get; set; } 15 | public bool UseLoginForStreams { get; set; } 16 | public bool UseSecureHTTPS { get; set; } 17 | public bool UseSecureHTTPSForStreams { get; set; } 18 | public bool OnlyOneBouquet { get; set; } 19 | public string TVBouquet { get; set; } 20 | public bool ZapToChannel { get; set; } 21 | public bool FetchPiconsFromWebInterface { get; set; } 22 | public string PiconsPath { get; set; } 23 | 24 | public string RecordingPath { get; set; } 25 | 26 | public bool TranscodedStream { get; set; } 27 | 28 | public string TranscodingPort { get; set; } 29 | 30 | public bool EnableDebugLogging { get; set; } 31 | 32 | 33 | public PluginConfiguration() 34 | { 35 | HostName = "localhost"; 36 | StreamingPort = "8001"; 37 | WebInterfacePort = "8000"; 38 | WebInterfaceUsername = ""; 39 | WebInterfacePassword = ""; 40 | UseLoginForStreams = false; 41 | UseSecureHTTPS = false; 42 | UseSecureHTTPSForStreams = false; 43 | OnlyOneBouquet = true; 44 | TVBouquet = "Favourites (TV)"; 45 | ZapToChannel = false; 46 | FetchPiconsFromWebInterface = true; 47 | PiconsPath = ""; 48 | 49 | RecordingPath = ""; 50 | 51 | TranscodedStream = false; 52 | TranscodingPort = "8002"; 53 | 54 | EnableDebugLogging = false; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Enigma2/Configuration/configPage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Enigma2 5 | 6 | 7 |
8 | 9 |
10 |
11 | 12 |

Enigma2

13 | 14 |
15 | 16 |
17 | 18 |
19 | The host name (address) or ip address of your receiver 20 |
21 |
22 |
23 | 24 |
25 | The Streaming port of your receiver eg. 8001 26 |
27 |
28 |
29 | 30 |
31 | The web interface port of your receiver eg. 8000 32 |
33 |
34 |
35 | 36 |
37 | The web interface username of your receiver (optional) 38 |
39 |
40 |
41 | 42 |
43 | The web interface password of your receiver (optional) 44 |
45 |
46 | 47 |
48 | 52 |
53 | Use web interface login for streaming 54 |
55 |
56 | 57 |
58 | 62 |
63 | Use HTTPS to connect to your receiver 64 |
65 |
66 | 67 |
68 | 72 |
73 | Use HTTPS to connect to streaming port 74 |
75 |
76 | 77 |
78 | 82 |
83 | Limit channels to only those contained within the specified TV Bouquet below (optional) 84 |
85 |
86 | 87 | 88 |
89 | 90 |
91 | The TV Bouquet to load channels for (optional) 92 |
93 |
94 | 95 |
96 | 100 |
101 | Set if only one tuner within receiver 102 |
103 |
104 | 105 |
106 | 110 |
111 | Set if you want to retrieve Picons from the web interface of the receiver 112 |
113 |
114 | 115 | 116 |
117 | 118 |
119 | The local location of your Picons eg. C:\Picons\ (optional) 120 |
121 |
122 | 123 |
124 | 125 |
126 | The location to store your recordings on your receiver eg. /hdd/movie/ (optional) 127 |
128 |
129 | 130 |
131 | 135 |
136 | Request Transcoded Stream (h264, 720p, 1Mpbs and mp4 as default) 137 |
138 |
139 |
140 | 141 |
142 | The Transcoding port of your receiver eg. 8002 143 |
144 |
145 | 146 | 150 | 151 |
152 | 153 |
154 | 155 |
156 |
157 |
158 | 159 | 160 | 226 |
227 | 228 | 229 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Enigma2/Helpers/GeneralHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MediaBrowser.Model.LiveTv; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace Jellyfin.Plugin.Enigma2.Helpers 6 | { 7 | public static class ChannelHelper 8 | { 9 | public static ChannelType GetChannelType(string channelType) 10 | { 11 | var type = new ChannelType(); 12 | 13 | if (channelType == "0x1") 14 | { 15 | type = ChannelType.TV; 16 | } 17 | else if (channelType == "0xa") 18 | { 19 | type = ChannelType.Radio; 20 | } 21 | 22 | return type; 23 | } 24 | } 25 | 26 | public static class UtilsHelper 27 | { 28 | public static void DebugInformation(ILogger logger, string message) 29 | { 30 | var config = Plugin.Instance.Configuration; 31 | var enableDebugLogging = config.EnableDebugLogging; 32 | 33 | if (enableDebugLogging) 34 | { 35 | logger.LogDebug(message); 36 | } 37 | } 38 | 39 | } 40 | 41 | public static class RecordingHelper 42 | { 43 | 44 | } 45 | 46 | public static class ApiHelper 47 | { 48 | private static readonly DateTime UnixEpoch = 49 | new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); 50 | 51 | public static long GetCurrentUnixTimestampMillis() 52 | { 53 | return (long)(DateTime.UtcNow - UnixEpoch).TotalMilliseconds; 54 | } 55 | 56 | public static DateTime DateTimeFromUnixTimestampMillis(long millis) 57 | { 58 | return UnixEpoch.AddMilliseconds(millis); 59 | } 60 | 61 | public static long GetCurrentUnixTimestampSeconds(DateTime date) 62 | { 63 | return (long)(date - UnixEpoch).TotalSeconds; 64 | } 65 | 66 | public static DateTime DateTimeFromUnixTimestampSeconds(long seconds) 67 | { 68 | return UnixEpoch.AddSeconds(seconds); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Enigma2/Images/thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knackebrot/jellyfin-plugin-enigma2/d38c1595d8fdf129dcdef1f78da4935cf7b3358c/Jellyfin.Plugin.Enigma2/Images/thumb.png -------------------------------------------------------------------------------- /Jellyfin.Plugin.Enigma2/Jellyfin.Plugin.Enigma2.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6.0.0.0 6 | 6.0.0.0 7 | 8 | 9 | 10 | none 11 | false 12 | 13 | 14 | 15 | none 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Enigma2/LiveTvService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.IO; 5 | using System.Net; 6 | using System.Net.Http; 7 | using System.Net.Http.Headers; 8 | using System.Text; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using System.Xml; 12 | using MediaBrowser.Common.Extensions; 13 | using MediaBrowser.Common.Net; 14 | using MediaBrowser.Controller.Drawing; 15 | using MediaBrowser.Controller.LiveTv; 16 | using MediaBrowser.Model.Dto; 17 | using MediaBrowser.Model.Entities; 18 | using MediaBrowser.Model.LiveTv; 19 | using MediaBrowser.Model.MediaInfo; 20 | using Jellyfin.Plugin.Enigma2.Helpers; 21 | using Microsoft.Extensions.Logging; 22 | 23 | namespace Jellyfin.Plugin.Enigma2 24 | { 25 | /// 26 | /// Class LiveTvService 27 | /// 28 | public class LiveTvService : ILiveTvService 29 | { 30 | private readonly ILogger _logger; 31 | private int _liveStreams; 32 | 33 | private string tvBouquetSRef; 34 | private List tvChannelInfos = new List(); 35 | 36 | public DateTime LastRecordingChange = DateTime.MinValue; 37 | 38 | private IHttpClientFactory _httpClientFactory; 39 | 40 | public static LiveTvService Instance { get; private set; } 41 | 42 | public LiveTvService(ILogger logger, IHttpClientFactory httpClientFactory) 43 | { 44 | _logger = logger; 45 | _httpClientFactory = httpClientFactory; 46 | Instance = this; 47 | } 48 | 49 | 50 | /// 51 | /// Ensure that we are connected to the Enigma2 server 52 | /// 53 | /// 54 | /// 55 | public async Task EnsureConnectionAsync(CancellationToken cancellationToken) 56 | { 57 | _logger.LogInformation("[Enigma2] Start EnsureConnectionAsync"); 58 | 59 | var config = Plugin.Instance.Configuration; 60 | 61 | // log settings 62 | _logger.LogInformation(string.Format("[Enigma2] EnsureConnectionAsync HostName: {0}", config.HostName)); 63 | _logger.LogInformation(string.Format("[Enigma2] EnsureConnectionAsync StreamingPort: {0}", config.StreamingPort)); 64 | _logger.LogInformation(string.Format("[Enigma2] EnsureConnectionAsync WebInterfacePort: {0}", config.WebInterfacePort)); 65 | if (string.IsNullOrEmpty(config.WebInterfaceUsername)) 66 | { 67 | _logger.LogInformation("[Enigma2] EnsureConnectionAsync WebInterfaceUsername: "); 68 | } 69 | else 70 | { 71 | _logger.LogInformation(string.Format("[Enigma2] EnsureConnectionAsync WebInterfaceUsername: {0}", "********")); 72 | } 73 | 74 | if (string.IsNullOrEmpty(config.WebInterfacePassword)) 75 | { 76 | _logger.LogInformation("[Enigma2] EnsureConnectionAsync WebInterfacePassword: "); 77 | } 78 | else 79 | { 80 | _logger.LogInformation(string.Format("[Enigma2] EnsureConnectionAsync WebInterfaceUsername: {0}", "********")); 81 | } 82 | 83 | _logger.LogInformation(string.Format("[Enigma2] EnsureConnectionAsync UseLoginForStreams: {0}", config.UseLoginForStreams)); 84 | _logger.LogInformation(string.Format("[Enigma2] EnsureConnectionAsync UseSecureHTTPS: {0}", config.UseSecureHTTPS)); 85 | _logger.LogInformation(string.Format("[Enigma2] EnsureConnectionAsync UseSecureHTTPSForStreams: {0}", config.UseSecureHTTPSForStreams)); 86 | _logger.LogInformation(string.Format("[Enigma2] EnsureConnectionAsync OnlyOneBouquet: {0}", config.OnlyOneBouquet)); 87 | _logger.LogInformation(string.Format("[Enigma2] EnsureConnectionAsync TVBouquet: {0}", config.TVBouquet)); 88 | _logger.LogInformation(string.Format("[Enigma2] EnsureConnectionAsync ZapToChannel: {0}", config.ZapToChannel)); 89 | _logger.LogInformation(string.Format("[Enigma2] EnsureConnectionAsync FetchPiconsFromWebInterface: {0}", config.FetchPiconsFromWebInterface)); 90 | _logger.LogInformation(string.Format("[Enigma2] EnsureConnectionAsync PiconsPath: {0}", config.PiconsPath)); 91 | _logger.LogInformation(string.Format("[Enigma2] EnsureConnectionAsync RecordingPath: {0}", config.RecordingPath)); 92 | _logger.LogInformation(string.Format("[Enigma2] EnsureConnectionAsync EnableDebugLogging: {0}", config.EnableDebugLogging)); 93 | 94 | // validate settings 95 | if (string.IsNullOrEmpty(config.HostName)) 96 | { 97 | _logger.LogError("[Enigma2] HostName must be configured."); 98 | throw new InvalidOperationException("Enigma2 HostName must be configured."); 99 | } 100 | 101 | if (string.IsNullOrEmpty(config.StreamingPort)) 102 | { 103 | _logger.LogError("[Enigma2] Streaming Port must be configured."); 104 | throw new InvalidOperationException("Enigma2 Streaming Port must be configured."); 105 | } 106 | 107 | if (config.TranscodedStream && string.IsNullOrEmpty(config.TranscodingPort)) 108 | { 109 | _logger.LogError("[Enigma2] Transcoding Port must be configured."); 110 | throw new InvalidOperationException("Enigma2 Transcoding Port must be configured."); 111 | } 112 | 113 | if (string.IsNullOrEmpty(config.WebInterfacePort)) 114 | { 115 | _logger.LogError("[Enigma2] Web Interface Port must be configured."); 116 | throw new InvalidOperationException("Enigma2 Web Interface Port must be configured."); 117 | } 118 | 119 | if (config.OnlyOneBouquet) 120 | { 121 | if (string.IsNullOrEmpty(config.TVBouquet)) 122 | { 123 | _logger.LogError("[Enigma2] TV Bouquet must be configured if Fetch only one TV bouquet selected."); 124 | throw new InvalidOperationException("Enigma2 TVBouquet must be configured if Fetch only one TV bouquet selected."); 125 | } 126 | } 127 | 128 | if (!config.FetchPiconsFromWebInterface) 129 | { 130 | if (string.IsNullOrEmpty(config.PiconsPath)) 131 | { 132 | _logger.LogError("[Enigma2] Picons location must be configured if Fetch Picons from Web Service is disabled."); 133 | throw new InvalidOperationException("Enigma2 Picons location must be configured if Fetch Picons from Web Service is disabled."); 134 | } 135 | } 136 | 137 | _logger.LogInformation("[Enigma2] EnsureConnectionAsync Validation of config parameters completed"); 138 | 139 | if (config.OnlyOneBouquet) 140 | { 141 | // connect to Enigma2 box to test connectivity and at same time get sRef for TV Bouquet. 142 | tvBouquetSRef = await InitiateSession(cancellationToken, config.TVBouquet).ConfigureAwait(false); 143 | } 144 | else 145 | { 146 | // connect to Enigma2 box to test connectivity. 147 | var resultNotRequired = await InitiateSession(cancellationToken, null).ConfigureAwait(false); 148 | tvBouquetSRef = null; 149 | } 150 | } 151 | 152 | /// 153 | /// Creates HttpClient for connection to Enigma2 154 | /// 155 | /// 156 | /// 157 | private HttpClient GetHttpClient() 158 | { 159 | var httpClient = _httpClientFactory.CreateClient(NamedClient.Default); 160 | httpClient.DefaultRequestHeaders.UserAgent.Add( 161 | new ProductInfoHeaderValue(Name, Plugin.Instance.Version.ToString())); 162 | 163 | if (!string.IsNullOrEmpty(Plugin.Instance.Configuration.WebInterfaceUsername)) 164 | { 165 | var authInfo = Plugin.Instance.Configuration.WebInterfaceUsername + ":" + Plugin.Instance.Configuration.WebInterfacePassword; 166 | authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo)); 167 | httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authInfo); 168 | } 169 | 170 | return httpClient; 171 | } 172 | 173 | /// 174 | /// Checks connection to Enigma2 and retrieves service reference for channel if only one bouquet. 175 | /// 176 | /// The cancellation token. 177 | /// The TV Bouquet. 178 | /// Task{String>}. 179 | public async Task InitiateSession(CancellationToken cancellationToken, string tvBouquet) 180 | { 181 | _logger.LogInformation("[Enigma2] Start InitiateSession, validates connection and returns Bouquet reference if required"); 182 | //await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); 183 | 184 | var protocol = "http"; 185 | if (Plugin.Instance.Configuration.UseSecureHTTPS) 186 | { 187 | protocol = "https"; 188 | } 189 | 190 | var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; 191 | 192 | var url = string.Format("{0}/web/getservices", baseUrl); 193 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] InitiateSession url: {0}", url)); 194 | 195 | using (var stream = await GetHttpClient().GetStreamAsync(url, cancellationToken).ConfigureAwait(false)) 196 | { 197 | using (var reader = new StreamReader(stream)) 198 | { 199 | var xmlResponse = reader.ReadToEnd(); 200 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] InitiateSession response: {0}", xmlResponse)); 201 | 202 | try 203 | { 204 | var xml = new XmlDocument(); 205 | xml.LoadXml(xmlResponse); 206 | 207 | string tvBouquetReference = null; 208 | 209 | var e2services = xml.GetElementsByTagName("e2service"); 210 | 211 | // If TV Bouquet passed find associated service reference 212 | if (!string.IsNullOrEmpty(tvBouquet)) 213 | { 214 | foreach (XmlNode xmlNode in e2services) 215 | { 216 | var channelInfo = new ChannelInfo(); 217 | 218 | var e2servicereference = "?"; 219 | var e2servicename = "?"; 220 | 221 | foreach (XmlNode node in xmlNode.ChildNodes) 222 | { 223 | if (node.Name == "e2servicereference") 224 | { 225 | e2servicereference = node.InnerText; 226 | } 227 | else if (node.Name == "e2servicename") 228 | { 229 | e2servicename = node.InnerText; 230 | } 231 | } 232 | if (tvBouquet == e2servicename) 233 | { 234 | tvBouquetReference = e2servicereference; 235 | return tvBouquetReference; 236 | } 237 | } 238 | // make sure we have found the TV Bouquet 239 | if (!string.IsNullOrEmpty(tvBouquet)) 240 | { 241 | _logger.LogError("[Enigma2] Failed to find TV Bouquet specified in Enigma2 configuration."); 242 | throw new ApplicationException("Failed to find TV Bouquet specified in Enigma2 configuration."); 243 | } 244 | } 245 | return tvBouquetReference; 246 | } 247 | catch (Exception e) 248 | { 249 | _logger.LogError("[Enigma2] Failed to parse services information."); 250 | _logger.LogError(string.Format("[Enigma2] InitiateSession error: {0}", e.Message)); 251 | throw new ApplicationException("Failed to connect to Enigma2."); 252 | } 253 | 254 | } 255 | } 256 | } 257 | 258 | 259 | /// 260 | /// Gets the channels async. 261 | /// 262 | /// The cancellation token. 263 | /// Task{IEnumerable{ChannelInfo}}. 264 | public async Task> GetChannelsAsync(CancellationToken cancellationToken) 265 | { 266 | _logger.LogInformation("[Enigma2] Start GetChannelsAsync, retrieve all channels"); 267 | await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); 268 | 269 | var protocol = "http"; 270 | if (Plugin.Instance.Configuration.UseSecureHTTPS) 271 | { 272 | protocol = "https"; 273 | } 274 | 275 | var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; 276 | 277 | var baseUrlPicon = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; 278 | 279 | string url; 280 | if (string.IsNullOrEmpty(tvBouquetSRef)) 281 | { 282 | url = string.Format("{0}/web/getservices", baseUrl); 283 | } 284 | else 285 | { 286 | url = string.Format("{0}/web/getservices?sRef={1}", baseUrl, tvBouquetSRef); 287 | } 288 | 289 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] GetChannelsAsync url: {0}", url)); 290 | 291 | if (!string.IsNullOrEmpty(Plugin.Instance.Configuration.WebInterfaceUsername)) 292 | { 293 | baseUrlPicon = protocol + "://" + Plugin.Instance.Configuration.WebInterfaceUsername + ":" + Plugin.Instance.Configuration.WebInterfacePassword + "@" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; 294 | } 295 | 296 | using (var stream = await GetHttpClient().GetStreamAsync(url, cancellationToken).ConfigureAwait(false)) 297 | { 298 | using (var reader = new StreamReader(stream)) 299 | { 300 | 301 | var xmlResponse = reader.ReadToEnd(); 302 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] GetChannelsAsync response: {0}", xmlResponse)); 303 | 304 | try 305 | { 306 | var xml = new XmlDocument(); 307 | xml.LoadXml(xmlResponse); 308 | 309 | var channelInfos = new List(); 310 | 311 | if (string.IsNullOrEmpty(tvBouquetSRef)) 312 | { 313 | // Load channels from all TV Bouquets 314 | _logger.LogInformation("[Enigma2] GetChannelsAsync for all TV Bouquets"); 315 | 316 | var e2services = xml.GetElementsByTagName("e2service"); 317 | foreach (XmlNode xmlNode in e2services) 318 | { 319 | var channelInfo = new ChannelInfo(); 320 | var e2servicereference = "?"; 321 | var e2servicename = "?"; 322 | 323 | foreach (XmlNode node in xmlNode.ChildNodes) 324 | { 325 | if (node.Name == "e2servicereference") 326 | { 327 | e2servicereference = node.InnerText; 328 | } 329 | else if (node.Name == "e2servicename") 330 | { 331 | e2servicename = node.InnerText; 332 | } 333 | } 334 | 335 | // get all channels for TV Bouquet 336 | var channelInfosForBouquet = await GetChannelsForTVBouquetAsync(cancellationToken, e2servicereference).ConfigureAwait(false); 337 | 338 | // store all channels for TV Bouquet 339 | channelInfos.AddRange(channelInfosForBouquet); 340 | } 341 | 342 | return channelInfos; 343 | } 344 | else 345 | { 346 | // Load channels for specified TV Bouquet only 347 | var count = 1; 348 | 349 | var e2services = xml.GetElementsByTagName("e2service"); 350 | foreach (XmlNode xmlNode in e2services) 351 | { 352 | var channelInfo = new ChannelInfo(); 353 | 354 | var e2servicereference = "?"; 355 | var e2servicename = "?"; 356 | 357 | foreach (XmlNode node in xmlNode.ChildNodes) 358 | { 359 | if (node.Name == "e2servicereference") 360 | { 361 | e2servicereference = node.InnerText; 362 | } 363 | else if (node.Name == "e2servicename") 364 | { 365 | e2servicename = node.InnerText; 366 | } 367 | } 368 | 369 | // Check whether the current element is not just a label 370 | if (!e2servicereference.StartsWith("1:64:")) 371 | { 372 | //check for radio channel 373 | if (e2servicereference.ToUpper().Contains("RADIO")) 374 | { 375 | channelInfo.ChannelType = ChannelType.Radio; 376 | } 377 | else 378 | { 379 | channelInfo.ChannelType = ChannelType.TV; 380 | } 381 | 382 | channelInfo.HasImage = true; 383 | channelInfo.Id = e2servicereference; 384 | 385 | // image name is name is e2servicereference with last char removed, then replace all : with _, then add .png 386 | var imageName = e2servicereference.Remove(e2servicereference.Length - 1); 387 | imageName = imageName.Replace(":", "_"); 388 | imageName = imageName + ".png"; 389 | //var imageUrl = string.Format("{0}/picon/{1}", baseUrl, imageName); 390 | var imageUrl = string.Format("{0}/picon/{1}", baseUrlPicon, imageName); 391 | 392 | if (Plugin.Instance.Configuration.FetchPiconsFromWebInterface) 393 | { 394 | channelInfo.ImagePath = null; 395 | //channelInfo.ImageUrl = WebUtility.UrlEncode(imageUrl); 396 | channelInfo.ImageUrl = imageUrl; 397 | } 398 | else 399 | { 400 | channelInfo.ImagePath = Plugin.Instance.Configuration.PiconsPath + imageName; 401 | channelInfo.ImageUrl = null; 402 | } 403 | 404 | channelInfo.Name = e2servicename; 405 | channelInfo.Number = count.ToString(); 406 | 407 | channelInfos.Add(channelInfo); 408 | count = count + 1; 409 | } 410 | else 411 | { 412 | _logger.LogInformation("[Enigma2] ignoring channel label " + e2servicereference); 413 | } 414 | } 415 | } 416 | tvChannelInfos = channelInfos; 417 | return channelInfos; 418 | } 419 | catch (Exception e) 420 | { 421 | _logger.LogError("[Enigma2] Failed to parse channel information."); 422 | _logger.LogError(string.Format("[Enigma2] GetChannelsAsync error: {0}", e.Message)); 423 | throw new ApplicationException("Failed to parse channel information."); 424 | } 425 | } 426 | } 427 | } 428 | 429 | 430 | /// 431 | /// Gets the channels async. 432 | /// 433 | /// The cancellation token. 434 | /// Service reference 435 | /// Task{List}. 436 | public async Task> GetChannelsForTVBouquetAsync(CancellationToken cancellationToken, string sRef) 437 | { 438 | _logger.LogInformation("[Enigma2] Start GetChannelsForTVBouquetAsync, retrieve all channels for TV Bouquet " + sRef); 439 | await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); 440 | 441 | var protocol = "http"; 442 | if (Plugin.Instance.Configuration.UseSecureHTTPS) 443 | { 444 | protocol = "https"; 445 | } 446 | 447 | var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; 448 | 449 | var baseUrlPicon = baseUrl; 450 | 451 | var url = string.Format("{0}/web/getservices?sRef={1}", baseUrl, sRef); 452 | 453 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] GetChannelsForTVBouquetAsync url: {0}", url)); 454 | 455 | if (!string.IsNullOrEmpty(Plugin.Instance.Configuration.WebInterfaceUsername)) 456 | { 457 | baseUrlPicon = protocol + "://" + Plugin.Instance.Configuration.WebInterfaceUsername + ":" + Plugin.Instance.Configuration.WebInterfacePassword + "@" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; 458 | } 459 | 460 | using (var stream = await GetHttpClient().GetStreamAsync(url, cancellationToken).ConfigureAwait(false)) 461 | { 462 | using (var reader = new StreamReader(stream)) 463 | { 464 | var xmlResponse = reader.ReadToEnd(); 465 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] GetChannelsForTVBouquetAsync response: {0}", xmlResponse)); 466 | 467 | try 468 | { 469 | var xml = new XmlDocument(); 470 | xml.LoadXml(xmlResponse); 471 | 472 | var channelInfos = new List(); 473 | 474 | // Load channels for specified TV Bouquet only 475 | 476 | var count = 1; 477 | 478 | var e2services = xml.GetElementsByTagName("e2service"); 479 | foreach (XmlNode xmlNode in e2services) 480 | { 481 | var channelInfo = new ChannelInfo(); 482 | 483 | var e2servicereference = "?"; 484 | var e2servicename = "?"; 485 | 486 | foreach (XmlNode node in xmlNode.ChildNodes) 487 | { 488 | if (node.Name == "e2servicereference") 489 | { 490 | e2servicereference = node.InnerText; 491 | } 492 | else if (node.Name == "e2servicename") 493 | { 494 | e2servicename = node.InnerText; 495 | } 496 | } 497 | 498 | // Check whether the current element is not just a label 499 | if (!e2servicereference.StartsWith("1:64:")) 500 | { 501 | //check for radio channel 502 | if (e2servicereference.Contains("radio")) 503 | { 504 | channelInfo.ChannelType = ChannelType.Radio; 505 | } 506 | else 507 | { 508 | channelInfo.ChannelType = ChannelType.TV; 509 | } 510 | 511 | channelInfo.HasImage = true; 512 | channelInfo.Id = e2servicereference; 513 | 514 | // image name is name is e2servicereference with last char removed, then replace all : with _, then add .png 515 | var imageName = e2servicereference.Remove(e2servicereference.Length - 1); 516 | imageName = imageName.Replace(":", "_"); 517 | imageName = imageName + ".png"; 518 | //var imageUrl = string.Format("{0}/picon/{1}", baseUrl, imageName); 519 | var imageUrl = string.Format("{0}/picon/{1}", baseUrlPicon, imageName); 520 | 521 | if (Plugin.Instance.Configuration.FetchPiconsFromWebInterface) 522 | { 523 | channelInfo.ImagePath = null; 524 | //channelInfo.ImageUrl = WebUtility.UrlEncode(imageUrl); 525 | channelInfo.ImageUrl = imageUrl; 526 | } 527 | else 528 | { 529 | channelInfo.ImagePath = Plugin.Instance.Configuration.PiconsPath + imageName; 530 | channelInfo.ImageUrl = null; 531 | } 532 | 533 | channelInfo.Name = e2servicename; 534 | channelInfo.Number = count.ToString(); 535 | 536 | channelInfos.Add(channelInfo); 537 | count++; 538 | } 539 | else 540 | { 541 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] ignoring channel {0}", e2servicereference)); 542 | } 543 | } 544 | return channelInfos; 545 | } 546 | catch (Exception e) 547 | { 548 | _logger.LogError("[Enigma2] Failed to parse channel information."); 549 | _logger.LogError(string.Format("[Enigma2] GetChannelsForTVBouquetAsync error: {0}", e.Message)); 550 | throw new ApplicationException("Failed to parse channel information."); 551 | } 552 | } 553 | } 554 | } 555 | 556 | 557 | /// 558 | /// Gets the Recordings async 559 | /// 560 | /// The cancellation token. 561 | /// Task{IEnumerable{MyRecordingInfo}} 562 | public async Task> GetRecordingsAsync(CancellationToken cancellationToken) 563 | { 564 | await Task.Delay(0); //to avoid await warnings 565 | return new List(); 566 | } 567 | 568 | public async Task> GetAllRecordingsAsync(CancellationToken cancellationToken) 569 | { 570 | _logger.LogInformation("[Enigma2] Start GetRecordingsAsync, retrieve all 'Inprogress' and 'Completed' recordings "); 571 | await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); 572 | 573 | var protocol = "http"; 574 | if (Plugin.Instance.Configuration.UseSecureHTTPS) 575 | { 576 | protocol = "https"; 577 | } 578 | 579 | var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; 580 | 581 | var url = string.Format("{0}/web/movielist", baseUrl); 582 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] GetRecordingsAsync url: {0}", url)); 583 | 584 | using (var stream = await GetHttpClient().GetStreamAsync(url, cancellationToken).ConfigureAwait(false)) 585 | { 586 | using (var reader = new StreamReader(stream)) 587 | { 588 | var xmlResponse = reader.ReadToEnd(); 589 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] GetRecordingsAsync response: {0}", xmlResponse)); 590 | 591 | try 592 | { 593 | var xml = new XmlDocument(); 594 | xml.LoadXml(xmlResponse); 595 | 596 | var recordingInfos = new List(); 597 | 598 | var count = 1; 599 | 600 | var e2movie = xml.GetElementsByTagName("e2movie"); 601 | 602 | foreach (XmlNode xmlNode in e2movie) 603 | { 604 | var recordingInfo = new MyRecordingInfo(); 605 | 606 | var e2servicereference = "?"; 607 | var e2title = "?"; 608 | var e2description = "?"; 609 | var e2servicename = "?"; 610 | var e2time = "?"; 611 | var e2length = "?"; 612 | var e2filename = "?"; 613 | 614 | foreach (XmlNode node in xmlNode.ChildNodes) 615 | { 616 | if (node.Name == "e2servicereference") 617 | { 618 | e2servicereference = node.InnerText; 619 | } 620 | else if (node.Name == "e2title") 621 | { 622 | e2title = node.InnerText; 623 | } 624 | else if (node.Name == "e2description") 625 | { 626 | e2description = node.InnerText; 627 | } 628 | else if (node.Name == "e2servicename") 629 | { 630 | e2servicename = node.InnerText; 631 | } 632 | else if (node.Name == "e2time") 633 | { 634 | e2time = node.InnerText; 635 | } 636 | else if (node.Name == "e2length") 637 | { 638 | e2length = node.InnerText; 639 | } 640 | else if (node.Name == "e2filename") 641 | { 642 | e2filename = node.InnerText; 643 | } 644 | } 645 | 646 | recordingInfo.Audio = null; 647 | 648 | recordingInfo.ChannelId = null; 649 | //check for radio channel 650 | if (e2servicereference.ToUpper().Contains("RADIO")) 651 | { 652 | recordingInfo.ChannelType = ChannelType.Radio; 653 | } 654 | else 655 | { 656 | recordingInfo.ChannelType = ChannelType.TV; 657 | } 658 | 659 | recordingInfo.HasImage = false; 660 | recordingInfo.ImagePath = null; 661 | recordingInfo.ImageUrl = null; 662 | 663 | foreach (var channelInfo in tvChannelInfos) 664 | { 665 | if (channelInfo.Name == e2servicename) 666 | { 667 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] GetRecordingsAsync match on channel name : {0} for recording {1}", e2servicename, e2title)); 668 | recordingInfo.ChannelId = channelInfo.Id; 669 | recordingInfo.ChannelType = channelInfo.ChannelType; 670 | recordingInfo.HasImage = true; 671 | recordingInfo.ImagePath = channelInfo.ImagePath; 672 | recordingInfo.ImageUrl = channelInfo.ImageUrl; 673 | break; 674 | } 675 | } 676 | 677 | if (recordingInfo.ChannelId == null) 678 | { 679 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] GetRecordingsAsync no match on channel name : {0} for recording {1}", e2servicename, e2title)); 680 | } 681 | 682 | recordingInfo.CommunityRating = 0; 683 | 684 | var sdated = long.Parse(e2time); 685 | var sdate = ApiHelper.DateTimeFromUnixTimestampSeconds(sdated); 686 | recordingInfo.StartDate = sdate.ToUniversalTime(); 687 | 688 | //length in format mm:ss 689 | var words = e2length.Split(':'); 690 | var mins = long.Parse(words[0]); 691 | var seconds = long.Parse(words[1]); 692 | var edated = long.Parse(e2time) + (mins * 60) + (seconds); 693 | var edate = ApiHelper.DateTimeFromUnixTimestampSeconds(edated); 694 | recordingInfo.EndDate = edate.ToUniversalTime(); 695 | 696 | //recordingInfo.EpisodeTitle = e2title; 697 | recordingInfo.EpisodeTitle = null; 698 | 699 | recordingInfo.Overview = e2description; 700 | 701 | var genre = new List 702 | { 703 | "Unknown" 704 | }; 705 | recordingInfo.Genres = genre; 706 | 707 | recordingInfo.Id = e2servicereference; 708 | recordingInfo.IsHD = false; 709 | recordingInfo.IsKids = false; 710 | recordingInfo.IsLive = false; 711 | recordingInfo.IsMovie = false; 712 | recordingInfo.IsNews = false; 713 | recordingInfo.IsPremiere = false; 714 | recordingInfo.IsRepeat = false; 715 | recordingInfo.IsSeries = false; 716 | recordingInfo.IsSports = false; 717 | recordingInfo.Name = e2title; 718 | recordingInfo.OfficialRating = null; 719 | recordingInfo.OriginalAirDate = null; 720 | recordingInfo.Overview = e2description; 721 | recordingInfo.Path = null; 722 | recordingInfo.ProgramId = null; 723 | recordingInfo.SeriesTimerId = null; 724 | recordingInfo.Url = baseUrl + "/file?file=" + WebUtility.UrlEncode(e2filename); 725 | 726 | recordingInfos.Add(recordingInfo); 727 | count = count + 1; 728 | } 729 | return recordingInfos; 730 | } 731 | catch (Exception e) 732 | { 733 | _logger.LogError("[Enigma2] Failed to parse timer information."); 734 | _logger.LogError(string.Format("[Enigma2] GetRecordingsAsync error: {0}", e.Message)); 735 | throw new ApplicationException("Failed to parse timer information."); 736 | } 737 | } 738 | } 739 | } 740 | 741 | /// 742 | /// Delete the Recording async from the disk 743 | /// 744 | /// The recordingId 745 | /// The cancellationToken 746 | /// 747 | public async Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken) 748 | { 749 | _logger.LogInformation(string.Format("[Enigma2] Start Delete Recording Async for recordingId: {0}", recordingId)); 750 | await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); 751 | 752 | var protocol = "http"; 753 | if (Plugin.Instance.Configuration.UseSecureHTTPS) 754 | { 755 | protocol = "https"; 756 | } 757 | 758 | var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; 759 | 760 | var url = string.Format("{0}/web/moviedelete?sRef={1}", baseUrl, recordingId); 761 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] DeleteRecordingAsync url: {0}", url)); 762 | 763 | using (var stream = await GetHttpClient().GetStreamAsync(url, cancellationToken).ConfigureAwait(false)) 764 | { 765 | using (var reader = new StreamReader(stream)) 766 | { 767 | var xmlResponse = reader.ReadToEnd(); 768 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] DeleteRecordingAsync response: {0}", xmlResponse)); 769 | 770 | try 771 | { 772 | var xml = new XmlDocument(); 773 | xml.LoadXml(xmlResponse); 774 | 775 | var e2simplexmlresult = xml.GetElementsByTagName("e2simplexmlresult"); 776 | foreach (XmlNode xmlNode in e2simplexmlresult) 777 | { 778 | var recordingInfo = new MyRecordingInfo(); 779 | 780 | var e2state = "?"; 781 | var e2statetext = "?"; 782 | 783 | foreach (XmlNode node in xmlNode.ChildNodes) 784 | { 785 | if (node.Name == "e2state") 786 | { 787 | e2state = node.InnerText; 788 | } 789 | else if (node.Name == "e2statetext") 790 | { 791 | e2statetext = node.InnerText; 792 | } 793 | } 794 | 795 | if (e2state != "True") 796 | { 797 | _logger.LogError("[Enigma2] Failed to delete recording information."); 798 | _logger.LogError(string.Format("[Enigma2] DeleteRecordingAsync e2statetext: {0}", e2statetext)); 799 | throw new ApplicationException("Failed to delete recording."); 800 | } 801 | } 802 | } 803 | catch (Exception e) 804 | { 805 | _logger.LogError("[Enigma2] Failed to parse delete recording information."); 806 | _logger.LogError(string.Format("[Enigma2] DeleteRecordingAsync error: {0}", e.Message)); 807 | throw new ApplicationException("Failed to parse delete recording information."); 808 | } 809 | } 810 | } 811 | } 812 | 813 | 814 | /// 815 | /// Cancel pending scheduled Recording 816 | /// 817 | /// The timerId 818 | /// The cancellationToken 819 | /// 820 | public async Task CancelTimerAsync(string timerId, CancellationToken cancellationToken) 821 | { 822 | _logger.LogInformation(string.Format("[Enigma2] Start CancelTimerAsync for recordingId: {0}", timerId)); 823 | await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); 824 | 825 | // extract sRef, id, begin and end from passed timerId 826 | var words = timerId.Split('~'); 827 | var sRef = words[0]; 828 | var id = words[1]; 829 | var begin = words[2]; 830 | var end = words[3]; 831 | 832 | var protocol = "http"; 833 | if (Plugin.Instance.Configuration.UseSecureHTTPS) 834 | { 835 | protocol = "https"; 836 | } 837 | 838 | var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; 839 | 840 | var url = string.Format("{0}/web/timerdelete?sRef={1}&begin={2}&end={3}", baseUrl, sRef, begin, end); 841 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] CancelTimerAsync url: {0}", url)); 842 | 843 | using (var stream = await GetHttpClient().GetStreamAsync(url, cancellationToken).ConfigureAwait(false)) 844 | { 845 | using (var reader = new StreamReader(stream)) 846 | { 847 | var xmlResponse = reader.ReadToEnd(); 848 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] CancelTimerAsync response: {0}", xmlResponse)); 849 | 850 | try 851 | { 852 | var xml = new XmlDocument(); 853 | xml.LoadXml(xmlResponse); 854 | 855 | var e2simplexmlresult = xml.GetElementsByTagName("e2simplexmlresult"); 856 | foreach (XmlNode xmlNode in e2simplexmlresult) 857 | { 858 | var recordingInfo = new MyRecordingInfo(); 859 | 860 | var e2state = "?"; 861 | var e2statetext = "?"; 862 | 863 | foreach (XmlNode node in xmlNode.ChildNodes) 864 | { 865 | if (node.Name == "e2state") 866 | { 867 | e2state = node.InnerText; 868 | } 869 | else if (node.Name == "e2statetext") 870 | { 871 | e2statetext = node.InnerText; 872 | } 873 | } 874 | 875 | if (e2state != "True") 876 | { 877 | _logger.LogError("[Enigma2] Failed to cancel timer."); 878 | _logger.LogError(string.Format("[Enigma2] CancelTimerAsync e2statetext: {0}", e2statetext)); 879 | throw new ApplicationException("Failed to cancel timer."); 880 | } 881 | } 882 | } 883 | catch (Exception e) 884 | { 885 | _logger.LogError("[Enigma2] Failed to parse cancel timer information."); 886 | _logger.LogError(string.Format("[Enigma2] CancelTimerAsync error: {0}", e.Message)); 887 | throw new ApplicationException("Failed to parse cancel timer information."); 888 | } 889 | } 890 | } 891 | } 892 | 893 | 894 | /// 895 | /// Create a new recording 896 | /// 897 | /// The TimerInfo 898 | /// The cancellationToken 899 | /// 900 | public async Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken) 901 | { 902 | _logger.LogInformation(string.Format("[Enigma2] Start CreateTimerAsync for ChannelId: {0} & Name: {1}", info.ChannelId, info.Name)); 903 | await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); 904 | 905 | // extract eventid from info.ProgramId 906 | var words = info.ProgramId.Split('~'); 907 | var eventid = words[1]; 908 | 909 | var protocol = "http"; 910 | if (Plugin.Instance.Configuration.UseSecureHTTPS) 911 | { 912 | protocol = "https"; 913 | } 914 | 915 | var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; 916 | 917 | var url = string.Format("{0}/web/timeraddbyeventid?sRef={1}&eventid={2}", baseUrl, info.ChannelId, eventid); 918 | 919 | if (!string.IsNullOrEmpty(Plugin.Instance.Configuration.RecordingPath)) 920 | { 921 | url = url + string.Format("&dirname={0}", WebUtility.UrlEncode(Plugin.Instance.Configuration.RecordingPath)); 922 | } 923 | 924 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] CreateTimerAsync url: {0}", url)); 925 | 926 | using (var stream = await GetHttpClient().GetStreamAsync(url, cancellationToken).ConfigureAwait(false)) 927 | { 928 | using (var reader = new StreamReader(stream)) 929 | { 930 | var xmlResponse = reader.ReadToEnd(); 931 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] CancelTimerAsync response: {0}", xmlResponse)); 932 | 933 | try 934 | { 935 | var xml = new XmlDocument(); 936 | xml.LoadXml(xmlResponse); 937 | 938 | var e2simplexmlresult = xml.GetElementsByTagName("e2simplexmlresult"); 939 | foreach (XmlNode xmlNode in e2simplexmlresult) 940 | { 941 | var recordingInfo = new MyRecordingInfo(); 942 | 943 | var e2state = "?"; 944 | var e2statetext = "?"; 945 | 946 | foreach (XmlNode node in xmlNode.ChildNodes) 947 | { 948 | if (node.Name == "e2state") 949 | { 950 | e2state = node.InnerText; 951 | } 952 | else if (node.Name == "e2statetext") 953 | { 954 | e2statetext = node.InnerText; 955 | } 956 | } 957 | 958 | if (e2state != "True") 959 | { 960 | _logger.LogError("[Enigma2] Failed to create timer."); 961 | _logger.LogError(string.Format("[Enigma2] CreateTimerAsync e2statetext: {0}", e2statetext)); 962 | throw new ApplicationException("Failed to create timer."); 963 | } 964 | } 965 | } 966 | catch (Exception e) 967 | { 968 | _logger.LogError("[Enigma2] Failed to parse create timer information."); 969 | _logger.LogError(string.Format("[Enigma2] CreateTimerAsync error: {0}", e.Message)); 970 | throw new ApplicationException("Failed to parse create timer information."); 971 | } 972 | } 973 | } 974 | } 975 | 976 | 977 | /// 978 | /// Get the pending Recordings. 979 | /// 980 | /// The CancellationToken 981 | /// IEnumerable 982 | public async Task> GetTimersAsync(CancellationToken cancellationToken) 983 | { 984 | _logger.LogInformation("[Enigma2] Start GetTimerAsync, retrieve the 'Pending' recordings"); 985 | await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); 986 | 987 | var protocol = "http"; 988 | if (Plugin.Instance.Configuration.UseSecureHTTPS) 989 | { 990 | protocol = "https"; 991 | } 992 | 993 | var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; 994 | 995 | var url = string.Format("{0}/web/timerlist", baseUrl); 996 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] GetTimersAsync url: {0}", url)); 997 | 998 | using (var stream = await GetHttpClient().GetStreamAsync(url, cancellationToken).ConfigureAwait(false)) 999 | { 1000 | using (var reader = new StreamReader(stream)) 1001 | { 1002 | var xmlResponse = reader.ReadToEnd(); 1003 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] GetTimersAsync response: {0}", xmlResponse)); 1004 | 1005 | try 1006 | { 1007 | var xml = new XmlDocument(); 1008 | xml.LoadXml(xmlResponse); 1009 | 1010 | var timerInfos = new List(); 1011 | 1012 | var count = 1; 1013 | 1014 | var e2timer = xml.GetElementsByTagName("e2timer"); 1015 | foreach (XmlNode xmlNode in e2timer) 1016 | { 1017 | var timerInfo = new TimerInfo(); 1018 | 1019 | var e2servicereference = "?"; 1020 | var e2name = "?"; 1021 | var e2description = "?"; 1022 | var e2eit = "?"; 1023 | var e2timebegin = "?"; 1024 | var e2timeend = "?"; 1025 | var e2state = "?"; 1026 | 1027 | foreach (XmlNode node in xmlNode.ChildNodes) 1028 | { 1029 | if (node.Name == "e2servicereference") 1030 | { 1031 | e2servicereference = node.InnerText; 1032 | } 1033 | else if (node.Name == "e2name") 1034 | { 1035 | e2name = node.InnerText; 1036 | } 1037 | else if (node.Name == "e2description") 1038 | { 1039 | e2description = node.InnerText; 1040 | } 1041 | else if (node.Name == "e2eit") 1042 | { 1043 | e2eit = node.InnerText; 1044 | } 1045 | else if (node.Name == "e2timebegin") 1046 | { 1047 | e2timebegin = node.InnerText; 1048 | } 1049 | else if (node.Name == "e2timeend") 1050 | { 1051 | e2timeend = node.InnerText; 1052 | } 1053 | else if (node.Name == "e2state") 1054 | { 1055 | e2state = node.InnerText; 1056 | } 1057 | } 1058 | 1059 | // only interested in pending timers and ones recording now 1060 | if (e2state == "0" || e2state == "2") 1061 | { 1062 | 1063 | timerInfo.ChannelId = e2servicereference; 1064 | 1065 | var edated = long.Parse(e2timeend); 1066 | var edate = ApiHelper.DateTimeFromUnixTimestampSeconds(edated); 1067 | timerInfo.EndDate = edate.ToUniversalTime(); 1068 | 1069 | timerInfo.Id = e2servicereference + "~" + e2eit + "~" + e2timebegin + "~" + e2timeend + "~" + count; 1070 | 1071 | timerInfo.IsPostPaddingRequired = false; 1072 | timerInfo.IsPrePaddingRequired = false; 1073 | timerInfo.Name = e2name; 1074 | timerInfo.Overview = e2description; 1075 | timerInfo.PostPaddingSeconds = 0; 1076 | timerInfo.PrePaddingSeconds = 0; 1077 | timerInfo.Priority = 0; 1078 | timerInfo.ProgramId = null; 1079 | timerInfo.SeriesTimerId = null; 1080 | 1081 | var sdated = long.Parse(e2timebegin); 1082 | var sdate = ApiHelper.DateTimeFromUnixTimestampSeconds(sdated); 1083 | timerInfo.StartDate = sdate.ToUniversalTime(); 1084 | 1085 | if (e2state == "0") 1086 | { 1087 | timerInfo.Status = RecordingStatus.New; 1088 | } 1089 | 1090 | if (e2state == "2") 1091 | { 1092 | timerInfo.Status = RecordingStatus.InProgress; 1093 | } 1094 | 1095 | timerInfos.Add(timerInfo); 1096 | count = count + 1; 1097 | } 1098 | else 1099 | { 1100 | _logger.LogInformation("[Enigma2] ignoring timer " + e2name); 1101 | } 1102 | } 1103 | return timerInfos; 1104 | } 1105 | catch (Exception e) 1106 | { 1107 | _logger.LogError("[Enigma2] Failed to parse timer information."); 1108 | _logger.LogError(string.Format("[Enigma2] GetTimersAsync error: {0}", e.Message)); 1109 | throw new ApplicationException("Failed to parse timer information."); 1110 | } 1111 | } 1112 | } 1113 | } 1114 | 1115 | public Task> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken) 1116 | { 1117 | throw new NotImplementedException(); 1118 | } 1119 | 1120 | public Task> GetRecordingStreamMediaSources(string recordingId, CancellationToken cancellationToken) 1121 | { 1122 | throw new NotImplementedException(); 1123 | } 1124 | 1125 | /// 1126 | /// Get the live channel stream, zap to channel if required. 1127 | /// 1128 | /// The CancellationToken 1129 | /// MediaSourceInfo 1130 | public async Task GetChannelStream(string channelOid, string mediaSourceId, CancellationToken cancellationToken) 1131 | { 1132 | _logger.LogInformation("[Enigma2] Start GetChannelStream"); 1133 | 1134 | var protocol = "http"; 1135 | if (Plugin.Instance.Configuration.UseSecureHTTPSForStreams) 1136 | { 1137 | protocol = "https"; 1138 | } 1139 | 1140 | var streamingPort = Plugin.Instance.Configuration.StreamingPort; 1141 | 1142 | if (Plugin.Instance.Configuration.TranscodedStream) 1143 | { 1144 | streamingPort = Plugin.Instance.Configuration.TranscodingPort; 1145 | } 1146 | 1147 | var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + streamingPort; 1148 | 1149 | //check if we need to zap to channel - single tuner 1150 | if (Plugin.Instance.Configuration.ZapToChannel) 1151 | { 1152 | await ZapToChannel(cancellationToken, channelOid).ConfigureAwait(false); 1153 | } 1154 | 1155 | if (Plugin.Instance.Configuration.UseLoginForStreams && !string.IsNullOrEmpty(Plugin.Instance.Configuration.WebInterfaceUsername)) 1156 | { 1157 | baseUrl = protocol + "://" + Plugin.Instance.Configuration.WebInterfaceUsername + ":" + Plugin.Instance.Configuration.WebInterfacePassword + "@" + Plugin.Instance.Configuration.HostName + ":" + streamingPort; 1158 | } 1159 | 1160 | var trancodingUrl = ""; 1161 | if (Plugin.Instance.Configuration.TranscodedStream) 1162 | { 1163 | trancodingUrl = "?bitrate=1000000?width=1280?height=720?vcodec=h264?aspectratio=2?interlaced=0.mp4"; 1164 | } 1165 | 1166 | _liveStreams++; 1167 | var streamUrl = string.Format("{0}/{1}{2}", baseUrl, channelOid, trancodingUrl); 1168 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] GetChannelStream url: {0}", streamUrl)); 1169 | 1170 | return new MediaSourceInfo 1171 | { 1172 | Id = _liveStreams.ToString(CultureInfo.InvariantCulture), 1173 | Path = streamUrl, 1174 | Protocol = MediaProtocol.Http, 1175 | MediaStreams = new List 1176 | { 1177 | new MediaStream 1178 | { 1179 | Type = MediaStreamType.Video, 1180 | // Set the index to -1 because we don't know the exact index of the video stream within the container 1181 | Index = -1, 1182 | 1183 | // Set to true if unknown to enable deinterlacing 1184 | IsInterlaced = true 1185 | 1186 | }, 1187 | new MediaStream 1188 | { 1189 | Type = MediaStreamType.Audio, 1190 | // Set the index to -1 because we don't know the exact index of the audio stream within the container 1191 | Index = -1 1192 | } 1193 | } 1194 | }; 1195 | throw new ResourceNotFoundException(string.Format("Could not stream channel {0}", channelOid)); 1196 | } 1197 | 1198 | 1199 | /// 1200 | /// zap to channel. 1201 | /// 1202 | /// The CancellationToken 1203 | /// The channel id 1204 | /// 1205 | public async Task ZapToChannel(CancellationToken cancellationToken, string channelOid) 1206 | { 1207 | _logger.LogInformation("[Enigma2] Start ZapToChannel"); 1208 | await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); 1209 | 1210 | var protocol = "http"; 1211 | if (Plugin.Instance.Configuration.UseSecureHTTPS) 1212 | { 1213 | protocol = "https"; 1214 | } 1215 | 1216 | var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; 1217 | 1218 | var url = string.Format("{0}/web/zap?sRef={1}", baseUrl, channelOid); 1219 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] ZapToChannel url: {0}", url)); 1220 | 1221 | using (var stream = await GetHttpClient().GetStreamAsync(url, cancellationToken).ConfigureAwait(false)) 1222 | { 1223 | using (var reader = new StreamReader(stream)) 1224 | { 1225 | var xmlResponse = reader.ReadToEnd(); 1226 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] ZapToChannel response: {0}", xmlResponse)); 1227 | 1228 | try 1229 | { 1230 | var xml = new XmlDocument(); 1231 | xml.LoadXml(xmlResponse); 1232 | 1233 | var e2simplexmlresult = xml.GetElementsByTagName("e2simplexmlresult"); 1234 | foreach (XmlNode xmlNode in e2simplexmlresult) 1235 | { 1236 | var recordingInfo = new MyRecordingInfo(); 1237 | 1238 | var e2state = "?"; 1239 | var e2statetext = "?"; 1240 | 1241 | foreach (XmlNode node in xmlNode.ChildNodes) 1242 | { 1243 | if (node.Name == "e2state") 1244 | { 1245 | e2state = node.InnerText; 1246 | } 1247 | else if (node.Name == "e2statetext") 1248 | { 1249 | e2statetext = node.InnerText; 1250 | } 1251 | } 1252 | 1253 | if (e2state != "True") 1254 | { 1255 | _logger.LogError("[Enigma2] Failed to zap to channel."); 1256 | _logger.LogError(string.Format("[Enigma2] ZapToChannel e2statetext: {0}", e2statetext)); 1257 | throw new ApplicationException("Failed to zap to channel."); 1258 | } 1259 | } 1260 | } 1261 | catch (Exception e) 1262 | { 1263 | _logger.LogError("[Enigma2] Failed to parse create timer information."); 1264 | _logger.LogError(string.Format("[Enigma2] ZapToChannel error: {0}", e.Message)); 1265 | throw new ApplicationException("Failed to parse zap to channel information."); 1266 | } 1267 | } 1268 | } 1269 | 1270 | 1271 | } 1272 | 1273 | 1274 | /// 1275 | /// Get new timer defaults. 1276 | /// 1277 | /// The CancellationToken 1278 | /// null 1279 | /// SeriesTimerInfo 1280 | public async Task GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo program = null) 1281 | { 1282 | _logger.LogInformation("[Enigma2] Start GetNewTimerDefaultsAsync"); 1283 | 1284 | await Task.Delay(0); //to avoid await warnings 1285 | var seriesTimerInfo = new SeriesTimerInfo(); 1286 | 1287 | return seriesTimerInfo; 1288 | } 1289 | 1290 | 1291 | /// 1292 | /// Get programs for specified channel within start and end date. 1293 | /// 1294 | /// channel id 1295 | /// start date/time 1296 | /// end date/time 1297 | /// The CancellationToken 1298 | /// IEnumerable 1299 | public async Task> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) 1300 | { 1301 | _logger.LogInformation("[Enigma2] Start GetProgramsAsync"); 1302 | await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); 1303 | 1304 | var rnd = new Random(); 1305 | 1306 | var imagePath = ""; 1307 | var imageUrl = ""; 1308 | 1309 | var protocol = "http"; 1310 | if (Plugin.Instance.Configuration.UseSecureHTTPS) 1311 | { 1312 | protocol = "https"; 1313 | } 1314 | 1315 | var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; 1316 | 1317 | var url = string.Format("{0}/web/epgservice?sRef={1}", baseUrl, channelId); 1318 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] GetProgramsAsync url: {0}", url)); 1319 | 1320 | using (var stream = await GetHttpClient().GetStreamAsync(url, cancellationToken).ConfigureAwait(false)) 1321 | { 1322 | using (var reader = new StreamReader(stream)) 1323 | { 1324 | var xmlResponse = reader.ReadToEnd(); 1325 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] GetProgramsAsync response: {0}", xmlResponse)); 1326 | 1327 | try 1328 | { 1329 | var xml = new XmlDocument(); 1330 | xml.LoadXml(xmlResponse); 1331 | 1332 | var programInfos = new List(); 1333 | 1334 | var count = 1; 1335 | 1336 | var e2event = xml.GetElementsByTagName("e2event"); 1337 | foreach (XmlNode xmlNode in e2event) 1338 | { 1339 | var programInfo = new ProgramInfo(); 1340 | 1341 | var e2eventid = "?"; 1342 | var e2eventstart = "?"; 1343 | var e2eventduration = "?"; 1344 | var e2eventcurrenttime = "?"; 1345 | var e2eventtitle = "?"; 1346 | var e2eventdescription = "?"; 1347 | var e2eventdescriptionextended = "?"; 1348 | var e2eventservicereference = "?"; 1349 | var e2eventservicename = "?"; 1350 | 1351 | foreach (XmlNode node in xmlNode.ChildNodes) 1352 | { 1353 | if (node.Name == "e2eventid") 1354 | { 1355 | e2eventid = node.InnerText; 1356 | } 1357 | else if (node.Name == "e2eventstart") 1358 | { 1359 | e2eventstart = node.InnerText; 1360 | } 1361 | else if (node.Name == "e2eventduration") 1362 | { 1363 | e2eventduration = node.InnerText; 1364 | } 1365 | else if (node.Name == "e2eventcurrenttime") 1366 | { 1367 | e2eventcurrenttime = node.InnerText; 1368 | } 1369 | else if (node.Name == "e2eventtitle") 1370 | { 1371 | e2eventtitle = node.InnerText; 1372 | } 1373 | else if (node.Name == "e2eventdescription") 1374 | { 1375 | e2eventdescription = node.InnerText; 1376 | } 1377 | else if (node.Name == "e2eventdescriptionextended") 1378 | { 1379 | e2eventdescriptionextended = node.InnerText; 1380 | } 1381 | else if (node.Name == "e2eventservicereference") 1382 | { 1383 | e2eventservicereference = node.InnerText; 1384 | } 1385 | else if (node.Name == "e2eventservicename") 1386 | { 1387 | e2eventservicename = node.InnerText; 1388 | } 1389 | } 1390 | 1391 | var sdated = long.Parse(e2eventstart); 1392 | var sdate = ApiHelper.DateTimeFromUnixTimestampSeconds(sdated); 1393 | 1394 | // Check whether the current element is within the time range passed 1395 | if (sdate > endDateUtc) 1396 | { 1397 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] GetProgramsAsync epc full ending without adding channel name : {0} program : {1}", e2eventservicename, e2eventtitle)); 1398 | return programInfos; 1399 | } 1400 | else 1401 | { 1402 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] GetProgramsAsync adding program for channel name : {0} program : {1}", e2eventservicename, e2eventtitle)); 1403 | //programInfo.HasImage = false; 1404 | //programInfo.ImagePath = null; 1405 | //programInfo.ImageUrl = null; 1406 | if (count == 1) 1407 | { 1408 | foreach (var channelInfo in tvChannelInfos) 1409 | { 1410 | if (channelInfo.Name == e2eventservicename) 1411 | { 1412 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] GetProgramsAsync match on channel name : {0}", e2eventservicename)); 1413 | //programInfo.HasImage = true; 1414 | //programInfo.ImagePath = channelInfo.ImagePath; 1415 | //programInfo.ImageUrl = channelInfo.ImageUrl; 1416 | imagePath = channelInfo.ImagePath; 1417 | imageUrl = channelInfo.ImageUrl; 1418 | break; 1419 | } 1420 | } 1421 | } 1422 | 1423 | programInfo.HasImage = true; 1424 | programInfo.ImagePath = imagePath; 1425 | programInfo.ImageUrl = imageUrl; 1426 | 1427 | programInfo.ChannelId = e2eventservicereference; 1428 | 1429 | // for some reason the Id appears to have to be unique so will make it so 1430 | programInfo.Id = e2eventservicereference + "~" + e2eventid + "~" + count + "~" + rnd.Next(); 1431 | 1432 | programInfo.Overview = e2eventdescriptionextended; 1433 | 1434 | var edated = long.Parse(e2eventstart) + long.Parse(e2eventduration); 1435 | var edate = ApiHelper.DateTimeFromUnixTimestampSeconds(edated); 1436 | 1437 | programInfo.StartDate = sdate.ToUniversalTime(); 1438 | programInfo.EndDate = edate.ToUniversalTime(); 1439 | 1440 | var genre = new List 1441 | { 1442 | "Unknown" 1443 | }; 1444 | programInfo.Genres = genre; 1445 | 1446 | //programInfo.OriginalAirDate = null; 1447 | programInfo.Name = e2eventtitle; 1448 | //programInfo.OfficialRating = null; 1449 | //programInfo.CommunityRating = null; 1450 | //programInfo.EpisodeTitle = null; 1451 | //programInfo.Audio = null; 1452 | //programInfo.IsHD = false; 1453 | //programInfo.IsRepeat = false; 1454 | //programInfo.IsSeries = false; 1455 | //programInfo.IsNews = false; 1456 | //programInfo.IsMovie = false; 1457 | //programInfo.IsKids = false; 1458 | //programInfo.IsSports = false; 1459 | 1460 | programInfos.Add(programInfo); 1461 | count = count + 1; 1462 | } 1463 | } 1464 | return programInfos; 1465 | } 1466 | catch (Exception e) 1467 | { 1468 | _logger.LogError("[Enigma2] Failed to parse program information."); 1469 | _logger.LogError(string.Format("[Enigma2] GetProgramsAsync error: {0}", e.Message)); 1470 | throw new ApplicationException("Failed to parse channel information."); 1471 | } 1472 | } 1473 | } 1474 | } 1475 | 1476 | 1477 | /// 1478 | /// Get server status info. 1479 | /// 1480 | /// The CancellationToken 1481 | /// LiveTvServiceStatusInfo 1482 | /*public async Task GetStatusInfoAsync(CancellationToken cancellationToken) 1483 | { 1484 | _logger.LogInformation("[Enigma2] Start GetStatusInfoAsync Async, retrieve status details"); 1485 | await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false); 1486 | 1487 | //TODO: Version check 1488 | 1489 | var upgradeAvailable = false; 1490 | var serverVersion = "Unknown"; 1491 | 1492 | var protocol = "http"; 1493 | if (Plugin.Instance.Configuration.UseSecureHTTPS) 1494 | { 1495 | protocol = "https"; 1496 | } 1497 | 1498 | var baseUrl = protocol + "://" + Plugin.Instance.Configuration.HostName + ":" + Plugin.Instance.Configuration.WebInterfacePort; 1499 | 1500 | var url = string.Format("{0}/web/deviceinfo", baseUrl); 1501 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] GetStatusInfoAsync url: {0}", url)); 1502 | 1503 | var liveTvTunerInfos = new List(); 1504 | 1505 | using (var stream = await _httpClient.GetStreamAsync(url, cancellationToken).ConfigureAwait(false)) 1506 | { 1507 | using (var reader = new StreamReader(stream)) 1508 | { 1509 | var xmlResponse = reader.ReadToEnd(); 1510 | UtilsHelper.DebugInformation(_logger, string.Format("[Enigma2] GetStatusInfoAsync response: {0}", xmlResponse)); 1511 | 1512 | try 1513 | { 1514 | var xml = new XmlDocument(); 1515 | xml.LoadXml(xmlResponse); 1516 | 1517 | var e2frontend = xml.GetElementsByTagName("e2frontend"); 1518 | foreach (XmlNode xmlNode in e2frontend) 1519 | { 1520 | var liveTvTunerInfo = new LiveTvTunerInfo(); 1521 | 1522 | var e2name = "?"; 1523 | var e2model = "?"; 1524 | 1525 | foreach (XmlNode node in xmlNode.ChildNodes) 1526 | { 1527 | if (node.Name == "e2name") 1528 | { 1529 | e2name = node.InnerText; 1530 | } 1531 | else if (node.Name == "e2model") 1532 | { 1533 | e2model = node.InnerText; 1534 | } 1535 | } 1536 | 1537 | liveTvTunerInfo.Id = e2model; 1538 | liveTvTunerInfo.Name = e2name; 1539 | liveTvTunerInfo.SourceType = ""; 1540 | 1541 | liveTvTunerInfos.Add(liveTvTunerInfo); 1542 | } 1543 | 1544 | return new LiveTvServiceStatusInfo 1545 | { 1546 | HasUpdateAvailable = upgradeAvailable, 1547 | Version = serverVersion, 1548 | Tuners = liveTvTunerInfos 1549 | }; 1550 | 1551 | } 1552 | catch (Exception e) 1553 | { 1554 | _logger.LogError("[Enigma2] Failed to parse tuner information."); 1555 | _logger.LogError(string.Format("[Enigma2] GetStatusInfoAsync error: {0}", e.Message)); 1556 | throw new ApplicationException("Failed to parse tuner information."); 1557 | } 1558 | 1559 | } 1560 | } 1561 | }*/ 1562 | 1563 | 1564 | /// 1565 | /// Gets the homepage url. 1566 | /// 1567 | /// The homepage url. 1568 | public string HomePageUrl => "https://github.com/oe-alliance"; 1569 | 1570 | 1571 | /// 1572 | /// Gets the name. 1573 | /// 1574 | /// The name. 1575 | public string Name => "Enigma2"; 1576 | 1577 | 1578 | public async Task GetRecordingStream(string recordingId, string mediaSourceId, CancellationToken cancellationToken) 1579 | { 1580 | await Task.Delay(0); //to avoid await warnings 1581 | throw new NotImplementedException(); 1582 | } 1583 | 1584 | 1585 | public async Task CloseLiveStream(string id, CancellationToken cancellationToken) 1586 | { 1587 | await Task.Factory.StartNew(() => 1588 | { 1589 | _logger.LogDebug("[Enigma2] LiveTvService.CloseLiveStream: closed stream for subscriptionId: {id}", id); 1590 | return id; 1591 | }); 1592 | 1593 | } 1594 | 1595 | 1596 | public async Task CopyFilesAsync(StreamReader source, StreamWriter destination) 1597 | { 1598 | _logger.LogInformation("[Enigma2] Start CopyFiles Async"); 1599 | var buffer = new char[0x1000]; 1600 | int numRead; 1601 | while ((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) != 0) 1602 | { 1603 | await destination.WriteAsync(buffer, 0, numRead); 1604 | } 1605 | } 1606 | 1607 | 1608 | public Task RecordLiveStream(string id, CancellationToken cancellationToken) 1609 | { 1610 | throw new NotImplementedException(); 1611 | } 1612 | 1613 | 1614 | /// 1615 | /// Get the recurrent recordings 1616 | /// 1617 | /// The CancellationToken 1618 | /// 1619 | public async Task> GetSeriesTimersAsync(CancellationToken cancellationToken) 1620 | { 1621 | _logger.LogInformation("[Enigma2] Start GetSeriesTimersAsync"); 1622 | await Task.Delay(0); //to avoid await warnings 1623 | var seriesTimerInfo = new List(); 1624 | return seriesTimerInfo; 1625 | } 1626 | 1627 | 1628 | /// 1629 | /// Create a recurrent recording 1630 | /// 1631 | /// The recurrend program info 1632 | /// The CancelationToken 1633 | /// 1634 | public async Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) 1635 | { 1636 | await Task.Delay(0); //to avoid await warnings 1637 | throw new NotImplementedException(); 1638 | } 1639 | 1640 | 1641 | /// 1642 | /// Update the series Timer 1643 | /// 1644 | /// The series program info 1645 | /// The CancellationToken 1646 | /// 1647 | public async Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) 1648 | { 1649 | await Task.Delay(0); //to avoid await warnings 1650 | throw new NotImplementedException(); 1651 | } 1652 | 1653 | 1654 | /// 1655 | /// Update a single Timer 1656 | /// 1657 | /// The program info 1658 | /// The CancellationToken 1659 | /// 1660 | public async Task UpdateTimerAsync(TimerInfo info, CancellationToken cancellationToken) 1661 | { 1662 | await Task.Delay(0); //to avoid await warnings 1663 | throw new NotImplementedException(); 1664 | } 1665 | 1666 | 1667 | /// 1668 | /// Cancel the Series Timer 1669 | /// 1670 | /// The Timer Id 1671 | /// The CancellationToken 1672 | /// 1673 | public async Task CancelSeriesTimerAsync(string timerId, CancellationToken cancellationToken) 1674 | { 1675 | await Task.Delay(0); //to avoid await warnings 1676 | throw new NotImplementedException(); 1677 | } 1678 | 1679 | 1680 | public Task ResetTuner(string id, CancellationToken cancellationToken) 1681 | { 1682 | throw new NotImplementedException(); 1683 | } 1684 | 1685 | 1686 | public Task GetChannelImageAsync(string channelId, CancellationToken cancellationToken) 1687 | { 1688 | // Leave as is. This is handled by supplying image url to ChannelInfo 1689 | throw new NotImplementedException(); 1690 | } 1691 | 1692 | 1693 | public Task GetProgramImageAsync(string programId, string channelId, CancellationToken cancellationToken) 1694 | { 1695 | // Leave as is. This is handled by supplying image url to ProgramInfo 1696 | throw new NotImplementedException(); 1697 | } 1698 | 1699 | 1700 | public Task GetRecordingImageAsync(string recordingId, CancellationToken cancellationToken) 1701 | { 1702 | // Leave as is. This is handled by supplying image url to RecordingInfo 1703 | throw new NotImplementedException(); 1704 | } 1705 | 1706 | } 1707 | 1708 | } 1709 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Enigma2/Plugin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using MediaBrowser.Common.Configuration; 4 | using MediaBrowser.Common.Plugins; 5 | using MediaBrowser.Model.Plugins; 6 | using MediaBrowser.Model.Serialization; 7 | using Jellyfin.Plugin.Enigma2.Configuration; 8 | 9 | namespace Jellyfin.Plugin.Enigma2 10 | { 11 | /// 12 | /// Class Plugin 13 | /// 14 | public class Plugin : BasePlugin, IHasWebPages 15 | { 16 | public Plugin( 17 | IApplicationPaths applicationPaths, 18 | IXmlSerializer xmlSerializer 19 | ) : base(applicationPaths, xmlSerializer) 20 | { 21 | Instance = this; 22 | } 23 | 24 | /// 25 | /// Gets the name of the plugin 26 | /// 27 | /// The name. 28 | public override string Name => "Enigma2"; 29 | 30 | /// 31 | /// Gets the description. 32 | /// 33 | /// The description. 34 | public override string Description => "Provides live TV using Enigma2 PVR as a back-end."; 35 | 36 | private Guid _id = new Guid("193f29f9-ea6c-4595-a6f6-55e79d7c590a"); 37 | public override Guid Id => _id; 38 | 39 | /// 40 | /// Gets the instance. 41 | /// 42 | /// The instance. 43 | public static Plugin Instance { get; private set; } 44 | 45 | public IEnumerable GetPages() 46 | { 47 | return new[] 48 | { 49 | new PluginPageInfo 50 | { 51 | Name = "enigma2", 52 | EmbeddedResourcePath = "Jellyfin.Plugin.Enigma2.Configuration.configPage.html" 53 | } 54 | }; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Enigma2/RecordingsChannel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using MediaBrowser.Common.Extensions; 8 | using MediaBrowser.Controller.Channels; 9 | using MediaBrowser.Controller.Entities; 10 | using MediaBrowser.Controller.Providers; 11 | using MediaBrowser.Model.Channels; 12 | using MediaBrowser.Model.Dto; 13 | using MediaBrowser.Model.Entities; 14 | using MediaBrowser.Model.LiveTv; 15 | using MediaBrowser.Model.MediaInfo; 16 | using Microsoft.Extensions.Logging; 17 | 18 | namespace Jellyfin.Plugin.Enigma2 19 | { 20 | public class RecordingsChannel : IChannel, IHasCacheKey, ISupportsDelete, ISupportsLatestMedia, ISupportsMediaProbe, IHasFolderAttributes 21 | { 22 | private readonly ILogger _logger; 23 | 24 | public RecordingsChannel(ILoggerFactory loggerFactory) 25 | { 26 | _logger = loggerFactory.CreateLogger(); 27 | _logger.LogDebug("[Enigma2] RecordingsChannel()"); 28 | } 29 | 30 | public string Name => "Enigma2 Recordings"; 31 | 32 | public string[] Attributes => new[] { "Recordings" }; 33 | 34 | public string Description => "Enigma2 Recordings"; 35 | 36 | public string DataVersion => "1"; 37 | 38 | public string HomePageUrl => null; 39 | 40 | public ChannelParentalRating ParentalRating => ChannelParentalRating.GeneralAudience; 41 | 42 | public string GetCacheKey(string userId) 43 | { 44 | var now = DateTime.UtcNow; 45 | 46 | var values = new List 47 | { 48 | now.DayOfYear.ToString(CultureInfo.InvariantCulture), 49 | now.Hour.ToString(CultureInfo.InvariantCulture) 50 | }; 51 | 52 | double minute = now.Minute; 53 | minute /= 5; 54 | 55 | values.Add(Math.Floor(minute).ToString(CultureInfo.InvariantCulture)); 56 | 57 | values.Add(GetService().LastRecordingChange.Ticks.ToString(CultureInfo.InvariantCulture)); 58 | 59 | return string.Join("-", values.ToArray()); 60 | } 61 | 62 | public InternalChannelFeatures GetChannelFeatures() 63 | { 64 | return new InternalChannelFeatures 65 | { 66 | ContentTypes = new List 67 | { 68 | ChannelMediaContentType.Movie, 69 | ChannelMediaContentType.Episode, 70 | ChannelMediaContentType.Clip 71 | }, 72 | MediaTypes = new List 73 | { 74 | ChannelMediaType.Audio, 75 | ChannelMediaType.Video 76 | }, 77 | SupportsContentDownloading = true 78 | }; 79 | } 80 | 81 | public Task GetChannelImage(ImageType type, CancellationToken cancellationToken) 82 | { 83 | if (type == ImageType.Primary) 84 | { 85 | return Task.FromResult(new DynamicImageResponse 86 | { 87 | Path = "https://github.com/CompoUK/MediaBrowser.Plugins.VuPlus/blob/master/src/Images/Vu+.png?raw=true", 88 | Protocol = MediaProtocol.Http, 89 | HasImage = true 90 | }); 91 | } 92 | 93 | return Task.FromResult(new DynamicImageResponse 94 | { 95 | HasImage = false 96 | }); 97 | } 98 | 99 | public IEnumerable GetSupportedChannelImages() 100 | { 101 | return new List 102 | { 103 | ImageType.Primary 104 | }; 105 | } 106 | 107 | public bool IsEnabledFor(string userId) 108 | { 109 | return true; 110 | } 111 | 112 | private LiveTvService GetService() 113 | { 114 | return LiveTvService.Instance; 115 | } 116 | 117 | public bool CanDelete(BaseItem item) 118 | { 119 | return !item.IsFolder; 120 | } 121 | 122 | public Task DeleteItem(string id, CancellationToken cancellationToken) 123 | { 124 | return GetService().DeleteRecordingAsync(id, cancellationToken); 125 | } 126 | 127 | public async Task> GetLatestMedia(ChannelLatestMediaSearch request, CancellationToken cancellationToken) 128 | { 129 | var result = await GetChannelItems(new InternalChannelItemQuery(), i => true, cancellationToken).ConfigureAwait(false); 130 | 131 | return result.Items.OrderByDescending(i => i.DateCreated ?? DateTime.MinValue); 132 | } 133 | 134 | public Task GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken) 135 | { 136 | if (string.IsNullOrWhiteSpace(query.FolderId)) 137 | { 138 | return GetRecordingGroups(query, cancellationToken); 139 | } 140 | 141 | if (query.FolderId.StartsWith("series_", StringComparison.OrdinalIgnoreCase)) 142 | { 143 | var hash = query.FolderId.Split('_')[1]; 144 | return GetChannelItems(query, i => i.IsSeries && string.Equals(i.Name.GetMD5().ToString("N"), hash, StringComparison.Ordinal), cancellationToken); 145 | } 146 | 147 | if (string.Equals(query.FolderId, "kids", StringComparison.OrdinalIgnoreCase)) 148 | { 149 | return GetChannelItems(query, i => i.IsKids, cancellationToken); 150 | } 151 | 152 | if (string.Equals(query.FolderId, "movies", StringComparison.OrdinalIgnoreCase)) 153 | { 154 | return GetChannelItems(query, i => i.IsMovie, cancellationToken); 155 | } 156 | 157 | if (string.Equals(query.FolderId, "news", StringComparison.OrdinalIgnoreCase)) 158 | { 159 | return GetChannelItems(query, i => i.IsNews, cancellationToken); 160 | } 161 | 162 | if (string.Equals(query.FolderId, "sports", StringComparison.OrdinalIgnoreCase)) 163 | { 164 | return GetChannelItems(query, i => i.IsSports, cancellationToken); 165 | } 166 | 167 | if (string.Equals(query.FolderId, "others", StringComparison.OrdinalIgnoreCase)) 168 | { 169 | return GetChannelItems(query, i => !i.IsSports && !i.IsNews && !i.IsMovie && !i.IsKids && !i.IsSeries, cancellationToken); 170 | } 171 | 172 | var result = new ChannelItemResult() 173 | { 174 | Items = new List() 175 | }; 176 | 177 | return Task.FromResult(result); 178 | } 179 | 180 | public async Task GetChannelItems(InternalChannelItemQuery query, Func filter, CancellationToken cancellationToken) 181 | { 182 | var service = GetService(); 183 | var allRecordings = await service.GetAllRecordingsAsync(cancellationToken).ConfigureAwait(false); 184 | 185 | return new ChannelItemResult() 186 | { 187 | Items = allRecordings.Where(filter).Select(ConvertToChannelItem).ToList() 188 | }; 189 | } 190 | 191 | private ChannelItemInfo ConvertToChannelItem(MyRecordingInfo item) 192 | { 193 | var path = string.IsNullOrEmpty(item.Path) ? item.Url : item.Path; 194 | 195 | var channelItem = new ChannelItemInfo 196 | { 197 | Name = string.IsNullOrEmpty(item.EpisodeTitle) ? item.Name : item.EpisodeTitle, 198 | SeriesName = !string.IsNullOrEmpty(item.EpisodeTitle) || item.IsSeries ? item.Name : null, 199 | OfficialRating = item.OfficialRating, 200 | CommunityRating = item.CommunityRating, 201 | ContentType = item.IsMovie ? ChannelMediaContentType.Movie : (item.IsSeries ? ChannelMediaContentType.Episode : ChannelMediaContentType.Clip), 202 | Genres = item.Genres, 203 | ImageUrl = item.ImageUrl, 204 | //HomePageUrl = item.HomePageUrl 205 | Id = item.Id, 206 | //IndexNumber = item.IndexNumber, 207 | MediaType = item.ChannelType == ChannelType.TV ? ChannelMediaType.Video : ChannelMediaType.Audio, 208 | MediaSources = new List 209 | { 210 | new MediaSourceInfo 211 | { 212 | Path = path, 213 | Protocol = path.StartsWith("http", StringComparison.OrdinalIgnoreCase) ? MediaProtocol.Http : MediaProtocol.File, 214 | Id = item.Id 215 | } 216 | }, 217 | //ParentIndexNumber = item.ParentIndexNumber, 218 | PremiereDate = item.OriginalAirDate, 219 | //ProductionYear = item.ProductionYear, 220 | //Studios = item.Studios, 221 | Type = ChannelItemType.Media, 222 | DateModified = item.DateLastUpdated, 223 | Overview = item.Overview, 224 | //People = item.People 225 | IsLiveStream = item.Status == RecordingStatus.InProgress, 226 | Etag = item.Status.ToString() 227 | }; 228 | 229 | return channelItem; 230 | } 231 | 232 | private async Task GetRecordingGroups(InternalChannelItemQuery query, CancellationToken cancellationToken) 233 | { 234 | var service = GetService(); 235 | 236 | var allRecordings = await service.GetAllRecordingsAsync(cancellationToken).ConfigureAwait(false); 237 | 238 | var series = allRecordings 239 | .Where(i => i.IsSeries) 240 | .ToLookup(i => i.Name, StringComparer.OrdinalIgnoreCase); 241 | 242 | var items = new List(); 243 | 244 | items.AddRange(series.OrderBy(i => i.Key).Select(i => new ChannelItemInfo 245 | { 246 | Name = i.Key, 247 | FolderType = ChannelFolderType.Container, 248 | Id = "series_" + i.Key.GetMD5().ToString("N"), 249 | Type = ChannelItemType.Folder, 250 | ImageUrl = i.First().ImageUrl 251 | })); 252 | 253 | var kids = allRecordings.FirstOrDefault(i => i.IsKids); 254 | 255 | if (kids != null) 256 | { 257 | items.Add(new ChannelItemInfo 258 | { 259 | Name = "Kids", 260 | FolderType = ChannelFolderType.Container, 261 | Id = "kids", 262 | Type = ChannelItemType.Folder, 263 | ImageUrl = kids.ImageUrl 264 | }); 265 | } 266 | 267 | var movies = allRecordings.FirstOrDefault(i => i.IsMovie); 268 | if (movies != null) 269 | { 270 | items.Add(new ChannelItemInfo 271 | { 272 | Name = "Movies", 273 | FolderType = ChannelFolderType.Container, 274 | Id = "movies", 275 | Type = ChannelItemType.Folder, 276 | ImageUrl = movies.ImageUrl 277 | }); 278 | } 279 | 280 | var news = allRecordings.FirstOrDefault(i => i.IsNews); 281 | if (news != null) 282 | { 283 | items.Add(new ChannelItemInfo 284 | { 285 | Name = "News", 286 | FolderType = ChannelFolderType.Container, 287 | Id = "news", 288 | Type = ChannelItemType.Folder, 289 | ImageUrl = news.ImageUrl 290 | }); 291 | } 292 | 293 | var sports = allRecordings.FirstOrDefault(i => i.IsSports); 294 | if (sports != null) 295 | { 296 | items.Add(new ChannelItemInfo 297 | { 298 | Name = "Sports", 299 | FolderType = ChannelFolderType.Container, 300 | Id = "sports", 301 | Type = ChannelItemType.Folder, 302 | ImageUrl = sports.ImageUrl 303 | }); 304 | } 305 | 306 | var other = allRecordings.FirstOrDefault(i => !i.IsSports && !i.IsNews && !i.IsMovie && !i.IsKids && !i.IsSeries); 307 | if (other != null) 308 | { 309 | items.Add(new ChannelItemInfo 310 | { 311 | Name = "Others", 312 | FolderType = ChannelFolderType.Container, 313 | Id = "others", 314 | Type = ChannelItemType.Folder, 315 | ImageUrl = other.ImageUrl 316 | }); 317 | } 318 | 319 | return new ChannelItemResult() 320 | { 321 | Items = items 322 | }; 323 | } 324 | } 325 | public class MyRecordingInfo 326 | { 327 | /// 328 | /// Id of the recording. 329 | /// 330 | public string Id { get; set; } 331 | 332 | /// 333 | /// Gets or sets the series timer identifier. 334 | /// 335 | /// The series timer identifier. 336 | public string SeriesTimerId { get; set; } 337 | 338 | /// 339 | /// Gets or sets the timer identifier. 340 | /// 341 | /// The timer identifier. 342 | public string TimerId { get; set; } 343 | 344 | /// 345 | /// ChannelId of the recording. 346 | /// 347 | public string ChannelId { get; set; } 348 | 349 | /// 350 | /// Gets or sets the type of the channel. 351 | /// 352 | /// The type of the channel. 353 | public ChannelType ChannelType { get; set; } 354 | 355 | /// 356 | /// Name of the recording. 357 | /// 358 | public string Name { get; set; } 359 | 360 | /// 361 | /// Gets or sets the path. 362 | /// 363 | /// The path. 364 | public string Path { get; set; } 365 | 366 | /// 367 | /// Gets or sets the URL. 368 | /// 369 | /// The URL. 370 | public string Url { get; set; } 371 | 372 | /// 373 | /// Gets or sets the overview. 374 | /// 375 | /// The overview. 376 | public string Overview { get; set; } 377 | 378 | /// 379 | /// The start date of the recording, in UTC. 380 | /// 381 | public DateTime StartDate { get; set; } 382 | 383 | /// 384 | /// The end date of the recording, in UTC. 385 | /// 386 | public DateTime EndDate { get; set; } 387 | 388 | /// 389 | /// Gets or sets the program identifier. 390 | /// 391 | /// The program identifier. 392 | public string ProgramId { get; set; } 393 | 394 | /// 395 | /// Gets or sets the status. 396 | /// 397 | /// The status. 398 | public RecordingStatus Status { get; set; } 399 | 400 | /// 401 | /// Genre of the program. 402 | /// 403 | public List Genres { get; set; } 404 | 405 | /// 406 | /// Gets or sets a value indicating whether this instance is repeat. 407 | /// 408 | /// true if this instance is repeat; otherwise, false. 409 | public bool IsRepeat { get; set; } 410 | 411 | /// 412 | /// Gets or sets the episode title. 413 | /// 414 | /// The episode title. 415 | public string EpisodeTitle { get; set; } 416 | 417 | /// 418 | /// Gets or sets a value indicating whether this instance is hd. 419 | /// 420 | /// true if this instance is hd; otherwise, false. 421 | public bool? IsHD { get; set; } 422 | 423 | /// 424 | /// Gets or sets the audio. 425 | /// 426 | /// The audio. 427 | public ProgramAudio? Audio { get; set; } 428 | 429 | /// 430 | /// Gets or sets the original air date. 431 | /// 432 | /// The original air date. 433 | public DateTime? OriginalAirDate { get; set; } 434 | 435 | /// 436 | /// Gets or sets a value indicating whether this instance is movie. 437 | /// 438 | /// true if this instance is movie; otherwise, false. 439 | public bool IsMovie { get; set; } 440 | 441 | /// 442 | /// Gets or sets a value indicating whether this instance is sports. 443 | /// 444 | /// true if this instance is sports; otherwise, false. 445 | public bool IsSports { get; set; } 446 | 447 | /// 448 | /// Gets or sets a value indicating whether this instance is series. 449 | /// 450 | /// true if this instance is series; otherwise, false. 451 | public bool IsSeries { get; set; } 452 | 453 | /// 454 | /// Gets or sets a value indicating whether this instance is live. 455 | /// 456 | /// true if this instance is live; otherwise, false. 457 | public bool IsLive { get; set; } 458 | 459 | /// 460 | /// Gets or sets a value indicating whether this instance is news. 461 | /// 462 | /// true if this instance is news; otherwise, false. 463 | public bool IsNews { get; set; } 464 | 465 | /// 466 | /// Gets or sets a value indicating whether this instance is kids. 467 | /// 468 | /// true if this instance is kids; otherwise, false. 469 | public bool IsKids { get; set; } 470 | 471 | /// 472 | /// Gets or sets a value indicating whether this instance is premiere. 473 | /// 474 | /// true if this instance is premiere; otherwise, false. 475 | public bool IsPremiere { get; set; } 476 | 477 | /// 478 | /// Gets or sets the official rating. 479 | /// 480 | /// The official rating. 481 | public string OfficialRating { get; set; } 482 | 483 | /// 484 | /// Gets or sets the community rating. 485 | /// 486 | /// The community rating. 487 | public float? CommunityRating { get; set; } 488 | 489 | /// 490 | /// Supply the image path if it can be accessed directly from the file system 491 | /// 492 | /// The image path. 493 | public string ImagePath { get; set; } 494 | 495 | /// 496 | /// Supply the image url if it can be downloaded 497 | /// 498 | /// The image URL. 499 | public string ImageUrl { get; set; } 500 | 501 | /// 502 | /// Gets or sets a value indicating whether this instance has image. 503 | /// 504 | /// null if [has image] contains no value, true if [has image]; otherwise, false. 505 | public bool? HasImage { get; set; } 506 | /// 507 | /// Gets or sets the show identifier. 508 | /// 509 | /// The show identifier. 510 | public string ShowId { get; set; } 511 | 512 | /// 513 | /// Gets or sets the date last updated. 514 | /// 515 | /// The date last updated. 516 | public DateTime DateLastUpdated { get; set; } 517 | 518 | public MyRecordingInfo() 519 | { 520 | Genres = new List(); 521 | } 522 | } 523 | 524 | } 525 | -------------------------------------------------------------------------------- /Jellyfin.Plugin.Enigma2/ServiceRegistrator.cs: -------------------------------------------------------------------------------- 1 | using MediaBrowser.Controller.Channels; 2 | using MediaBrowser.Controller.LiveTv; 3 | using MediaBrowser.Controller.Plugins; 4 | using MediaBrowser.Controller; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using System.Net.Http; 7 | using Jellyfin.Plugin.Enigma2.Helpers; 8 | 9 | namespace Jellyfin.Plugin.Enigma2 10 | { 11 | public class ServiceRegistrator : IPluginServiceRegistrator 12 | { 13 | /// 14 | public void RegisterServices(IServiceCollection serviceCollection, IServerApplicationHost applicationHost) 15 | { 16 | serviceCollection.AddSingleton(); 17 | serviceCollection.AddSingleton(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MediaBrowser.Plugins.VuPlus 2 | MediaBrowser Vu+ Plugin 3 | 4 | Plugin for MediaBrowser to allow it to view channels, browse epg, set and delete timers, play recordings etc. 5 | 6 | Known Issues / Limitations 7 | -------------------------- 8 | 9 | Genres are set to 'Unknown'. 10 | No Series related functionality. 11 | If paths are entered on the config page then remember to end them with a slash 12 | 13 | 14 | Setup on Vu+ / Enigma2 15 | ---------------------- 16 | 17 | Ensure openWebIf is installed and running - go into settings and make a note of them so they can be entered on the MediaBrowser Vu+ config page. 18 | 19 | 20 | Setup on MediaBrowser server 21 | ---------------------------- 22 | 23 | Install Vu+ plugin and restart MediaBrowser server. 24 | Go to config page for Vu+ and amend default contents as follows (at a minimum, the first 3 must be entered): 25 | 26 | Vu+ hostname or ip address: 27 | The host name (address) or ip address of your Vu+ receiver 28 | 29 | Vu+ streaming port: 30 | The Streaming port of your Vu+ receiver eg. 8001 / 8002 31 | 32 | Vu+ Web Interface port: 33 | The web Interface port of your receiver eg. 8000 34 | 35 | Vu+ Web Interface username: 36 | The web Interface username of your receiver (optional) 37 | Vu+ Web Interface password: 38 | The web Interface password of your receiver (optional) 39 | 40 | Use secure HTTP (HTTPS): 41 | Use HTTPS instead of HTTPS to connect to your receiver 42 | 43 | Fetch Only one TV bouquet: 44 | Limit channels to only those contained within the specified TV Bouquet below (optional) 45 | Vu+ TVBouquet: 46 | The TV Bouquet to load channels for (optional - only required if 'Fetch Only one TV bouquet' set) 47 | 48 | Zap before Channel switch (single tuner) 49 | Set if only one tuner within receiver to make tuner jump to channel 50 | 51 | Fetch picons from webinterface: 52 | Set if you want to retrieve Picons from the web interface of the receiver 53 | Picons Path: 54 | The local location of your Picons (must end with appropriate slash) eg. C:\Picons\ (optional - only required if 'Fetch picons from webinterface' is not set) 55 | 56 | Recording Path: 57 | The location to store your recordings on your receiver (must end with appropriate slash) eg. /hdd/movie/ (optional) 58 | 59 | Enable VuPlus debug logging: 60 | Plugin Debugging 61 | 62 | -------------------------------------------------------------------------------- /build.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # The name of the plugin; official plugins always start "jellyfin-plugin-" 3 | name: "jellyfin-plugin-enigma2" 4 | 5 | # The GUID of the plugin, found in Plugin.cs 6 | guid: "193f29f9-ea6c-4595-a6f6-55e79d7c590a" 7 | 8 | # The version of the plugin, starting at 1. Note that the AssemblyVersion 9 | # and FileVersion flags in the `.csproj` file must have two additional 10 | # fiels, e.g. 1.0.0 to be built correctly. 11 | version: "5.0.1.0" # Please increment with each pull request 12 | 13 | # The supported Jellyfin version, usually the earliest binary-compatible 14 | # version. Based on the Jellyfin components from NuGet. 15 | targetAbi: "10.8.0.0" # The earliest binary-compatible version 16 | 17 | framework: "net6.0" 18 | 19 | # The owner name of the plugin, "jellyfin" for official plugins. Change 20 | # to your own name if you wish to remain a 3rd-party plugin. 21 | owner: "knackebrot" 22 | 23 | # A short description of the plugin 24 | overview: "Enigma2" 25 | 26 | # A longer and more detailed description of the plugin; use multiple 27 | # lines as required for readability. 28 | description: > 29 | Live TV plugin for watching Enigma 2 channels and recordings 30 | 31 | # The plugin category, in a general sense. These fields are dynamic. 32 | category: "Live TV" 33 | 34 | # A list of built artifacts to be bundled into the ZIP for installation. 35 | # Include the main output file, as well as any dependency `.dll`s that 36 | # might be required for the plugin to run. 37 | artifacts: 38 | - "Jellyfin.Plugin.Enigma2.dll" 39 | 40 | # Build information values for the build infrastructure; these should 41 | # not need to be changed usually. 42 | build_type: "dotnet" 43 | dotnet_configuration: "Release" 44 | dotnet_framework: "net6.0" 45 | --------------------------------------------------------------------------------