├── .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 |
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 |
--------------------------------------------------------------------------------