aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/ISSUE_TEMPLATE/issue report.yml2
-rw-r--r--.github/workflows/ci-openapi.yml4
-rw-r--r--.github/workflows/ci-tests.yml2
-rw-r--r--Directory.Packages.props2
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-AR.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/es_419.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/kw.json82
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json4
-rw-r--r--MediaBrowser.Providers/Lyric/LrcLyricParser.cs125
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/LiveTvControllerTests.cs96
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/PluginsControllerTests.cs45
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj3
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Test Data/dummy.m3u81
15 files changed, 244 insertions, 138 deletions
diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml
index b71b365f7..cfb5a6ec2 100644
--- a/.github/ISSUE_TEMPLATE/issue report.yml
+++ b/.github/ISSUE_TEMPLATE/issue report.yml
@@ -86,7 +86,7 @@ body:
label: Jellyfin Server version
description: What version of Jellyfin are you using?
options:
- - 10.9.9+
+ - 10.9.10+
- Master
- Unstable
- Older*
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml
index 93bfc83a2..d01c506db 100644
--- a/.github/workflows/ci-openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -27,7 +27,7 @@ jobs:
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: openapi-head
retention-days: 14
@@ -61,7 +61,7 @@ jobs:
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: openapi-base
retention-days: 14
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index 91c2be87b..af8106c0a 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/ci-tests.yml
@@ -34,7 +34,7 @@ jobs:
--verbosity minimal
- name: Merge code coverage results
- uses: danielpalme/ReportGenerator-GitHub-Action@5808021ec4deecb0ab3da051d49b4ce65fcc20af # 5.3.8
+ uses: danielpalme/ReportGenerator-GitHub-Action@e3af7259842d9c814021ea121f85526e0872b25f # v5.3.9
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"
diff --git a/Directory.Packages.props b/Directory.Packages.props
index a153e1d5f..a52bcb53e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -22,7 +22,7 @@
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="4.0.7" />
- <PackageVersion Include="LrcParser" Version="2023.524.0" />
+ <PackageVersion Include="LrcParser" Version="2024.0728.2" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.8" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.8" />
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index ce98979e6..865a1ef95 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylists": "Sammlungen und Playlisten aufräumen",
"TaskCleanCollectionsAndPlaylistsDescription": "Lösche nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten.",
"TaskAudioNormalization": "Audio Normalisierung",
- "TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten."
+ "TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten.",
+ "TaskDownloadMissingLyricsDescription": "Lädt Liedtexte herunter",
+ "TaskDownloadMissingLyrics": "Fehlende Liedtexte herunterladen"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json
index 9433da28b..b926d9d30 100644
--- a/Emby.Server.Implementations/Localization/Core/es-AR.json
+++ b/Emby.Server.Implementations/Localization/Core/es-AR.json
@@ -130,5 +130,7 @@
"TaskAudioNormalization": "Normalización de audio",
"TaskAudioNormalizationDescription": "Escanea archivos en busca de datos de normalización de audio.",
"TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
- "TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen."
+ "TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen.",
+ "TaskDownloadMissingLyrics": "Descargar letra faltante",
+ "TaskDownloadMissingLyricsDescription": "Descarga letras de canciones"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index 13e007b4c..210ee4446 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
"TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen.",
"TaskAudioNormalization": "Normalización de audio",
- "TaskAudioNormalizationDescription": "Escanear archivos para obtener datos de normalización."
+ "TaskAudioNormalizationDescription": "Escanear archivos para obtener datos de normalización.",
+ "TaskDownloadMissingLyricsDescription": "Descargar letras para las canciones",
+ "TaskDownloadMissingLyrics": "Descargar letras faltantes"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json
index e7deefbb0..b458ed423 100644
--- a/Emby.Server.Implementations/Localization/Core/es_419.json
+++ b/Emby.Server.Implementations/Localization/Core/es_419.json
@@ -129,5 +129,7 @@
"TaskAudioNormalization": "Normalización de audio",
"TaskCleanCollectionsAndPlaylistsDescription": "Quita elementos que ya no existen de colecciones y listas de reproducción.",
"TaskAudioNormalizationDescription": "Analiza los archivos para normalizar el audio.",
- "TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción"
+ "TaskCleanCollectionsAndPlaylists": "Limpieza de colecciones y listas de reproducción",
+ "TaskDownloadMissingLyrics": "Descargar letra faltante",
+ "TaskDownloadMissingLyricsDescription": "Descarga letras de canciones"
}
diff --git a/Emby.Server.Implementations/Localization/Core/kw.json b/Emby.Server.Implementations/Localization/Core/kw.json
index d6ff58785..ffb4345c8 100644
--- a/Emby.Server.Implementations/Localization/Core/kw.json
+++ b/Emby.Server.Implementations/Localization/Core/kw.json
@@ -26,13 +26,13 @@
"NotificationOptionPluginUninstalled": "Ystynnans anynstallys",
"NotificationOptionPluginUpdateInstalled": "Nowedheans ystynnans ynstallys",
"Application": "Gweythres",
- "Favorites": "Drudh",
+ "Favorites": "Moyha Kerys",
"Forced": "Konstrynys",
"Albums": "Albomow",
"Books": "Lyvrow",
- "Channels": "Gothi",
+ "Channels": "Kanolyow",
"AppDeviceValues": "App: {0}, Devis: {1}",
- "Artists": "Artydhyon",
+ "Artists": "Artyhdyon",
"HeaderAlbumArtists": "Albom artydhyon",
"HeaderNextUp": "Nessa",
"CameraImageUploadedFrom": "Skeusen kamera nowydh re beu ughkargys a-dhyworth {0}",
@@ -44,7 +44,7 @@
"ItemRemovedWithName": "{0} a veu dileys a-dhyworth an lyverva",
"LabelIpAddressValue": "Trigva PK: {)}",
"Music": "Ilow",
- "HeaderContinueWatching": "Pesya ow kweles",
+ "HeaderContinueWatching": "Pesya Ow Kweles",
"NameSeasonNumber": "Seson {0}",
"NotificationOptionApplicationUpdateInstalled": "Nowedheans gweythres ynstallys",
"NotificationOptionCameraImageUploaded": "Skeusen kamera ughkargys",
@@ -59,5 +59,77 @@
"NotificationOptionInstallationFailed": "Defowt ynstallyans",
"Genres": "Eghennow",
"NotificationOptionPluginInstalled": "Ystynnans ynstallys",
- "NotificationOptionServerRestartRequired": "Dastalleth servell yw res"
+ "NotificationOptionServerRestartRequired": "Dastalleth servell yw res",
+ "StartupEmbyServerIsLoading": "Yma Servell Jellyfin ow kargya. Assay arta yn berr mar pleg.",
+ "SubtitleDownloadFailureFromForItem": "Istitlow a fyllis iskarga a-dhyworth {0] rag {1}",
+ "System": "Kevreyth",
+ "User": "Devnydhyer",
+ "UserDeletedWithName": "Devnydhyer {0} re beu dileys",
+ "UserLockedOutWithName": "Devnydhyer {0} re beu alhwedhys yn-mes",
+ "UserStoppedPlayingItemWithValues": "{0} re worfennas gwari {1} war {2}",
+ "UserOfflineFromDevice": "{0} re anjunyas a-dhyworth {1}",
+ "UserOnlineFromDevice": "{0} yw warlinen a-dhyworth {1}",
+ "NotificationOptionUserLockedOut": "Devnydhyer yw alhwedhys yn-mes",
+ "Photos": "Skeusennow",
+ "Playlists": "Rolyow-gwari",
+ "Plugin": "Ystynnans",
+ "PluginInstalledWithName": "{0} a veu ynstallys",
+ "UserPolicyUpdatedWithName": "Polici devnydhyer re beu nowedhys rag {0}",
+ "PluginUpdatedWithName": "{0} a veu nowedhys",
+ "ScheduledTaskFailedWithName": "{0} a fyllis",
+ "Songs": "Kanow",
+ "Sync": "Kesseni",
+ "TvShows": "Towlennow PW",
+ "Undefined": "Anstyrys",
+ "UserCreatedWithName": "Devnydhyer {0} re beu gwruthys",
+ "UserDownloadingItemWithValues": "Yma {0} owth iskarga {1}",
+ "UserPasswordChangedWithName": "Ger-tremena re beu chanjys rag devnydhyer {0}",
+ "UserStartedPlayingItemWithValues": "Yma {0} ow kwari {1} war {2}",
+ "ValueHasBeenAddedToLibrary": "{0} re beu keworrys dhe'th lyverva media",
+ "VersionNumber": "Versyon {0}",
+ "TasksLibraryCategory": "Lyverva",
+ "TaskCleanActivityLog": "Glanhe Kovlyver Gwrians",
+ "TaskRefreshPeople": "Disegha Tus",
+ "TaskRefreshLibrary": "Arhwilas Lyverva Media",
+ "TaskCleanTranscodeDescription": "Y hwra dilea restrennow treylya neg a veu gwrys kyns nans yw dydh.",
+ "NotificationOptionVideoPlaybackStopped": "Gwareans gwydhyow yw hedhys",
+ "NotificationOptionVideoPlayback": "Gwareans gwydhyow yw dallethys",
+ "PluginUninstalledWithName": "{0} a veu anynstallys",
+ "NotificationOptionTaskFailed": "Defowt oberen towlennys",
+ "ProviderValue": "Provier: {0}",
+ "ScheduledTaskStartedWithName": "{0} a dhallathas",
+ "ServerNameNeedsToBeRestarted": "Yma edhom dhe {0} a vos dastallathys",
+ "ValueSpecialEpisodeName": "Arbennik - {0}",
+ "TasksMaintenanceCategory": "Mentons",
+ "TasksApplicationCategory": "Gweythres",
+ "TasksChannelsCategory": "Kanolyow Kesrosweyth",
+ "TaskCleanLogs": "Glanhe Kevarwodhyador Kovlyver",
+ "TaskAudioNormalization": "Normalheans Klewans",
+ "TaskRefreshChannels": "Disegha Kanolyow",
+ "TaskCleanTranscode": "Glanhe Kevarwodhyador Treylya",
+ "TaskUpdatePlugins": "Nowedhi Ystynansow",
+ "Shows": "Diskwedhyansow",
+ "TaskCleanCache": "Glanhe Kevarwodhyador Gwithva",
+ "TaskCleanActivityLogDescription": "Y hwra dilea lin kovlyver gwrians kottha ages an bloodh dewisys.",
+ "TaskCleanCacheDescription": "Y hwra dilea restrennow gwithva nag yw res rag an kevreyth.",
+ "TaskRefreshPeopleDescription": "Y hwra nowedhi metadata rag gwarioryon ha kevarwodhoryon yn dha lyverva media.",
+ "TaskRefreshChapterImages": "Kuntel Imajys Chaptra",
+ "TaskRefreshChapterImagesDescription": "Y hwra ewines meus rag gwydhyowyow gans chaptraow.",
+ "TaskRefreshTrickplayImagesDescription": "Y hwra kynwelyow trickplay rag gwydhyowyow yn lyvervaow gallosegys.",
+ "TaskRefreshTrickplayImages": "Dinythi Imajys Trickplay",
+ "TaskCleanLogsDescription": "Y hwra dilea restrennow kovlyver a veu gwrys kyns nans yw {0} dydh.",
+ "TaskDownloadMissingLyrics": "Iskarga geryow kellys",
+ "TaskUpdatePluginsDescription": "Y hwra iskarga hag ynstallya nowedheansow rag ystynansow neb yw dewisys dhe nowedhi yn awtomatek.",
+ "TaskDownloadMissingSubtitles": "Iskarga istitlow kellys",
+ "TaskRefreshChannelsDescription": "Y hwra disegha kedhlow kanolyow kesrosweyth.",
+ "TaskDownloadMissingLyricsDescription": "Y hwra iskarga geryow rag kanow",
+ "TaskDownloadMissingSubtitlesDescription": "Y hwra hwilas an kesrosweyth rag istitlow kellys a-dhywoth dewisyans metadata.",
+ "TaskOptimizeDatabase": "Gwellhe selvanylyon",
+ "TaskOptimizeDatabaseDescription": "Y hwra kesstrotha ha berrhe efander rydh. Martesen y hwra gwellhe gwryth mar kwre'ta an oberen ma wosa ty dhe arhwilas an lyverva, po neb chanj aral neb a brof chanjyansow selvanylyon.",
+ "TaskAudioNormalizationDescription": "Y hwra arhwilas restrennow rag manylyon normalheans klewans.",
+ "TaskRefreshLibraryDescription": "Y hwra arhwilas dha lyverva media rag restrennow nowydh ha disegha metamanylyon.",
+ "TaskCleanCollectionsAndPlaylists": "Glanhe kuntellow ha rolyow-gwari",
+ "TaskKeyframeExtractor": "Estennell Framalhwedh",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Y hwra dilea taklow a-dhyworth kuntellow ha rolyow-gwari na vos na moy.",
+ "TaskKeyframeExtractorDescription": "Y hwra kuntel framyowalhwedh a-dhyworth restrennow gwydhyowyow rag gul rolyow-gwari HLS moy poran. Martesen y hwra an oberen ma ow ponya rag termyn hir."
}
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index b66818ddc..747652538 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -130,5 +130,7 @@
"TaskCleanCollectionsAndPlaylists": "Rydd kolleksjoner og spillelister",
"TaskAudioNormalization": "Lyd Normalisering",
"TaskAudioNormalizationDescription": "Skan filer for lyd normaliserende data",
- "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra kolleksjoner og spillelister som ikke lengere finnes"
+ "TaskCleanCollectionsAndPlaylistsDescription": "Fjerner elementer fra kolleksjoner og spillelister som ikke lengere finnes",
+ "TaskDownloadMissingLyrics": "Last ned manglende tekster",
+ "TaskDownloadMissingLyricsDescription": "Last ned sangtekster"
}
diff --git a/MediaBrowser.Providers/Lyric/LrcLyricParser.cs b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs
index 67b26e457..fffdf4887 100644
--- a/MediaBrowser.Providers/Lyric/LrcLyricParser.cs
+++ b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using System.Linq;
using Jellyfin.Extensions;
@@ -20,7 +19,6 @@ public class LrcLyricParser : ILyricParser
private readonly LyricParser _lrcLyricParser;
private static readonly string[] _supportedMediaTypes = [".lrc", ".elrc"];
- private static readonly string[] _acceptedTimeFormats = ["HH:mm:ss", "H:mm:ss", "mm:ss", "m:ss"];
/// <summary>
/// Initializes a new instance of the <see cref="LrcLyricParser"/> class.
@@ -59,37 +57,7 @@ public class LrcLyricParser : ILyricParser
return null;
}
- List<LrcParser.Model.Lyric> sortedLyricData = lyricData.Lyrics.Where(x => x.TimeTags.Count > 0).OrderBy(x => x.TimeTags.First().Value).ToList();
-
- // Parse metadata rows
- var metaDataRows = lyricData.Lyrics
- .Where(x => x.TimeTags.Count == 0)
- .Where(x => x.Text.StartsWith('[') && x.Text.EndsWith(']'))
- .Select(x => x.Text)
- .ToList();
-
- var fileMetaData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
- foreach (string metaDataRow in metaDataRows)
- {
- var index = metaDataRow.IndexOf(':', StringComparison.OrdinalIgnoreCase);
- if (index == -1)
- {
- continue;
- }
-
- // Remove square bracket before field name, and after field value
- // Example 1: [au: 1hitsong]
- // Example 2: [ar: Calabrese]
- var metaDataFieldName = GetMetadataFieldName(metaDataRow, index);
- var metaDataFieldValue = GetMetadataValue(metaDataRow, index);
-
- if (string.IsNullOrEmpty(metaDataFieldName) || string.IsNullOrEmpty(metaDataFieldValue))
- {
- continue;
- }
-
- fileMetaData[metaDataFieldName] = metaDataFieldValue;
- }
+ List<LrcParser.Model.Lyric> sortedLyricData = lyricData.Lyrics.OrderBy(x => x.StartTime).ToList();
if (sortedLyricData.Count == 0)
{
@@ -100,99 +68,10 @@ public class LrcLyricParser : ILyricParser
for (int i = 0; i < sortedLyricData.Count; i++)
{
- var timeData = sortedLyricData[i].TimeTags.First().Value;
- if (timeData is null)
- {
- continue;
- }
-
- long ticks = TimeSpan.FromMilliseconds(timeData.Value).Ticks;
+ long ticks = TimeSpan.FromMilliseconds(sortedLyricData[i].StartTime).Ticks;
lyricList.Add(new LyricLine(sortedLyricData[i].Text.Trim(), ticks));
}
- if (fileMetaData.Count != 0)
- {
- // Map metaData values from LRC file to LyricMetadata properties
- LyricMetadata lyricMetadata = MapMetadataValues(fileMetaData);
-
- return new LyricDto { Metadata = lyricMetadata, Lyrics = lyricList };
- }
-
return new LyricDto { Lyrics = lyricList };
}
-
- /// <summary>
- /// Converts metadata from an LRC file to LyricMetadata properties.
- /// </summary>
- /// <param name="metaData">The metadata from the LRC file.</param>
- /// <returns>A lyricMetadata object with mapped property data.</returns>
- private static LyricMetadata MapMetadataValues(Dictionary<string, string> metaData)
- {
- LyricMetadata lyricMetadata = new();
-
- if (metaData.TryGetValue("ar", out var artist) && !string.IsNullOrEmpty(artist))
- {
- lyricMetadata.Artist = artist;
- }
-
- if (metaData.TryGetValue("al", out var album) && !string.IsNullOrEmpty(album))
- {
- lyricMetadata.Album = album;
- }
-
- if (metaData.TryGetValue("ti", out var title) && !string.IsNullOrEmpty(title))
- {
- lyricMetadata.Title = title;
- }
-
- if (metaData.TryGetValue("au", out var author) && !string.IsNullOrEmpty(author))
- {
- lyricMetadata.Author = author;
- }
-
- if (metaData.TryGetValue("length", out var length) && !string.IsNullOrEmpty(length))
- {
- if (DateTime.TryParseExact(length, _acceptedTimeFormats, null, DateTimeStyles.None, out var value))
- {
- lyricMetadata.Length = value.TimeOfDay.Ticks;
- }
- }
-
- if (metaData.TryGetValue("by", out var by) && !string.IsNullOrEmpty(by))
- {
- lyricMetadata.By = by;
- }
-
- if (metaData.TryGetValue("offset", out var offset) && !string.IsNullOrEmpty(offset))
- {
- if (int.TryParse(offset, out var value))
- {
- lyricMetadata.Offset = TimeSpan.FromMilliseconds(value).Ticks;
- }
- }
-
- if (metaData.TryGetValue("re", out var creator) && !string.IsNullOrEmpty(creator))
- {
- lyricMetadata.Creator = creator;
- }
-
- if (metaData.TryGetValue("ve", out var version) && !string.IsNullOrEmpty(version))
- {
- lyricMetadata.Version = version;
- }
-
- return lyricMetadata;
- }
-
- private static string GetMetadataFieldName(string metaDataRow, int index)
- {
- var metadataFieldName = metaDataRow.AsSpan(1, index - 1).Trim();
- return metadataFieldName.IsEmpty ? string.Empty : metadataFieldName.ToString();
- }
-
- private static string GetMetadataValue(string metaDataRow, int index)
- {
- var metadataValue = metaDataRow.AsSpan(index + 1, metaDataRow.Length - index - 2).Trim();
- return metadataValue.IsEmpty ? string.Empty : metadataValue.ToString();
- }
}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LiveTvControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LiveTvControllerTests.cs
new file mode 100644
index 000000000..dd971fa87
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LiveTvControllerTests.cs
@@ -0,0 +1,96 @@
+using System.Net;
+using System.Net.Http.Json;
+using System.Net.Mime;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Model.LiveTv;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public sealed class LiveTvControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+ private readonly JellyfinApplicationFactory _factory;
+ private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+ private static string? _accessToken;
+
+ public LiveTvControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task AddTunerHost_Unauthorized_ReturnsUnauthorized()
+ {
+ var client = _factory.CreateClient();
+
+ var body = new TunerHostInfo()
+ {
+ Type = "m3u",
+ Url = "Test Data/dummy.m3u8"
+ };
+
+ var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions);
+
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task AddTunerHost_Valid_ReturnsCorrectResponse()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
+
+ var body = new TunerHostInfo()
+ {
+ Type = "m3u",
+ Url = "Test Data/dummy.m3u8"
+ };
+
+ var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions);
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
+ Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
+ var responseBody = await response.Content.ReadFromJsonAsync<TunerHostInfo>();
+ Assert.NotNull(responseBody);
+ Assert.Equal(body.Type, responseBody.Type);
+ Assert.Equal(body.Url, responseBody.Url);
+ }
+
+ [Fact]
+ public async Task AddTunerHost_InvalidType_ReturnsNotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
+
+ var body = new TunerHostInfo()
+ {
+ Type = "invalid",
+ Url = "Test Data/dummy.m3u8"
+ };
+
+ var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions);
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task AddTunerHost_InvalidUrl_ReturnsNotFound()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
+
+ var body = new TunerHostInfo()
+ {
+ Type = "m3u",
+ Url = "thisgoesnowhere"
+ };
+
+ var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions);
+
+ Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/PluginsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/PluginsControllerTests.cs
new file mode 100644
index 000000000..547bfcc0f
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/PluginsControllerTests.cs
@@ -0,0 +1,45 @@
+using System.Net;
+using System.Net.Http.Json;
+using System.Net.Mime;
+using System.Text;
+using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Model.Plugins;
+using Xunit;
+
+namespace Jellyfin.Server.Integration.Tests.Controllers;
+
+public sealed class PluginsControllerTests : IClassFixture<JellyfinApplicationFactory>
+{
+ private readonly JellyfinApplicationFactory _factory;
+ private static string? _accessToken;
+
+ public PluginsControllerTests(JellyfinApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact]
+ public async Task GetPlugins_Unauthorized_ReturnsUnauthorized()
+ {
+ var client = _factory.CreateClient();
+
+ var response = await client.GetAsync("/Plugins");
+
+ Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task GetPlugins_Authorized_ReturnsCorrectResponse()
+ {
+ var client = _factory.CreateClient();
+ client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
+
+ var response = await client.GetAsync("/Plugins");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
+ Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
+ _ = await response.Content.ReadFromJsonAsync<PluginInfo[]>(JsonDefaults.Options);
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
index a5296d8c9..8228c0df7 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
+++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
@@ -18,6 +18,9 @@
</ItemGroup>
<ItemGroup>
+ <None Include="Test Data\**\*.*">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
<!-- Don't run tests in parallel -->
<None Update="xunit.runner.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
diff --git a/tests/Jellyfin.Server.Integration.Tests/Test Data/dummy.m3u8 b/tests/Jellyfin.Server.Integration.Tests/Test Data/dummy.m3u8
new file mode 100644
index 000000000..7f60f38a6
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Test Data/dummy.m3u8
@@ -0,0 +1 @@
+C:\Music