mirror of
https://github.com/Jellyfin2Samsung/Samsung-Jellyfin-Installer.git
synced 2026-03-01 11:21:12 +03:00
Refactor: Consolidate helpers, standardize JSON library, and improve code organization
Summary
- Introduced centralized helper classes to eliminate code duplication and improve maintainability
- Standardized JSON serialization from Newtonsoft.Json to System.Text.Json
- Refactored large methods into focused, testable units
- Added pre-compiled regex patterns for better performance
New Helper Classes
| File | Purpose |
|------|---------|
| Helpers/Core/Constants.cs | Centralized magic strings, error codes, ports, app identifiers |
| Helpers/Core/UrlHelper.cs | URL normalization (NormalizeServerUrl, CombineUrl, IsValidHttpUrl) |
| Helpers/Core/PlatformService.cs | Cross-platform OS detection and platform-specific operations |
| Helpers/Core/RegexPatterns.cs | Pre-compiled regex patterns for version parsing, Tizen capabilities, HTML, etc. |
| Helpers/Core/JsonSerializerOptionsProvider.cs | Centralized JsonSerializerOptions configurations |
Key Changes
TizenInstallerService Refactoring
Split 215-line InstallPackageAsync into focused methods:
- PrepareDeviceAsync() – Device preparation and diagnostics
- GetDeviceInfoAsync() – Device information collection
- HandleCertificateAsync() – Certificate handling logic
- ApplyConfigurationAsync() – Configuration application
- HandleInstallationResultAsync() – Result processing
JSON Library Migration
Migrated from Newtonsoft.Json to System.Text.Json:
- Models/GitHubRelease.cs
- Helpers/Core/AddLatestRelease.cs
- Helpers/API/TizenApiClient.cs
- Services/SamsungLoginService.cs
- Services/TizenInstallerService.cs
- ViewModels/MainWindowViewModel.cs
Code Quality Improvements
- Replaced 13× duplicate TrimEnd('/') calls with UrlHelper
- Unified OS detection via PlatformService (previously mixed OperatingSystem.* and RuntimeInformation.*)
- Used Random.Shared instead of new Random() for thread safety
- Eliminated magic strings/numbers via Constants class
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Jellyfin2Samsung.Models;
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
@@ -29,7 +30,7 @@ namespace Jellyfin2Samsung.Helpers.API
|
||||
return !string.IsNullOrEmpty(AppSettings.Default.JellyfinFullUrl) &&
|
||||
!string.IsNullOrEmpty(AppSettings.Default.JellyfinAccessToken) &&
|
||||
!string.IsNullOrEmpty(AppSettings.Default.JellyfinUserId) &&
|
||||
IsValidUrl($"{AppSettings.Default.JellyfinFullUrl}/Users");
|
||||
UrlHelper.IsValidHttpUrl($"{AppSettings.Default.JellyfinFullUrl}/Users");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -41,51 +42,40 @@ namespace Jellyfin2Samsung.Helpers.API
|
||||
!string.IsNullOrEmpty(AppSettings.Default.JellyfinUserId);
|
||||
}
|
||||
|
||||
public static bool IsValidUrl(string url)
|
||||
{
|
||||
return Uri.TryCreate(url, UriKind.Absolute, out var uriResult)
|
||||
&& (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
|
||||
}
|
||||
|
||||
public async Task<List<JellyfinPluginInfo>> GetInstalledPluginsAsync(string serverUrl)
|
||||
{
|
||||
var list = new List<JellyfinPluginInfo>();
|
||||
try
|
||||
{
|
||||
string url = serverUrl.TrimEnd('/') + "/Plugins";
|
||||
Trace.WriteLine("▶ Fetching installed plugins from: " + url);
|
||||
string url = UrlHelper.CombineUrl(serverUrl, "/Plugins");
|
||||
Trace.WriteLine("Fetching installed plugins from: " + url);
|
||||
var json = await _httpClient.GetStringAsync(url);
|
||||
var parsed = JsonSerializer.Deserialize<List<JellyfinPluginInfo>>(json,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
var parsed = JsonSerializer.Deserialize<List<JellyfinPluginInfo>>(json, JsonSerializerOptionsProvider.Default);
|
||||
|
||||
if (parsed != null)
|
||||
list.AddRange(parsed);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine("⚠ Failed to fetch /Plugins: " + ex);
|
||||
Trace.WriteLine("Failed to fetch /Plugins: " + ex);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public async Task<JellyfinPublicSystemInfo?> GetPublicSystemInfoAsync(string serverUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
string url = serverUrl.TrimEnd('/') + "/System/Info/Public";
|
||||
Trace.WriteLine("▶ Fetching Jellyfin public system info from: " + url);
|
||||
string url = UrlHelper.CombineUrl(serverUrl, "/System/Info/Public");
|
||||
Trace.WriteLine("Fetching Jellyfin public system info from: " + url);
|
||||
|
||||
var json = await _httpClient.GetStringAsync(url);
|
||||
|
||||
var info = JsonSerializer.Deserialize<JellyfinPublicSystemInfo>(
|
||||
json,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
return info;
|
||||
return JsonSerializer.Deserialize<JellyfinPublicSystemInfo>(json, JsonSerializerOptionsProvider.Default);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine("⚠ Failed to fetch /System/Info/Public: " + ex);
|
||||
Trace.WriteLine("Failed to fetch /System/Info/Public: " + ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -98,13 +88,14 @@ namespace Jellyfin2Samsung.Helpers.API
|
||||
try
|
||||
{
|
||||
SetupHeaders();
|
||||
var serverUrl = UrlHelper.NormalizeServerUrl(AppSettings.Default.JellyfinFullUrl);
|
||||
|
||||
foreach (string userId in userIds.Where(u => !string.IsNullOrWhiteSpace(u)))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Fetch user info
|
||||
var getUserResponse = await _httpClient.GetAsync($"{AppSettings.Default.JellyfinFullUrl}/Users/{userId}");
|
||||
var getUserResponse = await _httpClient.GetAsync($"{serverUrl}/Users/{userId}");
|
||||
getUserResponse.EnsureSuccessStatusCode();
|
||||
var userJson = await getUserResponse.Content.ReadAsStringAsync();
|
||||
|
||||
@@ -114,13 +105,13 @@ namespace Jellyfin2Samsung.Helpers.API
|
||||
userNode["EnableAutoLogin"] = AppSettings.Default.UserAutoLogin;
|
||||
|
||||
var userContent = new StringContent(
|
||||
userNode.ToJsonString(new JsonSerializerOptions { WriteIndented = true }),
|
||||
userNode.ToJsonString(JsonSerializerOptionsProvider.Indented),
|
||||
Encoding.UTF8,
|
||||
"application/json");
|
||||
Constants.Api.JsonContentType);
|
||||
|
||||
using (userContent)
|
||||
{
|
||||
var userResponse = await _httpClient.PostAsync($"{AppSettings.Default.JellyfinFullUrl}/Users?userId={userId}", userContent);
|
||||
var userResponse = await _httpClient.PostAsync($"{serverUrl}/Users?userId={userId}", userContent);
|
||||
userResponse.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
@@ -135,10 +126,10 @@ namespace Jellyfin2Samsung.Helpers.API
|
||||
EnableNextEpisodeAutoPlay = AppSettings.Default.AutoPlayNextEpisode,
|
||||
};
|
||||
|
||||
var configJson = JsonSerializer.Serialize(userConfig, new JsonSerializerOptions { WriteIndented = true });
|
||||
var configJson = JsonSerializer.Serialize(userConfig, JsonSerializerOptionsProvider.Indented);
|
||||
|
||||
using var configContent = new StringContent(configJson, Encoding.UTF8, "application/json");
|
||||
var configResponse = await _httpClient.PostAsync($"{AppSettings.Default.JellyfinFullUrl}/Users/Configuration?userId={userId}", configContent);
|
||||
using var configContent = new StringContent(configJson, Encoding.UTF8, Constants.Api.JsonContentType);
|
||||
var configResponse = await _httpClient.PostAsync($"{serverUrl}/Users/Configuration?userId={userId}", configContent);
|
||||
configResponse.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception userEx)
|
||||
@@ -160,8 +151,9 @@ namespace Jellyfin2Samsung.Helpers.API
|
||||
private void SetupHeaders()
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Clear();
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("SamsungJellyfinInstaller/1.0");
|
||||
_httpClient.DefaultRequestHeaders.Add("Authorization", $"MediaBrowser Token=\"{AppSettings.Default.JellyfinAccessToken}\"");
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(Constants.Api.UserAgent);
|
||||
_httpClient.DefaultRequestHeaders.Add("Authorization",
|
||||
string.Format(Constants.Api.MediaBrowserAuthHeader, AppSettings.Default.JellyfinAccessToken));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -172,12 +164,11 @@ namespace Jellyfin2Samsung.Helpers.API
|
||||
{
|
||||
try
|
||||
{
|
||||
var serverUrl = AppSettings.Default.JellyfinFullUrl.TrimEnd('/');
|
||||
var serverUrl = UrlHelper.NormalizeServerUrl(AppSettings.Default.JellyfinFullUrl);
|
||||
var authUrl = $"{serverUrl}/Users/AuthenticateByName";
|
||||
|
||||
_httpClient.DefaultRequestHeaders.Clear();
|
||||
_httpClient.DefaultRequestHeaders.Add("X-Emby-Authorization",
|
||||
"MediaBrowser Client=\"Samsung Jellyfin Installer\", Device=\"PC\", DeviceId=\"samsungjellyfin\", Version=\"1.0.0\"");
|
||||
_httpClient.DefaultRequestHeaders.Add("X-Emby-Authorization", Constants.Api.EmbyAuthHeader);
|
||||
|
||||
var authPayload = new
|
||||
{
|
||||
@@ -186,7 +177,7 @@ namespace Jellyfin2Samsung.Helpers.API
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(authPayload);
|
||||
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
using var content = new StringContent(json, Encoding.UTF8, Constants.Api.JsonContentType);
|
||||
|
||||
var response = await _httpClient.PostAsync(authUrl, content);
|
||||
|
||||
@@ -226,7 +217,7 @@ namespace Jellyfin2Samsung.Helpers.API
|
||||
try
|
||||
{
|
||||
SetupHeaders();
|
||||
var serverUrl = AppSettings.Default.JellyfinFullUrl.TrimEnd('/');
|
||||
var serverUrl = UrlHelper.NormalizeServerUrl(AppSettings.Default.JellyfinFullUrl);
|
||||
var usersUrl = $"{serverUrl}/Users";
|
||||
|
||||
Trace.WriteLine($"[LoadUsers] Fetching users from: {usersUrl}");
|
||||
@@ -276,8 +267,7 @@ namespace Jellyfin2Samsung.Helpers.API
|
||||
{
|
||||
try
|
||||
{
|
||||
var serverUrl = AppSettings.Default.JellyfinFullUrl.TrimEnd('/');
|
||||
var testUrl = $"{serverUrl}/System/Info/Public";
|
||||
var testUrl = UrlHelper.CombineUrl(AppSettings.Default.JellyfinFullUrl, "/System/Info/Public");
|
||||
|
||||
_httpClient.DefaultRequestHeaders.Clear();
|
||||
var response = await _httpClient.GetAsync(testUrl);
|
||||
@@ -289,4 +279,4 @@ namespace Jellyfin2Samsung.Helpers.API
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.API
|
||||
@@ -13,6 +14,7 @@ namespace Jellyfin2Samsung.Helpers.API
|
||||
{
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public TizenApiClient(
|
||||
HttpClient httpClient,
|
||||
IDialogService dialogService)
|
||||
@@ -25,40 +27,51 @@ namespace Jellyfin2Samsung.Helpers.API
|
||||
{
|
||||
try
|
||||
{
|
||||
string url = $"http://{device.IpAddress}:8001/api/v2/";
|
||||
string url = $"http://{device.IpAddress}:{Constants.Ports.SamsungTvApiPort}/api/v2/";
|
||||
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
string jsonContent = await response.Content.ReadAsStringAsync();
|
||||
JObject jsonObject = JObject.Parse(jsonContent);
|
||||
var jsonObject = JsonNode.Parse(jsonContent);
|
||||
|
||||
var deviceNode = jsonObject?["device"];
|
||||
if (deviceNode == null)
|
||||
{
|
||||
return CreateFallbackDevice(device);
|
||||
}
|
||||
|
||||
return new NetworkDevice
|
||||
{
|
||||
IpAddress = jsonObject["device"]?["ip"]?.ToString(),
|
||||
DeviceName = WebUtility.HtmlDecode(jsonObject["device"]?["name"]?.ToString()),
|
||||
ModelName = jsonObject["device"]?["modelName"].ToString(),
|
||||
Manufacturer = jsonObject["device"]?["type"]?.ToString(),
|
||||
DeveloperMode = jsonObject["device"]?["developerMode"]?.ToString() ?? string.Empty,
|
||||
DeveloperIP = jsonObject["device"]?["developerIP"]?.ToString() ?? string.Empty
|
||||
IpAddress = deviceNode["ip"]?.GetValue<string>() ?? device.IpAddress,
|
||||
DeviceName = WebUtility.HtmlDecode(deviceNode["name"]?.GetValue<string>() ?? string.Empty),
|
||||
ModelName = deviceNode["modelName"]?.GetValue<string>() ?? string.Empty,
|
||||
Manufacturer = deviceNode["type"]?.GetValue<string>() ?? string.Empty,
|
||||
DeveloperMode = deviceNode["developerMode"]?.GetValue<string>() ?? string.Empty,
|
||||
DeveloperIP = deviceNode["developerIP"]?.GetValue<string>() ?? string.Empty
|
||||
};
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
await _dialogService.ShowErrorAsync(
|
||||
$"Error connecting to Samsung TV at {device.IpAddress}: {ex}");
|
||||
$"Error connecting to Samsung TV at {device.IpAddress}: {ex.Message}");
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
await _dialogService.ShowErrorAsync(
|
||||
$"Error parsing JSON response: {ex}");
|
||||
$"Error parsing JSON response: {ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _dialogService.ShowErrorAsync(
|
||||
$"Unexpected error: {ex}");
|
||||
$"Unexpected error: {ex.Message}");
|
||||
}
|
||||
|
||||
return CreateFallbackDevice(device);
|
||||
}
|
||||
|
||||
private static NetworkDevice CreateFallbackDevice(NetworkDevice device)
|
||||
{
|
||||
return new NetworkDevice
|
||||
{
|
||||
IpAddress = device.IpAddress,
|
||||
|
||||
@@ -101,7 +101,7 @@ namespace Jellyfin2Samsung.Helpers
|
||||
{
|
||||
get
|
||||
{
|
||||
var baseUrl = JellyfinIP?.TrimEnd('/') ?? "";
|
||||
var baseUrl = Core.UrlHelper.NormalizeServerUrl(JellyfinIP);
|
||||
var basePath = JellyfinBasePath?.Trim('/') ?? "";
|
||||
|
||||
if (string.IsNullOrEmpty(basePath))
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using Jellyfin2Samsung.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
@@ -17,40 +16,49 @@ namespace Jellyfin2Samsung.Helpers.Core
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<GitHubRelease?> GetLatestReleaseAsync(
|
||||
string url,
|
||||
string displayName,
|
||||
JsonSerializerSettings settings)
|
||||
public async Task<GitHubRelease?> GetLatestReleaseAsync(string url, string displayName)
|
||||
{
|
||||
using var stream = await _httpClient.GetStreamAsync(url);
|
||||
using var sr = new StreamReader(stream);
|
||||
using var reader = new JsonTextReader(sr);
|
||||
|
||||
var serializer = JsonSerializer.Create(settings);
|
||||
|
||||
GitHubRelease? latest = null;
|
||||
|
||||
if (reader.Read())
|
||||
try
|
||||
{
|
||||
// Case 1: API returns array (normal GitHub behavior)
|
||||
if (reader.TokenType == JsonToken.StartArray)
|
||||
using var response = await _httpClient.GetAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync();
|
||||
|
||||
// Try to parse as array first (normal GitHub releases endpoint)
|
||||
try
|
||||
{
|
||||
reader.Read(); // move to first element
|
||||
latest = serializer.Deserialize<GitHubRelease>(reader);
|
||||
var releases = await JsonSerializer.DeserializeAsync<List<GitHubRelease>>(
|
||||
stream,
|
||||
JsonSerializerOptionsProvider.Default);
|
||||
|
||||
var latest = releases?.Count > 0 ? releases[0] : null;
|
||||
|
||||
if (latest != null)
|
||||
latest.Name = displayName;
|
||||
|
||||
return latest;
|
||||
}
|
||||
// Case 2: API returns single object
|
||||
else if (reader.TokenType == JsonToken.StartObject)
|
||||
catch (JsonException)
|
||||
{
|
||||
latest = serializer.Deserialize<GitHubRelease>(reader);
|
||||
// If array parsing fails, try parsing as single object
|
||||
stream.Position = 0;
|
||||
|
||||
var latest = await JsonSerializer.DeserializeAsync<GitHubRelease>(
|
||||
stream,
|
||||
JsonSerializerOptionsProvider.Default);
|
||||
|
||||
if (latest != null)
|
||||
latest.Name = displayName;
|
||||
|
||||
return latest;
|
||||
}
|
||||
}
|
||||
|
||||
if (latest == null)
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Trace.WriteLine($"Failed to fetch release from {url}: {ex}");
|
||||
return null;
|
||||
|
||||
latest.Name = displayName;
|
||||
return latest;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
235
Jellyfin2Samsung-CrossOS/Helpers/Core/Constants.cs
Normal file
235
Jellyfin2Samsung-CrossOS/Helpers/Core/Constants.cs
Normal file
@@ -0,0 +1,235 @@
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Centralized constants for the application.
|
||||
/// Eliminates magic strings and numbers scattered throughout the codebase.
|
||||
/// </summary>
|
||||
public static class Constants
|
||||
{
|
||||
/// <summary>
|
||||
/// Application identifiers and names.
|
||||
/// </summary>
|
||||
public static class AppIdentifiers
|
||||
{
|
||||
public const string JellyfinAppName = "Jellyfin";
|
||||
public const string Jelly2SamsDefault = "Jelly2Sams (default)";
|
||||
public const string Jelly2Sams = "Jelly2Sams";
|
||||
public const string CustomWgtFile = "Custom WGT File";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tizen installation error codes returned by the SDB tool.
|
||||
/// </summary>
|
||||
public static class TizenErrorCodes
|
||||
{
|
||||
public const string DownloadFailed116 = "download failed[116]";
|
||||
public const string InstallFailed118012 = "install failed[118012]";
|
||||
public const string InstallFailed118Minus12 = "install failed[118, -12]";
|
||||
public const string InstallFailed118 = "install failed[118]";
|
||||
public const string Installing100 = "installing[100]";
|
||||
public const string InstallCompleted = "install completed";
|
||||
public const string ResignFailed = "Re-sign failed";
|
||||
public const string Failed = "failed";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default values used throughout the application.
|
||||
/// </summary>
|
||||
public static class Defaults
|
||||
{
|
||||
public const string TizenOsVersion = "7.0";
|
||||
public const string SdkToolPath = "/opt/usr/apps/tmp";
|
||||
public const string HomeDeveloperPath = "/home/developer";
|
||||
public const string TizenSdbDefaultVersion = "v1.0.0";
|
||||
public const int SamsungLoginTimeoutMinutes = 5;
|
||||
public const int NetworkScanTimeoutMs = 1000;
|
||||
public const int HttpRequestTimeoutSeconds = 15;
|
||||
public const int WebSocketMonitorDelaySeconds = 10;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tizen version thresholds for feature compatibility.
|
||||
/// </summary>
|
||||
public static class TizenVersions
|
||||
{
|
||||
public const string CertificateRequired = "7.0";
|
||||
public const string PushInstallMax = "4.0";
|
||||
public const string IntermediateVersion = "3.0";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Network ports used by the application.
|
||||
/// </summary>
|
||||
public static class Ports
|
||||
{
|
||||
public const int TizenDevPort = 26101;
|
||||
public const int SamsungTvApiPort = 8001;
|
||||
public const int SamsungLoginCallbackPort = 4794;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// File extensions and patterns.
|
||||
/// </summary>
|
||||
public static class FilePatterns
|
||||
{
|
||||
public const string WgtExtension = ".wgt";
|
||||
public const string TpkExtension = ".tpk";
|
||||
public const string P12Extension = ".p12";
|
||||
public const string CsrExtension = ".csr";
|
||||
public const string CerExtension = ".cer";
|
||||
public const string CrtExtension = ".crt";
|
||||
public const string JsExtension = ".js";
|
||||
public const string CssExtension = ".css";
|
||||
public const string WgtPattern = "*.wgt";
|
||||
public const string TpkPattern = "*.tpk";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Platform-specific binary names.
|
||||
/// </summary>
|
||||
public static class PlatformBinaries
|
||||
{
|
||||
public const string TizenSdbWindowsPattern = "TizenSdb*.exe";
|
||||
public const string TizenSdbLinuxPattern = "TizenSdb*_linux";
|
||||
public const string TizenSdbMacOsPattern = "TizenSdb*_macos";
|
||||
public const string WindowsExtension = ".exe";
|
||||
public const string LinuxSuffix = "_linux";
|
||||
public const string MacOsSuffix = "_macos";
|
||||
public const string EsbuildWindows = "win-x64";
|
||||
public const string EsbuildLinux = "linux-x64";
|
||||
public const string EsbuildMacOs = "osx-universal";
|
||||
public const string EsbuildExecutable = "esbuild";
|
||||
public const string EsbuildExecutableWindows = "esbuild.exe";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP and API related constants.
|
||||
/// </summary>
|
||||
public static class Api
|
||||
{
|
||||
public const string UserAgent = "SamsungJellyfinInstaller/1.0";
|
||||
public const string MediaBrowserAuthHeader = "MediaBrowser Token=\"{0}\"";
|
||||
public const string EmbyAuthHeader = "MediaBrowser Client=\"Samsung Jellyfin Installer\", Device=\"PC\", DeviceId=\"samsungjellyfin\", Version=\"1.0.0\"";
|
||||
public const string JsonContentType = "application/json";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Samsung API endpoints and OAuth constants.
|
||||
/// </summary>
|
||||
public static class Samsung
|
||||
{
|
||||
public const string LoopbackHost = "localhost";
|
||||
public const string CallbackPath = "/signin/callback";
|
||||
public const string OAuthClientId = "v285zxnl3h";
|
||||
public const string OAuthState = "accountcheckdogeneratedstatetext";
|
||||
public const string TokenType = "TOKEN";
|
||||
public const string SignInGateUrl = "https://account.samsung.com/accounts/be1dce529476c1a6d407c4c7578c31bd/signInGate";
|
||||
public const string PlatformVd = "VD";
|
||||
public const string PrivilegeLevelPublic = "Public";
|
||||
public const string DeveloperTypeIndividual = "Individual";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Certificate related constants.
|
||||
/// </summary>
|
||||
public static class Certificate
|
||||
{
|
||||
public const string AuthorFileName = "author.p12";
|
||||
public const string DistributorFileName = "distributor.p12";
|
||||
public const string PasswordFileName = "password.txt";
|
||||
public const string DeviceProfileFileName = "device-profile.xml";
|
||||
public const string AuthorCsrFileName = "author.csr";
|
||||
public const string DistributorCsrFileName = "distributor.csr";
|
||||
public const string SignedAuthorCerFileName = "signed_author.cer";
|
||||
public const string SignedDistributorCerFileName = "signed_distributor.cer";
|
||||
public const string AuthorCaFileName = "vd_tizen_dev_author_ca.cer";
|
||||
public const string DistributorCaFileName = "vd_tizen_dev_public2.crt";
|
||||
public const string KeyAlias = "usercertificate";
|
||||
public const string CsrSubjectAuthor = "C=, ST=, L=, O=, OU=, CN=Jelly2Sams";
|
||||
public const string CsrSubjectDistributorTemplate = "CN=TizenSDK, OU=, O=, L=, ST=, C=, emailAddress={0}";
|
||||
public const string SigningAlgorithm = "SHA256withRSA";
|
||||
public const int RsaKeySize = 2048;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Jellyfin web app paths and file names.
|
||||
/// </summary>
|
||||
public static class JellyfinWeb
|
||||
{
|
||||
public const string IndexHtml = "index.html";
|
||||
public const string ConfigJson = "config.json";
|
||||
public const string WwwFolder = "www";
|
||||
public const string PluginCacheFolder = "plugin_cache";
|
||||
public const string CredentialsStorageKey = "jellyfin_credentials";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration update modes.
|
||||
/// </summary>
|
||||
public static class ConfigUpdateModes
|
||||
{
|
||||
public const string None = "None";
|
||||
public const string Server = "Server";
|
||||
public const string Browser = "Browser";
|
||||
public const string User = "User";
|
||||
public const string All = "All";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Random string generation character sets.
|
||||
/// </summary>
|
||||
public static class CharacterSets
|
||||
{
|
||||
public const string AlphaLower = "abcdefghijklmnopqrstuvwxyz";
|
||||
public const string AlphaUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
public const string Alpha = AlphaLower + AlphaUpper;
|
||||
public const string AlphaNumeric = Alpha + "0123456789";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Localization keys used for status messages.
|
||||
/// </summary>
|
||||
public static class LocalizationKeys
|
||||
{
|
||||
public const string InstallTizenSdb = "InstallTizenSdb";
|
||||
public const string DiagnoseTv = "diagnoseTv";
|
||||
public const string AlreadyInstalled = "alreadyInstalled";
|
||||
public const string DeleteExistingVersion = "deleteExistingVersion";
|
||||
public const string DeleteExistingFailed = "deleteExistingFailed";
|
||||
public const string DeleteExistingSuccess = "deleteExistingSuccess";
|
||||
public const string DeleteExistingNotAllowed = "deleteExistingNotAllowed";
|
||||
public const string ConnectingToDevice = "ConnectingToDevice";
|
||||
public const string TvNameNotFound = "TvNameNotFound";
|
||||
public const string TvDuidNotFound = "TvDuidNotFound";
|
||||
public const string SamsungLogin = "SamsungLogin";
|
||||
public const string CreatingCertificateProfile = "CreatingCertificateProfile";
|
||||
public const string PackageAndSign = "packageAndSign";
|
||||
public const string InstallingPackage = "InstallingPackage";
|
||||
public const string InstallationFailed = "InstallationFailed";
|
||||
public const string InstallationSuccessful = "InstallationSuccessful";
|
||||
public const string InsufficientSpace = "insufficientSpace";
|
||||
public const string AuthorMismatch = "AuthorMismatch";
|
||||
public const string ModifyConfigRequired = "modiyConfigRequired";
|
||||
public const string FailedTizenSdb = "FailedTizenSdb";
|
||||
public const string CheckingTizenSdb = "CheckingTizenSdb";
|
||||
public const string ScanningNetwork = "ScanningNetwork";
|
||||
public const string InitializationFailed = "InitializationFailed";
|
||||
public const string NoDevicesFound = "NoDevicesFound";
|
||||
public const string NoDevicesFoundRetry = "NoDevicesFoundRetry";
|
||||
public const string Ready = "Ready";
|
||||
public const string FailedLoadingReleases = "FailedLoadingReleases";
|
||||
public const string InvalidDeviceIp = "InvalidDeviceIp";
|
||||
public const string LblOther = "lblOther";
|
||||
public const string IpNotListed = "IpNotListed";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Esbuild transpilation settings.
|
||||
/// </summary>
|
||||
public static class Esbuild
|
||||
{
|
||||
public const string TempFolderName = "J2S_Esbuild";
|
||||
public const string TargetEs2015 = "es2015";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -14,27 +13,7 @@ namespace Jellyfin2Samsung.Helpers.Core
|
||||
try
|
||||
{
|
||||
string baseDir = AppContext.BaseDirectory;
|
||||
string relPath;
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
relPath = Path.Combine(AppSettings.EsbuildPath, "win-x64", "esbuild.exe");
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
relPath = Path.Combine(AppSettings.EsbuildPath, "linux-x64", "esbuild");
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
relPath = Path.Combine(AppSettings.EsbuildPath, "osx-universal", "esbuild");
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string fullPath = Path.Combine(baseDir, relPath);
|
||||
return File.Exists(fullPath) ? fullPath : null;
|
||||
return PlatformService.GetEsbuildPath(Path.Combine(baseDir, AppSettings.EsbuildPath));
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -53,22 +32,22 @@ namespace Jellyfin2Samsung.Helpers.Core
|
||||
string? esbuildPath = GetEsbuildPath();
|
||||
if (string.IsNullOrEmpty(esbuildPath))
|
||||
{
|
||||
Trace.WriteLine($"⚠ esbuild binary not found, skipping transpile for {relPathForLog ?? "unknown"}");
|
||||
Trace.WriteLine($"esbuild binary not found, skipping transpile for {relPathForLog ?? "unknown"}");
|
||||
return js;
|
||||
}
|
||||
|
||||
string tempRoot = Path.Combine(Path.GetTempPath(), "J2S_Esbuild");
|
||||
string tempRoot = Path.Combine(Path.GetTempPath(), Constants.Esbuild.TempFolderName);
|
||||
Directory.CreateDirectory(tempRoot);
|
||||
|
||||
string inputPath = Path.Combine(tempRoot, Guid.NewGuid().ToString("N") + ".js");
|
||||
string outputPath = Path.Combine(tempRoot, Guid.NewGuid().ToString("N") + ".js");
|
||||
string inputPath = Path.Combine(tempRoot, Guid.NewGuid().ToString("N") + Constants.FilePatterns.JsExtension);
|
||||
string outputPath = Path.Combine(tempRoot, Guid.NewGuid().ToString("N") + Constants.FilePatterns.JsExtension);
|
||||
|
||||
await File.WriteAllTextAsync(inputPath, js, Encoding.UTF8);
|
||||
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = esbuildPath,
|
||||
Arguments = $"\"{inputPath}\" --outfile=\"{outputPath}\" --target=es2015",
|
||||
Arguments = $"\"{inputPath}\" --outfile=\"{outputPath}\" --target={Constants.Esbuild.TargetEs2015}",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
@@ -85,7 +64,7 @@ namespace Jellyfin2Samsung.Helpers.Core
|
||||
|
||||
if (proc.ExitCode != 0 || !File.Exists(outputPath))
|
||||
{
|
||||
Trace.WriteLine($"⚠ esbuild failed for {relPathForLog ?? "unknown"} (exit {proc.ExitCode}): {stderr}");
|
||||
Trace.WriteLine($"esbuild failed for {relPathForLog ?? "unknown"} (exit {proc.ExitCode}): {stderr}");
|
||||
return js;
|
||||
}
|
||||
|
||||
@@ -101,14 +80,14 @@ namespace Jellyfin2Samsung.Helpers.Core
|
||||
// ignore cleanup errors
|
||||
}
|
||||
|
||||
Trace.WriteLine($" ✓ Transpiled {relPathForLog ?? "unknown"} via esbuild");
|
||||
Trace.WriteLine($"Transpiled {relPathForLog ?? "unknown"} via esbuild");
|
||||
return transpiled;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"⚠ esbuild transpile error for {relPathForLog ?? "unknown"}: {ex}");
|
||||
Trace.WriteLine($"esbuild transpile error for {relPathForLog ?? "unknown"}: {ex}");
|
||||
return js;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,9 +56,9 @@ namespace Jellyfin2Samsung.Helpers.Core
|
||||
var baseName = Path.GetFileNameWithoutExtension(originalPath);
|
||||
var extension = Path.GetExtension(originalPath);
|
||||
|
||||
var random = new Random();
|
||||
const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
var randomSuffix = new string(Enumerable.Range(0, 4).Select(_ => chars[random.Next(chars.Length)]).ToArray());
|
||||
var randomSuffix = new string(Enumerable.Range(0, 4)
|
||||
.Select(_ => Constants.CharacterSets.Alpha[Random.Shared.Next(Constants.CharacterSets.Alpha.Length)])
|
||||
.ToArray());
|
||||
|
||||
var newFileName = $"{baseName}{randomSuffix}{extension}";
|
||||
var newFilePath = Path.Combine(directory ?? Environment.CurrentDirectory, newFileName);
|
||||
@@ -76,11 +76,8 @@ namespace Jellyfin2Samsung.Helpers.Core
|
||||
public List<ExtensionEntry> ParseExtensions(string output)
|
||||
{
|
||||
var extensions = new List<ExtensionEntry>();
|
||||
var regex = new Regex(
|
||||
@"Index\s*:\s*(\d+)\s+Name\s*:\s*(.*?)\s+Repository\s*:\s*.*?\s+Id\s*:\s*.*?\s+Vendor\s*:\s*.*?\s+Description\s*:\s*.*?\s+Default\s*:\s*.*?\s+Activate\s*:\s*(true|false)",
|
||||
RegexOptions.Singleline);
|
||||
|
||||
foreach (Match match in regex.Matches(output))
|
||||
foreach (Match match in RegexPatterns.Extension.ExtensionEntry.Matches(output))
|
||||
{
|
||||
extensions.Add(new ExtensionEntry
|
||||
{
|
||||
@@ -112,13 +109,7 @@ namespace Jellyfin2Samsung.Helpers.Core
|
||||
using (var reader = new StreamReader(configEntry.Open(), Encoding.UTF8))
|
||||
configContent = await reader.ReadToEndAsync();
|
||||
|
||||
// Regex to extract the package prefix before .Jellyfin
|
||||
var regex = new Regex(
|
||||
@"<tizen:application\s+id=""(?<pkg>[A-Za-z0-9]+)\.Jellyfin""\s+package=""\k<pkg>""",
|
||||
RegexOptions.Multiline
|
||||
);
|
||||
|
||||
var match = regex.Match(configContent);
|
||||
var match = RegexPatterns.WgtConfig.TizenApplicationId.Match(configContent);
|
||||
return match.Success ? match.Groups["pkg"].Value : null;
|
||||
}
|
||||
public static async Task<bool> ModifyWgtPackageId(string wgtPath)
|
||||
@@ -149,10 +140,8 @@ namespace Jellyfin2Samsung.Helpers.Core
|
||||
configContent = await reader.ReadToEndAsync();
|
||||
|
||||
// Replace old package ID with the new one
|
||||
var regex = new Regex(
|
||||
$@"<tizen:application\s+id=""{oldPkg}\.Jellyfin""\s+package=""{oldPkg}""",
|
||||
RegexOptions.Multiline
|
||||
);
|
||||
var pattern = RegexPatterns.WgtConfig.CreatePackageIdReplacePattern(oldPkg);
|
||||
var regex = new Regex(pattern, RegexOptions.Multiline);
|
||||
|
||||
var newConfig = regex.Replace(configContent, m =>
|
||||
m.Value.Replace(oldPkg, newPkg)
|
||||
@@ -171,11 +160,9 @@ namespace Jellyfin2Samsung.Helpers.Core
|
||||
}
|
||||
private static string GenerateRandomString(int length)
|
||||
{
|
||||
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
var random = new Random();
|
||||
var sb = new StringBuilder(length);
|
||||
for (int i = 0; i < length; i++)
|
||||
sb.Append(chars[random.Next(chars.Length)]);
|
||||
sb.Append(Constants.CharacterSets.AlphaNumeric[Random.Shared.Next(Constants.CharacterSets.AlphaNumeric.Length)]);
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides centralized, pre-configured JsonSerializerOptions for consistent JSON serialization
|
||||
/// throughout the application. This eliminates inconsistent settings and improves performance
|
||||
/// by reusing options instances.
|
||||
/// </summary>
|
||||
public static class JsonSerializerOptionsProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Default options for general JSON serialization/deserialization.
|
||||
/// - Case-insensitive property matching
|
||||
/// - Camel case naming policy
|
||||
/// - Ignores null values when writing
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions Default { get; } = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Options for pretty-printed JSON output (e.g., config files, logs).
|
||||
/// Same as Default but with indentation enabled.
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions Indented { get; } = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Options optimized for GitHub API responses.
|
||||
/// Uses snake_case naming policy to match GitHub's JSON format.
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions GitHub { get; } = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Options for strict JSON parsing (no case-insensitive matching).
|
||||
/// Use when exact property name matching is required.
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions Strict { get; } = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Options for web/API responses with standard web conventions.
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions Web { get; } = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
}
|
||||
}
|
||||
195
Jellyfin2Samsung-CrossOS/Helpers/Core/PlatformService.cs
Normal file
195
Jellyfin2Samsung-CrossOS/Helpers/Core/PlatformService.cs
Normal file
@@ -0,0 +1,195 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides centralized platform detection and platform-specific operations.
|
||||
/// Eliminates duplicate OperatingSystem.* and RuntimeInformation.* checks throughout the codebase.
|
||||
/// </summary>
|
||||
public static class PlatformService
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the current operating system platform.
|
||||
/// </summary>
|
||||
public enum Platform
|
||||
{
|
||||
Windows,
|
||||
Linux,
|
||||
MacOS,
|
||||
Unknown
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current platform.
|
||||
/// </summary>
|
||||
public static Platform CurrentPlatform
|
||||
{
|
||||
get
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
return Platform.Windows;
|
||||
if (OperatingSystem.IsLinux())
|
||||
return Platform.Linux;
|
||||
if (OperatingSystem.IsMacOS())
|
||||
return Platform.MacOS;
|
||||
return Platform.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the current platform is Windows.
|
||||
/// </summary>
|
||||
public static bool IsWindows => OperatingSystem.IsWindows();
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the current platform is Linux.
|
||||
/// </summary>
|
||||
public static bool IsLinux => OperatingSystem.IsLinux();
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the current platform is macOS.
|
||||
/// </summary>
|
||||
public static bool IsMacOS => OperatingSystem.IsMacOS();
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the current platform is Unix-like (Linux or macOS).
|
||||
/// </summary>
|
||||
public static bool IsUnixLike => IsLinux || IsMacOS;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the appropriate TizenSdb search pattern for the current platform.
|
||||
/// </summary>
|
||||
/// <returns>The file search pattern for TizenSdb binary.</returns>
|
||||
/// <exception cref="PlatformNotSupportedException">Thrown when the platform is not supported.</exception>
|
||||
public static string GetTizenSdbSearchPattern()
|
||||
{
|
||||
return CurrentPlatform switch
|
||||
{
|
||||
Platform.Windows => Constants.PlatformBinaries.TizenSdbWindowsPattern,
|
||||
Platform.Linux => Constants.PlatformBinaries.TizenSdbLinuxPattern,
|
||||
Platform.MacOS => Constants.PlatformBinaries.TizenSdbMacOsPattern,
|
||||
_ => throw new PlatformNotSupportedException("Unsupported operating system")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the TizenSdb file name with version for the current platform.
|
||||
/// </summary>
|
||||
/// <param name="version">The version string to include in the file name.</param>
|
||||
/// <returns>The platform-specific file name.</returns>
|
||||
/// <exception cref="PlatformNotSupportedException">Thrown when the platform is not supported.</exception>
|
||||
public static string GetTizenSdbFileName(string version)
|
||||
{
|
||||
return CurrentPlatform switch
|
||||
{
|
||||
Platform.Windows => $"TizenSdb_{version}{Constants.PlatformBinaries.WindowsExtension}",
|
||||
Platform.Linux => $"TizenSdb_{version}{Constants.PlatformBinaries.LinuxSuffix}",
|
||||
Platform.MacOS => $"TizenSdb_{version}{Constants.PlatformBinaries.MacOsSuffix}",
|
||||
_ => throw new PlatformNotSupportedException("Unsupported operating system")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the platform identifier for matching GitHub release assets.
|
||||
/// </summary>
|
||||
/// <returns>The platform identifier string used in asset file names.</returns>
|
||||
/// <exception cref="PlatformNotSupportedException">Thrown when the platform is not supported.</exception>
|
||||
public static string GetAssetPlatformIdentifier()
|
||||
{
|
||||
return CurrentPlatform switch
|
||||
{
|
||||
Platform.Windows => "exe",
|
||||
Platform.Linux => "linux",
|
||||
Platform.MacOS => "macos",
|
||||
_ => throw new PlatformNotSupportedException("Unsupported operating system")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the esbuild binary path relative to the esbuild directory.
|
||||
/// </summary>
|
||||
/// <param name="esbuildBasePath">The base path to the esbuild directory.</param>
|
||||
/// <returns>The full path to the esbuild binary, or null if not supported.</returns>
|
||||
public static string? GetEsbuildPath(string esbuildBasePath)
|
||||
{
|
||||
string relativePath = CurrentPlatform switch
|
||||
{
|
||||
Platform.Windows => Path.Combine(
|
||||
Constants.PlatformBinaries.EsbuildWindows,
|
||||
Constants.PlatformBinaries.EsbuildExecutableWindows),
|
||||
Platform.Linux => Path.Combine(
|
||||
Constants.PlatformBinaries.EsbuildLinux,
|
||||
Constants.PlatformBinaries.EsbuildExecutable),
|
||||
Platform.MacOS => Path.Combine(
|
||||
Constants.PlatformBinaries.EsbuildMacOs,
|
||||
Constants.PlatformBinaries.EsbuildExecutable),
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (relativePath == null)
|
||||
return null;
|
||||
|
||||
string fullPath = Path.Combine(esbuildBasePath, relativePath);
|
||||
return File.Exists(fullPath) ? fullPath : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ARP command arguments for the current platform.
|
||||
/// </summary>
|
||||
/// <param name="ipAddress">The IP address to query.</param>
|
||||
/// <returns>The ARP command arguments.</returns>
|
||||
public static string GetArpArguments(string ipAddress)
|
||||
{
|
||||
return IsWindows ? $"-a {ipAddress}" : $"-n {ipAddress}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the appropriate X509KeyStorageFlags for the current platform.
|
||||
/// </summary>
|
||||
/// <returns>The platform-appropriate key storage flags.</returns>
|
||||
public static X509KeyStorageFlags GetX509KeyStorageFlags()
|
||||
{
|
||||
return IsWindows
|
||||
? X509KeyStorageFlags.EphemeralKeySet
|
||||
: X509KeyStorageFlags.PersistKeySet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets firewall configuration help text for the current platform.
|
||||
/// </summary>
|
||||
/// <param name="port">The port number to include in instructions.</param>
|
||||
/// <returns>Platform-specific firewall configuration instructions.</returns>
|
||||
public static string GetFirewallHelpText(int port)
|
||||
{
|
||||
return CurrentPlatform switch
|
||||
{
|
||||
Platform.Windows =>
|
||||
$"Windows:\n" +
|
||||
$" netstat -ano | findstr {port}\n\n" +
|
||||
$" New-NetFirewallRule -DisplayName \"Jellyfin2Samsung Logs\" \\\n" +
|
||||
$" -Direction Inbound -Protocol TCP -LocalPort {port} -Action Allow\n",
|
||||
|
||||
Platform.Linux =>
|
||||
$"Linux:\n sudo ufw allow {port}/tcp\n sudo ufw reload\n",
|
||||
|
||||
Platform.MacOS =>
|
||||
"macOS:\n" +
|
||||
" System Settings -> Network -> Firewall -> Options\n" +
|
||||
" Allow incoming connections for Jellyfin2Samsung\n",
|
||||
|
||||
_ => "Ensure your firewall allows inbound TCP connections.\n"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the file needs executable permissions set (Unix-like systems only).
|
||||
/// </summary>
|
||||
/// <returns>True if chmod is needed, false otherwise.</returns>
|
||||
public static bool RequiresExecutablePermissions()
|
||||
{
|
||||
return IsUnixLike;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,7 +122,7 @@ namespace Jellyfin2Samsung.Helpers.Core
|
||||
|
||||
public async Task MakeExecutableAsync(string filePath)
|
||||
{
|
||||
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||
if (PlatformService.RequiresExecutablePermissions())
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
318
Jellyfin2Samsung-CrossOS/Helpers/Core/RegexPatterns.cs
Normal file
318
Jellyfin2Samsung-CrossOS/Helpers/Core/RegexPatterns.cs
Normal file
@@ -0,0 +1,318 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Centralized regex patterns used throughout the application.
|
||||
/// Pre-compiled patterns improve performance for frequently used expressions.
|
||||
/// </summary>
|
||||
public static partial class RegexPatterns
|
||||
{
|
||||
/// <summary>
|
||||
/// Patterns for version parsing and extraction.
|
||||
/// </summary>
|
||||
public static class Version
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern to extract version from a file name (e.g., "TizenSdb_v1.0.0.exe" -> "v1.0.0").
|
||||
/// </summary>
|
||||
public const string FileNameVersionPattern = @"_([v]?\d+\.\d+\.\d+)";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for file name version extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex FileNameVersion = new(FileNameVersionPattern, RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patterns for Tizen device capability parsing.
|
||||
/// </summary>
|
||||
public static class TizenCapability
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern to extract platform version from Tizen capability output.
|
||||
/// </summary>
|
||||
public const string PlatformVersionPattern = @"platform_version:\s*([\d.]+)";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to extract SDK tool path from Tizen capability output.
|
||||
/// </summary>
|
||||
public const string SdkToolPathPattern = @"sdk_toolpath:\s*([^\r\n]+)";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to detect app uninstall test failure in Tizen diagnose output.
|
||||
/// </summary>
|
||||
public const string AppUninstallFailedPattern = @"Testing '0 vd_appuninstall test':\s*FAILED";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for platform version extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex PlatformVersion = new(PlatformVersionPattern, RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for SDK tool path extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex SdkToolPath = new(SdkToolPathPattern, RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for app uninstall test failure detection.
|
||||
/// </summary>
|
||||
public static readonly Regex AppUninstallFailed = new(
|
||||
AppUninstallFailedPattern,
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patterns for Tizen app information parsing.
|
||||
/// </summary>
|
||||
public static class TizenApp
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern template to extract app block by title from Tizen apps output.
|
||||
/// Use string.Format or interpolation with the app title.
|
||||
/// </summary>
|
||||
public const string AppBlockByTitleTemplate = @"(^\s*-+app_title\s*=\s*{0}.*?)(?=^\s*-+app_title|\Z)";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to extract Tizen app ID from an app block.
|
||||
/// </summary>
|
||||
public const string AppTizenIdPattern = @"app_tizen_id\s*=\s*([A-Za-z0-9._]+)";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to extract Tizen app ID with delimiter.
|
||||
/// </summary>
|
||||
public const string AppTizenIdWithDelimiterPattern = @"app_tizen_id\s*=\s*([A-Za-z0-9._-]+?)(?=-{3,})";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for app Tizen ID extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex AppTizenId = new(
|
||||
AppTizenIdPattern,
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for app Tizen ID with delimiter extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex AppTizenIdWithDelimiter = new(
|
||||
AppTizenIdWithDelimiterPattern,
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a regex to find an app block by title.
|
||||
/// </summary>
|
||||
/// <param name="appTitle">The app title to search for.</param>
|
||||
/// <returns>A regex instance for matching the app block.</returns>
|
||||
public static Regex CreateAppBlockByTitleRegex(string appTitle)
|
||||
{
|
||||
var escapedTitle = Regex.Escape(appTitle);
|
||||
var pattern = string.Format(AppBlockByTitleTemplate, escapedTitle);
|
||||
return new Regex(
|
||||
pattern,
|
||||
RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Multiline);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patterns for WGT package config.xml parsing.
|
||||
/// </summary>
|
||||
public static class WgtConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern to extract package ID from Tizen application element in config.xml.
|
||||
/// </summary>
|
||||
public const string TizenApplicationIdPattern =
|
||||
@"<tizen:application\s+id=""(?<pkg>[A-Za-z0-9]+)\.Jellyfin""\s+package=""\k<pkg>""";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for Tizen application ID extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex TizenApplicationId = new(
|
||||
TizenApplicationIdPattern,
|
||||
RegexOptions.Compiled | RegexOptions.Multiline);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a pattern to match and replace a specific package ID in config.xml.
|
||||
/// </summary>
|
||||
/// <param name="packageId">The package ID to match.</param>
|
||||
/// <returns>The pattern string.</returns>
|
||||
public static string CreatePackageIdReplacePattern(string packageId)
|
||||
{
|
||||
return $@"<tizen:application\s+id=""{packageId}\.Jellyfin""\s+package=""{packageId}""";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patterns for HTML parsing and manipulation.
|
||||
/// </summary>
|
||||
public static class Html
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern to extract href from link elements.
|
||||
/// </summary>
|
||||
public const string LinkHrefPattern = @"<link[^>]+href=[""']([^""']+)[""'][^>]*>";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to extract src from script elements.
|
||||
/// </summary>
|
||||
public const string ScriptSrcPattern = @"<script[^>]+src=[""']([^""']+)[""'][^>]*>[\s\S]*?<\/script>";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for link href extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex LinkHref = new(
|
||||
LinkHrefPattern,
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for script src extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex ScriptSrc = new(
|
||||
ScriptSrcPattern,
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patterns for network and MAC address parsing.
|
||||
/// </summary>
|
||||
public static class Network
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern to extract MAC address from ARP output.
|
||||
/// </summary>
|
||||
public const string MacAddressPattern = @"([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for MAC address extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex MacAddress = new(MacAddressPattern, RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patterns for command line argument parsing.
|
||||
/// </summary>
|
||||
public static class CommandLine
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern to parse command line arguments (handles quoted strings).
|
||||
/// </summary>
|
||||
public const string ArgumentsPattern = @"[\""].+?[\""]|[^ ]+";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for argument parsing.
|
||||
/// </summary>
|
||||
public static readonly Regex Arguments = new(ArgumentsPattern, RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patterns for build info markdown parsing.
|
||||
/// </summary>
|
||||
public static class BuildInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern to extract versions table from markdown.
|
||||
/// </summary>
|
||||
public const string VersionsTablePattern = @"## Versions\s*\n(?<table>(\|[^\n]+\n)+)";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to extract applications table from markdown.
|
||||
/// </summary>
|
||||
public const string ApplicationsTablePattern =
|
||||
@"\|\s*🧩 Application\s*\|\s*📝 Description\s*\|\s*🔗 Repository\s*\|\s*\n(?<table>(\|[^\n]+\n)+)";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to extract table rows with 2 columns.
|
||||
/// </summary>
|
||||
public const string TableRow2ColumnsPattern = @"^\|([^|]+)\|([^|]+)\|";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to extract table rows with 3 columns.
|
||||
/// </summary>
|
||||
public const string TableRow3ColumnsPattern = @"^\|([^|]+)\|([^|]+)\|([^|]+)\|";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to remove markdown bold formatting.
|
||||
/// </summary>
|
||||
public const string MarkdownBoldPattern = @"\*\*(.*?)\*\*";
|
||||
|
||||
/// <summary>
|
||||
/// Pattern to remove emoji characters.
|
||||
/// </summary>
|
||||
public const string EmojiRangePattern = @"[\u2600-\u27BF]";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for versions table extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex VersionsTable = new(
|
||||
VersionsTablePattern,
|
||||
RegexOptions.Compiled | RegexOptions.Multiline);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for applications table extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex ApplicationsTable = new(
|
||||
ApplicationsTablePattern,
|
||||
RegexOptions.Compiled | RegexOptions.Multiline);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for 2-column table rows.
|
||||
/// </summary>
|
||||
public static readonly Regex TableRow2Columns = new(
|
||||
TableRow2ColumnsPattern,
|
||||
RegexOptions.Compiled | RegexOptions.Multiline);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for 3-column table rows.
|
||||
/// </summary>
|
||||
public static readonly Regex TableRow3Columns = new(
|
||||
TableRow3ColumnsPattern,
|
||||
RegexOptions.Compiled | RegexOptions.Multiline);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for markdown bold removal.
|
||||
/// </summary>
|
||||
public static readonly Regex MarkdownBold = new(MarkdownBoldPattern, RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for emoji removal.
|
||||
/// </summary>
|
||||
public static readonly Regex EmojiRange = new(EmojiRangePattern, RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patterns for plugin configuration parsing.
|
||||
/// </summary>
|
||||
public static class PluginConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern to extract defaultSkin value from JSON config.
|
||||
/// </summary>
|
||||
public const string DefaultSkinPattern = @"""defaultSkin""\s*:\s*""([^""]+)""";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for defaultSkin extraction.
|
||||
/// </summary>
|
||||
public static readonly Regex DefaultSkin = new(
|
||||
DefaultSkinPattern,
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patterns for extension parsing in Tizen package manager output.
|
||||
/// </summary>
|
||||
public static class Extension
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern to parse extension entries from package manager output.
|
||||
/// </summary>
|
||||
public const string ExtensionEntryPattern =
|
||||
@"Index\s*:\s*(\d+)\s+Name\s*:\s*(.*?)\s+Repository\s*:\s*.*?\s+Id\s*:\s*.*?\s+Vendor\s*:\s*.*?\s+Description\s*:\s*.*?\s+Default\s*:\s*.*?\s+Activate\s*:\s*(true|false)";
|
||||
|
||||
/// <summary>
|
||||
/// Pre-compiled regex for extension entry parsing.
|
||||
/// </summary>
|
||||
public static readonly Regex ExtensionEntry = new(
|
||||
ExtensionEntryPattern,
|
||||
RegexOptions.Compiled | RegexOptions.Singleline);
|
||||
}
|
||||
}
|
||||
}
|
||||
94
Jellyfin2Samsung-CrossOS/Helpers/Core/UrlHelper.cs
Normal file
94
Jellyfin2Samsung-CrossOS/Helpers/Core/UrlHelper.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
|
||||
namespace Jellyfin2Samsung.Helpers.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides URL manipulation utilities.
|
||||
/// Centralizes URL normalization to eliminate duplicate TrimEnd('/') calls across the codebase.
|
||||
/// </summary>
|
||||
public static class UrlHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Normalizes a server URL by removing trailing slashes.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to normalize.</param>
|
||||
/// <returns>The normalized URL without trailing slashes, or empty string if null.</returns>
|
||||
public static string NormalizeServerUrl(string? url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
return string.Empty;
|
||||
|
||||
return url.TrimEnd('/');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines a base URL with a path segment, ensuring proper slash handling.
|
||||
/// </summary>
|
||||
/// <param name="baseUrl">The base URL (trailing slash will be removed).</param>
|
||||
/// <param name="path">The path to append (leading slash will be added if missing).</param>
|
||||
/// <returns>The combined URL.</returns>
|
||||
public static string CombineUrl(string? baseUrl, string? path)
|
||||
{
|
||||
var normalizedBase = NormalizeServerUrl(baseUrl);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return normalizedBase;
|
||||
|
||||
var normalizedPath = path.TrimStart('/');
|
||||
|
||||
return string.IsNullOrEmpty(normalizedBase)
|
||||
? normalizedPath
|
||||
: $"{normalizedBase}/{normalizedPath}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an absolute URI from a base server URL and a relative or absolute path.
|
||||
/// </summary>
|
||||
/// <param name="serverUrl">The base server URL.</param>
|
||||
/// <param name="relativeOrAbsolutePath">A relative path or absolute URL.</param>
|
||||
/// <returns>The absolute URI.</returns>
|
||||
public static Uri GetAbsoluteUri(string serverUrl, string relativeOrAbsolutePath)
|
||||
{
|
||||
if (Uri.IsWellFormedUriString(relativeOrAbsolutePath, UriKind.Absolute))
|
||||
return new Uri(relativeOrAbsolutePath);
|
||||
|
||||
var baseUri = new Uri(NormalizeServerUrl(serverUrl) + "/");
|
||||
return new Uri(baseUri, relativeOrAbsolutePath.TrimStart('/'));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates if a string is a valid HTTP or HTTPS URL.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to validate.</param>
|
||||
/// <returns>True if the URL is valid, false otherwise.</returns>
|
||||
public static bool IsValidHttpUrl(string? url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
return false;
|
||||
|
||||
return Uri.TryCreate(url, UriKind.Absolute, out var uriResult)
|
||||
&& (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the file name from a URL path.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to extract the file name from.</param>
|
||||
/// <returns>The file name, or empty string if not found.</returns>
|
||||
public static string GetFileNameFromUrl(string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
var uri = new Uri(url);
|
||||
return System.IO.Path.GetFileName(uri.LocalPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ namespace Jellyfin2Samsung.Helpers.Jellyfin.Patches
|
||||
config["servers"] = servers;
|
||||
}
|
||||
|
||||
var serverUrl = AppSettings.Default.JellyfinFullUrl.TrimEnd('/');
|
||||
var serverUrl = UrlHelper.NormalizeServerUrl(AppSettings.Default.JellyfinFullUrl);
|
||||
|
||||
// Avoid duplicates
|
||||
if (!servers.Any(s => s?.GetValue<string>() == serverUrl))
|
||||
@@ -91,7 +91,7 @@ namespace Jellyfin2Samsung.Helpers.Jellyfin.Patches
|
||||
{
|
||||
var accessToken = AppSettings.Default.JellyfinAccessToken;
|
||||
var userId = AppSettings.Default.JellyfinUserId;
|
||||
var serverUrl = AppSettings.Default.JellyfinFullUrl.TrimEnd('/');
|
||||
var serverUrl = UrlHelper.NormalizeServerUrl(AppSettings.Default.JellyfinFullUrl);
|
||||
var serverId = AppSettings.Default.JellyfinServerId;
|
||||
var localAddress = AppSettings.Default.JellyfinServerLocalAddress;
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ namespace Jellyfin2Samsung.Helpers.Jellyfin.Plugins
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = serverUrl.TrimEnd('/') + "/web/index.html";
|
||||
var url = UrlHelper.CombineUrl(serverUrl, "/web/index.html");
|
||||
Trace.WriteLine($"▶ Fetching server index.html: {url}");
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
|
||||
@@ -219,10 +219,7 @@ namespace Jellyfin2Samsung.Helpers.Jellyfin.Plugins
|
||||
}
|
||||
private static Uri GetAbsoluteUri(string serverUrl, string relOrAbs)
|
||||
{
|
||||
if (Uri.IsWellFormedUriString(relOrAbs, UriKind.Absolute))
|
||||
return new Uri(relOrAbs);
|
||||
|
||||
return new Uri(new Uri(serverUrl.TrimEnd('/') + "/"), relOrAbs.TrimStart('/'));
|
||||
return UrlHelper.GetAbsoluteUri(serverUrl, relOrAbs);
|
||||
}
|
||||
private void AppendScriptOnce(StringBuilder js, string scriptTag, string src)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Jellyfin2Samsung.Helpers.Jellyfin.Plugins;
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Helpers.Jellyfin.Plugins;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -243,10 +244,7 @@ namespace Jellyfin2Samsung.Helpers.Jellyfin.Plugins.KefinTweaks
|
||||
return null;
|
||||
|
||||
var json = File.ReadAllText(configPath);
|
||||
var match = Regex.Match(
|
||||
json,
|
||||
@"""defaultSkin""\s*:\s*""([^""]+)""",
|
||||
RegexOptions.IgnoreCase);
|
||||
var match = RegexPatterns.PluginConfig.DefaultSkin.Match(json);
|
||||
|
||||
return match.Success ? match.Groups[1].Value : null;
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ namespace Jellyfin2Samsung.Helpers.Jellyfin.Plugins
|
||||
{
|
||||
try
|
||||
{
|
||||
string url = serverUrl.TrimEnd('/') + rel;
|
||||
string url = UrlHelper.CombineUrl(serverUrl, rel);
|
||||
Trace.WriteLine($" → Fetch: {url}");
|
||||
|
||||
string js = await _httpClient.GetStringAsync(url);
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Jellyfin2Samsung.Models
|
||||
{
|
||||
|
||||
public class GitHubRelease
|
||||
{
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
[JsonPropertyName("url")]
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("tag_name")]
|
||||
public string TagName { get; set; }
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("published_at")]
|
||||
public string PublishedAt { get; set; }
|
||||
[JsonPropertyName("tag_name")]
|
||||
public string TagName { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("assets")]
|
||||
public List<Asset> Assets { get; set; } = new List<Asset>();
|
||||
[JsonPropertyName("published_at")]
|
||||
public string PublishedAt { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("assets")]
|
||||
public List<Asset> Assets { get; set; } = new();
|
||||
|
||||
[JsonIgnore]
|
||||
public string? PrimaryDownloadUrl => Assets?.FirstOrDefault()?.DownloadUrl;
|
||||
|
||||
public string PrimaryDownloadUrl => Assets?.FirstOrDefault()?.DownloadUrl;
|
||||
[JsonConstructor]
|
||||
public GitHubRelease()
|
||||
{
|
||||
}
|
||||
@@ -31,30 +31,33 @@ namespace Jellyfin2Samsung.Models
|
||||
|
||||
public class Asset
|
||||
{
|
||||
[JsonProperty("name")]
|
||||
public string FileName { get; set; }
|
||||
[JsonPropertyName("name")]
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("browser_download_url")]
|
||||
public string DownloadUrl { get; set; }
|
||||
[JsonPropertyName("browser_download_url")]
|
||||
public string DownloadUrl { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("size")]
|
||||
[JsonPropertyName("size")]
|
||||
public long Size { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string DisplayText => $"{FileName} ({FormatFileSize(Size)})";
|
||||
|
||||
private string FormatFileSize(long bytes)
|
||||
private static string FormatFileSize(long bytes)
|
||||
{
|
||||
string[] sizes = { "B", "KB", "MB", "GB" };
|
||||
string[] sizes = ["B", "KB", "MB", "GB"];
|
||||
int order = 0;
|
||||
double len = bytes;
|
||||
|
||||
while (len >= 1024 && order < sizes.Length - 1)
|
||||
{
|
||||
order++;
|
||||
len /= 1024;
|
||||
}
|
||||
|
||||
return $"{len:0.##} {sizes[order]}";
|
||||
}
|
||||
[JsonConstructor]
|
||||
|
||||
public Asset()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Jellyfin2Samsung.Helpers;
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
@@ -19,8 +20,6 @@ namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
private readonly ITizenInstallerService _tizenInstaller;
|
||||
private static readonly HttpClient _httpClient = new HttpClient();
|
||||
private const int tvPort = 26101;
|
||||
private const int scanTimeoutMs = 1000;
|
||||
|
||||
public NetworkService(ITizenInstallerService tizenInstaller)
|
||||
{
|
||||
@@ -36,13 +35,13 @@ namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource(scanTimeoutMs);
|
||||
using var cts = new CancellationTokenSource(Constants.Defaults.NetworkScanTimeoutMs);
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
cts.Token, cancellationToken);
|
||||
|
||||
if (await IsPortOpenAsync(ip, tvPort, linkedCts.Token))
|
||||
if (await IsPortOpenAsync(ip, Constants.Ports.TizenDevPort, linkedCts.Token))
|
||||
{
|
||||
if (await IsPortOpenAsync(ip, 8001, linkedCts.Token))
|
||||
if (await IsPortOpenAsync(ip, Constants.Ports.SamsungTvApiPort, linkedCts.Token))
|
||||
{
|
||||
var manufacturer = await GetManufacturerFromIp(ip);
|
||||
var device = new NetworkDevice
|
||||
@@ -87,10 +86,10 @@ namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource(scanTimeoutMs);
|
||||
using var cts = new CancellationTokenSource(Constants.Defaults.NetworkScanTimeoutMs);
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
cts.Token, cancellationToken);
|
||||
if (await IsPortOpenAsync(ip, tvPort, linkedCts.Token))
|
||||
if (await IsPortOpenAsync(ip, Constants.Ports.TizenDevPort, linkedCts.Token))
|
||||
{
|
||||
var manufacturer = await GetManufacturerFromIp(ip);
|
||||
var device = new NetworkDevice
|
||||
@@ -111,7 +110,7 @@ namespace Jellyfin2Samsung.Services
|
||||
catch { /* Ignore scan failures */ }
|
||||
})));
|
||||
|
||||
Trace.WriteLine($"Scan complete! Found {foundDevices.Count} devices with port {tvPort} open.");
|
||||
Trace.WriteLine($"Scan complete! Found {foundDevices.Count} devices with port {Constants.Ports.TizenDevPort} open.");
|
||||
return foundDevices;
|
||||
}
|
||||
public IEnumerable<IPAddress> GetRelevantLocalIPs(bool virtualScan = false)
|
||||
@@ -186,7 +185,7 @@ namespace Jellyfin2Samsung.Services
|
||||
|
||||
private static async Task<string?> GetMacAddressFromIp(string ipAddress)
|
||||
{
|
||||
string arpArgs = OperatingSystem.IsWindows() ? $"-a {ipAddress}" : $"-n {ipAddress}";
|
||||
string arpArgs = PlatformService.GetArpArguments(ipAddress);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -206,7 +205,7 @@ namespace Jellyfin2Samsung.Services
|
||||
string output = await process.StandardOutput.ReadToEndAsync();
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
var match = Regex.Match(output, @"([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})");
|
||||
var match = RegexPatterns.Network.MacAddress.Match(output);
|
||||
return match.Success ? match.Value : null;
|
||||
}
|
||||
catch
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
using Avalonia.Controls;
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
@@ -17,13 +16,12 @@ namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
public class SamsungLoginService
|
||||
{
|
||||
private IWebHost _callbackServer;
|
||||
private const string LoopbackHost = "localhost";
|
||||
private const int FixedPort = 4794;
|
||||
private IWebHost? _callbackServer;
|
||||
|
||||
private string _callbackUrl => $"http://{LoopbackHost}:{FixedPort}/signin/callback";
|
||||
private string CallbackUrl =>
|
||||
$"http://{Constants.Samsung.LoopbackHost}:{Constants.Ports.SamsungLoginCallbackPort}{Constants.Samsung.CallbackPath}";
|
||||
|
||||
public Action<SamsungAuth> CallbackReceived;
|
||||
public Action<SamsungAuth>? CallbackReceived;
|
||||
|
||||
public static Task<SamsungAuth> PerformSamsungLoginAsync()
|
||||
{
|
||||
@@ -50,15 +48,15 @@ namespace Jellyfin2Samsung.Services
|
||||
await service.StartCallbackServer();
|
||||
|
||||
string loginUrl =
|
||||
$"https://account.samsung.com/accounts/be1dce529476c1a6d407c4c7578c31bd/signInGate" +
|
||||
$"?locale=&clientId=v285zxnl3h" +
|
||||
$"&redirect_uri={HttpUtility.UrlEncode(service._callbackUrl)}" +
|
||||
$"&state=accountcheckdogeneratedstatetext" +
|
||||
$"&tokenType=TOKEN";
|
||||
$"{Constants.Samsung.SignInGateUrl}" +
|
||||
$"?locale=&clientId={Constants.Samsung.OAuthClientId}" +
|
||||
$"&redirect_uri={HttpUtility.UrlEncode(service.CallbackUrl)}" +
|
||||
$"&state={Constants.Samsung.OAuthState}" +
|
||||
$"&tokenType={Constants.Samsung.TokenType}";
|
||||
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = loginUrl,
|
||||
UseShellExecute = true
|
||||
@@ -84,17 +82,18 @@ namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
_callbackServer = new WebHostBuilder()
|
||||
.UseKestrel()
|
||||
.UseUrls($"http://{LoopbackHost}:{FixedPort}")
|
||||
.UseUrls($"http://{Constants.Samsung.LoopbackHost}:{Constants.Ports.SamsungLoginCallbackPort}")
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Run(async context =>
|
||||
{
|
||||
if (context.Request.Path == "/signin/callback" && context.Request.Method == "POST")
|
||||
if (context.Request.Path == Constants.Samsung.CallbackPath &&
|
||||
context.Request.Method == "POST")
|
||||
{
|
||||
string body = await new StreamReader(context.Request.Body).ReadToEndAsync();
|
||||
|
||||
string state = null;
|
||||
string codeEncoded = null;
|
||||
string? state = null;
|
||||
string? codeEncoded = null;
|
||||
|
||||
var parts = body.Split('&', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
@@ -119,7 +118,10 @@ namespace Jellyfin2Samsung.Services
|
||||
|
||||
try
|
||||
{
|
||||
var auth = JsonConvert.DeserializeObject<SamsungAuth>(codeEncoded);
|
||||
var auth = JsonSerializer.Deserialize<SamsungAuth>(
|
||||
codeEncoded,
|
||||
JsonSerializerOptionsProvider.Default);
|
||||
|
||||
if (auth != null)
|
||||
{
|
||||
auth.state = state;
|
||||
@@ -134,7 +136,7 @@ namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
await context.Response.WriteAsync(
|
||||
$"[CallbackServer] JSON parse error: {ex}\n\nDecoded JSON:\n{codeEncoded}");
|
||||
$"[CallbackServer] JSON parse error: {ex.Message}\n\nDecoded JSON:\n{codeEncoded}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -151,8 +153,8 @@ namespace Jellyfin2Samsung.Services
|
||||
|
||||
await _callbackServer.StartAsync();
|
||||
|
||||
System.Diagnostics.Trace.WriteLine(
|
||||
$"[SamsungLoginService] Bound to http://{LoopbackHost}:{FixedPort}");
|
||||
Trace.WriteLine(
|
||||
$"[SamsungLoginService] Bound to http://{Constants.Samsung.LoopbackHost}:{Constants.Ports.SamsungLoginCallbackPort}");
|
||||
}
|
||||
|
||||
public async Task StopCallbackServer()
|
||||
@@ -165,4 +167,4 @@ namespace Jellyfin2Samsung.Services
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Jellyfin2Samsung.Extensions;
|
||||
using Jellyfin2Samsung.Helpers;
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Helpers.Tizen.Certificate;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Org.BouncyCastle.Asn1;
|
||||
@@ -291,7 +292,7 @@ namespace Jellyfin2Samsung.Services
|
||||
}
|
||||
|
||||
// --- Optional sanity check ---
|
||||
if (OperatingSystem.IsWindows())
|
||||
if (PlatformService.IsWindows)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -319,10 +320,7 @@ namespace Jellyfin2Samsung.Services
|
||||
}
|
||||
private static X509KeyStorageFlags GetX509KeyStorageFlags()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
return X509KeyStorageFlags.EphemeralKeySet;
|
||||
|
||||
return X509KeyStorageFlags.PersistKeySet;
|
||||
return PlatformService.GetX509KeyStorageFlags();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
using Fleck;
|
||||
using Fleck;
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -84,7 +84,7 @@ namespace Jellyfin2Samsung.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), token);
|
||||
await Task.Delay(TimeSpan.FromSeconds(Constants.Defaults.WebSocketMonitorDelaySeconds), token);
|
||||
|
||||
if (_connection != null || token.IsCancellationRequested)
|
||||
return;
|
||||
@@ -94,37 +94,9 @@ namespace Jellyfin2Samsung.Services
|
||||
onMessage(
|
||||
"[No incoming connections detected]\n" +
|
||||
"If the TV cannot connect, your firewall may be blocking this port.\n\n" +
|
||||
GetFirewallHelpText(port));
|
||||
PlatformService.GetFirewallHelpText(port));
|
||||
}
|
||||
catch (TaskCanceledException) { }
|
||||
}
|
||||
|
||||
private static string GetFirewallHelpText(int port)
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return
|
||||
"Windows:\n" +
|
||||
$" netstat -ano | findstr {port}\n\n" +
|
||||
$" New-NetFirewallRule -DisplayName \"Jellyfin2Samsung Logs\" \\\n" +
|
||||
$" -Direction Inbound -Protocol TCP -LocalPort {port} -Action Allow\n";
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return
|
||||
$"Linux:\n sudo ufw allow {port}/tcp\n sudo ufw reload\n";
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
return
|
||||
"macOS:\n" +
|
||||
" System Settings → Network → Firewall → Options\n" +
|
||||
" Allow incoming connections for Jellyfin2Samsung\n";
|
||||
}
|
||||
|
||||
return "Ensure your firewall allows inbound TCP connections.\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Jellyfin2Samsung.Helpers;
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Models;
|
||||
|
||||
namespace Jellyfin2Samsung.ViewModels
|
||||
@@ -64,24 +65,20 @@ namespace Jellyfin2Samsung.ViewModels
|
||||
// Remove markdown formatting like **bold**, emoji, etc.
|
||||
private static string CleanText(string input)
|
||||
{
|
||||
var text = Regex.Replace(input, @"\*\*(.*?)\*\*", "$1");
|
||||
text = Regex.Replace(text, @"[\u2600-\u27BF]", ""); // emoji range
|
||||
var text = RegexPatterns.BuildInfo.MarkdownBold.Replace(input, "$1");
|
||||
text = RegexPatterns.BuildInfo.EmojiRange.Replace(text, "");
|
||||
return text.Trim();
|
||||
}
|
||||
|
||||
private void ParseVersionsTable(string md, ObservableCollection<BuildVersion> target)
|
||||
{
|
||||
var match = Regex.Match(md,
|
||||
@"## Versions\s*\n(?<table>(\|[^\n]+\n)+)",
|
||||
RegexOptions.Multiline);
|
||||
var match = RegexPatterns.BuildInfo.VersionsTable.Match(md);
|
||||
|
||||
if (!match.Success) return;
|
||||
|
||||
var table = match.Groups["table"].Value;
|
||||
|
||||
var rows = Regex.Matches(table,
|
||||
@"^\|([^|]+)\|([^|]+)\|",
|
||||
RegexOptions.Multiline);
|
||||
var rows = RegexPatterns.BuildInfo.TableRow2Columns.Matches(table);
|
||||
|
||||
bool headerSkipped = false;
|
||||
|
||||
@@ -110,17 +107,13 @@ namespace Jellyfin2Samsung.ViewModels
|
||||
|
||||
private void ParseApplicationsTable(string md, ObservableCollection<BuildVersion> target)
|
||||
{
|
||||
var match = Regex.Match(md,
|
||||
@"\|\s*🧩 Application\s*\|\s*📝 Description\s*\|\s*🔗 Repository\s*\|\s*\n(?<table>(\|[^\n]+\n)+)",
|
||||
RegexOptions.Multiline);
|
||||
var match = RegexPatterns.BuildInfo.ApplicationsTable.Match(md);
|
||||
|
||||
if (!match.Success) return;
|
||||
|
||||
var table = match.Groups["table"].Value;
|
||||
|
||||
var rows = Regex.Matches(table,
|
||||
@"^\|([^|]+)\|([^|]+)\|([^|]+)\|",
|
||||
RegexOptions.Multiline);
|
||||
var rows = RegexPatterns.BuildInfo.TableRow3Columns.Matches(table);
|
||||
|
||||
bool headerSkipped = false;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Jellyfin2Samsung.Helpers;
|
||||
using Jellyfin2Samsung.Helpers.API;
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Helpers.Tizen.Certificate;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.Models;
|
||||
@@ -510,7 +511,7 @@ namespace Jellyfin2Samsung.ViewModels
|
||||
JellyfinServerIp = uri.Host;
|
||||
SelectedJellyfinPort = uri.IsDefaultPort ? (uri.Scheme == "https" ? "443" : "80") : uri.Port.ToString();
|
||||
|
||||
var path = uri.AbsolutePath.TrimEnd('/');
|
||||
var path = UrlHelper.NormalizeServerUrl(uri.AbsolutePath);
|
||||
if (!string.IsNullOrEmpty(path) && path != "/")
|
||||
{
|
||||
jellyfinBasePath = path;
|
||||
@@ -541,7 +542,7 @@ namespace Jellyfin2Samsung.ViewModels
|
||||
SelectedJellyfinPort = uri.IsDefaultPort ? (uri.Scheme == "https" ? "443" : "80") : uri.Port.ToString();
|
||||
|
||||
// Extract the path portion (without triggering this handler again)
|
||||
var path = uri.AbsolutePath.TrimEnd('/');
|
||||
var path = UrlHelper.NormalizeServerUrl(uri.AbsolutePath);
|
||||
AppSettings.Default.JellyfinBasePath = path;
|
||||
jellyfinBasePath = path; // Set backing field directly to avoid recursion
|
||||
OnPropertyChanged(nameof(JellyfinBasePath));
|
||||
|
||||
@@ -5,15 +5,12 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Jellyfin2Samsung.Helpers;
|
||||
using Jellyfin2Samsung.Helpers.API;
|
||||
using Jellyfin2Samsung.Helpers.Converters;
|
||||
using Jellyfin2Samsung.Helpers.Core;
|
||||
using Jellyfin2Samsung.Helpers.Tizen.Devices;
|
||||
using Jellyfin2Samsung.Interfaces;
|
||||
using Jellyfin2Samsung.Models;
|
||||
using Jellyfin2Samsung.Views;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
@@ -411,36 +408,28 @@ namespace Jellyfin2Samsung.ViewModels
|
||||
|
||||
try
|
||||
{
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
MissingMemberHandling = MissingMemberHandling.Ignore,
|
||||
Converters = { new SingleOrArrayConverter<GitHubRelease>() }
|
||||
};
|
||||
|
||||
// 1️⃣ Do ALL async / HTTP work first
|
||||
var list = new List<GitHubRelease>();
|
||||
|
||||
async Task fetch(string url, string name)
|
||||
{
|
||||
var release = await _addLatestRelease.GetLatestReleaseAsync(url, name, settings);
|
||||
var release = await _addLatestRelease.GetLatestReleaseAsync(url, name);
|
||||
if (release != null)
|
||||
list.Add(release);
|
||||
}
|
||||
|
||||
await fetch(AppSettings.Default.ReleasesUrl, "Jellyfin");
|
||||
await fetch(AppSettings.Default.ReleasesUrl, Constants.AppIdentifiers.JellyfinAppName);
|
||||
await fetch(AppSettings.Default.MoonfinRelease, "Moonfin");
|
||||
await fetch(AppSettings.Default.JellyfinAvRelease, "Jellyfin - AVPlay");
|
||||
await fetch(AppSettings.Default.JellyfinLegacy, "Jellyfin - Legacy");
|
||||
await fetch(AppSettings.Default.CommunityRelease, "Tizen Community");
|
||||
|
||||
// 2️⃣ ONE UI-thread update, just like your original working version
|
||||
Releases.Clear();
|
||||
foreach (var r in list)
|
||||
Releases.Add(r);
|
||||
|
||||
Releases.Add(new GitHubRelease
|
||||
{
|
||||
Name = "Custom WGT File",
|
||||
Name = Constants.AppIdentifiers.CustomWgtFile,
|
||||
TagName = string.Empty,
|
||||
PublishedAt = string.Empty,
|
||||
Url = string.Empty,
|
||||
@@ -450,7 +439,7 @@ namespace Jellyfin2Samsung.ViewModels
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine(ex);
|
||||
await _dialogService.ShowErrorAsync($"{L("FailedLoadingReleases")} {ex}");
|
||||
await _dialogService.ShowErrorAsync($"{L(Constants.LocalizationKeys.FailedLoadingReleases)} {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -66,11 +66,11 @@
|
||||
Foreground="#2C3E50"/>
|
||||
|
||||
<!-- Language Section -->
|
||||
<Grid ColumnDefinitions="150,*" ColumnSpacing="12">
|
||||
<Grid ColumnDefinitions="180,*" ColumnSpacing="12">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{Binding LblLanguage}"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#34495E"/>
|
||||
Classes="label"
|
||||
VerticalAlignment="Center"/>
|
||||
<ComboBox Grid.Column="1"
|
||||
Classes="clean"
|
||||
ItemsSource="{Binding AvailableLanguages}"
|
||||
@@ -85,11 +85,11 @@
|
||||
</Grid>
|
||||
|
||||
<!-- Certificate Section -->
|
||||
<Grid ColumnDefinitions="150,*" ColumnSpacing="12">
|
||||
<Grid ColumnDefinitions="180,*" ColumnSpacing="12">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{Binding LblCertificate}"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#34495E"/>
|
||||
Classes="label"
|
||||
VerticalAlignment="Center"/>
|
||||
<ComboBox Grid.Column="1"
|
||||
Classes="clean"
|
||||
ItemsSource="{Binding AvailableCertificates}"
|
||||
@@ -104,15 +104,15 @@
|
||||
</Grid>
|
||||
|
||||
<!-- Local Device IP -->
|
||||
<Grid ColumnDefinitions="150,*" ColumnSpacing="12">
|
||||
<Grid ColumnDefinitions="180,*" ColumnSpacing="12">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{Binding LblLocalIP}"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#34495E"/>
|
||||
Classes="label"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding LocalIP, Mode=TwoWay}"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#7F8C8D"/>
|
||||
Classes="subtitle"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
@@ -124,8 +124,8 @@
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="12">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{Binding LblTryOverwrite}"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#34495E"/>
|
||||
Classes="label"
|
||||
VerticalAlignment="Center"/>
|
||||
<ToggleSwitch Grid.Column="1"
|
||||
IsChecked="{Binding TryOverwrite}"
|
||||
VerticalAlignment="Center"/>
|
||||
@@ -135,8 +135,8 @@
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="12">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{Binding LblLaunchOnInstall}"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#34495E"/>
|
||||
Classes="label"
|
||||
VerticalAlignment="Center"/>
|
||||
<ToggleSwitch Grid.Column="1"
|
||||
IsChecked="{Binding OpenAfterInstall}"
|
||||
VerticalAlignment="Center"/>
|
||||
@@ -146,8 +146,8 @@
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="12">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{Binding LblRememberIp}"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#34495E"/>
|
||||
Classes="label"
|
||||
VerticalAlignment="Center"/>
|
||||
<ToggleSwitch Grid.Column="1"
|
||||
IsChecked="{Binding RememberCustomIP}"
|
||||
VerticalAlignment="Center"/>
|
||||
@@ -157,8 +157,8 @@
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="12">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{Binding LblDeletePrevious}"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#34495E"/>
|
||||
Classes="label"
|
||||
VerticalAlignment="Center"/>
|
||||
<ToggleSwitch Grid.Column="1"
|
||||
IsChecked="{Binding DeletePreviousInstall}"
|
||||
VerticalAlignment="Center"/>
|
||||
@@ -168,8 +168,8 @@
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="12">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{Binding LblForceLogin}"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#34495E"/>
|
||||
Classes="label"
|
||||
VerticalAlignment="Center"/>
|
||||
<ToggleSwitch Grid.Column="1"
|
||||
IsChecked="{Binding ForceSamsungLogin}"
|
||||
VerticalAlignment="Center"/>
|
||||
@@ -179,8 +179,8 @@
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="12">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{Binding LblRTL}"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#34495E"/>
|
||||
Classes="label"
|
||||
VerticalAlignment="Center"/>
|
||||
<ToggleSwitch Grid.Column="1"
|
||||
IsChecked="{Binding RtlReading}"
|
||||
VerticalAlignment="Center"/>
|
||||
@@ -190,8 +190,8 @@
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="12">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{Binding LblKeepWGTFile}"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#34495E"/>
|
||||
Classes="label"
|
||||
VerticalAlignment="Center"/>
|
||||
<ToggleSwitch Grid.Column="1"
|
||||
IsChecked="{Binding KeepWGTFile}"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
Reference in New Issue
Block a user