aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/automation.yml2
-rw-r--r--.github/workflows/codeql-analysis.yml6
-rw-r--r--.github/workflows/openapi.yml8
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs7
-rw-r--r--Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs7
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-MX.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/hr.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json7
-rw-r--r--Emby.Server.Implementations/TV/TVSeriesManager.cs30
-rw-r--r--Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs5
-rw-r--r--MediaBrowser.Controller/Entities/InternalItemsQuery.cs10
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs5
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs70
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/notitle.xml10
18 files changed, 134 insertions, 50 deletions
diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml
index 01cd41a08..0989df64b 100644
--- a/.github/workflows/automation.yml
+++ b/.github/workflows/automation.yml
@@ -14,7 +14,7 @@ jobs:
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Apply label
- uses: eps1lon/actions-label-merge-conflict@b8bf8341285ec9a4567d4318ba474fee998a6919 # tag=v2.0.1
+ uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with:
dirtyLabel: 'merge conflict'
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 7e6093391..39ba5ea4d 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -27,11 +27,11 @@ jobs:
dotnet-version: '6.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@cc7986c02bac29104a72998e67239bb5ee2ee110 # tag=v2
+ uses: github/codeql-action/init@c3b6fce4ee2ca25bc1066aa3bf73962fda0e8898 # tag=v2
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@cc7986c02bac29104a72998e67239bb5ee2ee110 # tag=v2
+ uses: github/codeql-action/autobuild@c3b6fce4ee2ca25bc1066aa3bf73962fda0e8898 # tag=v2
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@cc7986c02bac29104a72998e67239bb5ee2ee110 # tag=v2
+ uses: github/codeql-action/analyze@c3b6fce4ee2ca25bc1066aa3bf73962fda0e8898 # tag=v2
diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml
index 902d123c4..ca710fe83 100644
--- a/.github/workflows/openapi.yml
+++ b/.github/workflows/openapi.yml
@@ -23,7 +23,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@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3
+ uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3
with:
name: openapi-head
retention-days: 14
@@ -47,7 +47,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@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3
+ uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3
with:
name: openapi-base
retention-days: 14
@@ -63,12 +63,12 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
- uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741 # tag=v3
+ uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # tag=v3
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
- uses: actions/download-artifact@fb598a63ae348fa914e94cd0ff38f362e927b741 # tag=v3
+ uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # tag=v3
with:
name: openapi-base
path: openapi-base
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 0ebcd4c0b..371111dff 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -3524,10 +3524,11 @@ namespace Emby.Server.Implementations.Data
statement?.TryBind("@MinIndexNumber", query.MinIndexNumber.Value);
}
- if (query.MinParentIndexNumber.HasValue)
+ if (query.MinParentAndIndexNumber.HasValue)
{
- whereClauses.Add("ParentIndexNumber>=@MinParentIndexNumber");
- statement?.TryBind("@MinParentIndexNumber", query.MinParentIndexNumber.Value);
+ whereClauses.Add("((ParentIndexNumber=@MinParentAndIndexNumberParent and IndexNumber>=@MinParentAndIndexNumberIndex) or ParentIndexNumber>@MinParentAndIndexNumberParent)");
+ statement?.TryBind("@MinParentAndIndexNumberParent", query.MinParentAndIndexNumber.Value.ParentIndexNumber);
+ statement?.TryBind("@MinParentAndIndexNumberIndex", query.MinParentAndIndexNumber.Value.IndexNumber);
}
if (query.MinDateCreated.HasValue)
diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
index 7570a2bcf..82f0baf32 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
@@ -32,18 +32,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private readonly IServerConfigurationManager _config;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<XmlTvListingsProvider> _logger;
- private readonly IFileSystem _fileSystem;
public XmlTvListingsProvider(
IServerConfigurationManager config,
IHttpClientFactory httpClientFactory,
- ILogger<XmlTvListingsProvider> logger,
- IFileSystem fileSystem)
+ ILogger<XmlTvListingsProvider> logger)
{
_config = config;
_httpClientFactory = httpClientFactory;
_logger = logger;
- _fileSystem = fileSystem;
}
public string Name => "XmlTV";
@@ -165,7 +162,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
HasImage = !string.IsNullOrEmpty(program.Icon?.Source),
OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value,
CommunityRating = program.StarRating,
- SeriesId = program.Episode == null ? null : program.Title.GetMD5().ToString("N", CultureInfo.InvariantCulture)
+ SeriesId = program.Episode == null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture)
};
if (string.IsNullOrWhiteSpace(program.ProgramId))
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index 644d2676e..ab04693cc 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Optimitzar la base de dades",
"TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.",
"TaskKeyframeExtractor": "Extractor de fotogrames clau",
- "External": "Extern"
+ "External": "Extern",
+ "HearingImpaired": "Discapacitat Auditiva"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json
index a7391cc88..d677cc46c 100644
--- a/Emby.Server.Implementations/Localization/Core/es-MX.json
+++ b/Emby.Server.Implementations/Localization/Core/es-MX.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabaseDescription": "Compacta la base de datos y trunca el espacio libre. Puede mejorar el rendimiento si se realiza esta tarea después de escanear la biblioteca o después de realizar otros cambios que impliquen modificar la base de datos.",
"TaskKeyframeExtractorDescription": "Extrae los cuadros clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar un buen rato.",
"TaskKeyframeExtractor": "Extractor de Cuadros Clave",
- "External": "Externo"
+ "External": "Externo",
+ "HearingImpaired": "Discapacidad Auditiva"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index db65a0c6d..afffdf3bf 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabaseDescription": "Optimiza y libera el espacio libre en la base de datos. Ejecutar esta tarea tras escanear la biblioteca o hacer cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento.",
"TaskKeyframeExtractorDescription": "Extrae los fotogramas clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar mucho tiempo.",
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
- "External": "Externo"
+ "External": "Externo",
+ "HearingImpaired": "Discapacidad Auditiva"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index c63cd2b94..d01295419 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -123,5 +123,6 @@
"External": "Vanjski",
"TaskKeyframeExtractorDescription": "Izvlačenje ključnih okvira iz videozapisa za stvaranje objektivnije HLS liste za reprodukciju. Pokretanje ovog zadatka može potrajati.",
"TaskKeyframeExtractor": "Izvoditelj ključnog okvira",
- "TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka."
+ "TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka.",
+ "HearingImpaired": "Oštećen sluh"
}
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index 2aa84c536..3710f03e0 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -123,5 +123,6 @@
"TaskOptimizeDatabase": "Ottimizza Database",
"TaskKeyframeExtractor": "Estrattore di Keyframe",
"TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori playlist HLS. Questa procedura potrebbe richiedere molto tempo.",
- "External": "Esterno"
+ "External": "Esterno",
+ "HearingImpaired": "con problemi di udito"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index ea9a82d2b..dc45a8264 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -75,7 +75,7 @@
"StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.",
"SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить",
"SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}",
- "Sync": "Синхро",
+ "Sync": "Синхронизация",
"System": "Система",
"TvShows": "ТВ",
"User": "Пользователь",
@@ -117,11 +117,12 @@
"TaskCleanActivityLogDescription": "Удаляет записи журнала активности старше установленного возраста.",
"TaskCleanActivityLog": "Очистка журнала активности",
"Undefined": "Не определено",
- "Forced": "Форсир-ые",
+ "Forced": "Принудительно",
"Default": "По умолчанию",
"TaskOptimizeDatabaseDescription": "Сжимает базу данных и вырезает свободные места. Выполнение этой задачи после сканирования библиотеки или внесения других изменений, предполагающих модификации базы данных, может повысить производительность.",
"TaskOptimizeDatabase": "Оптимизация базы данных",
"TaskKeyframeExtractorDescription": "Извлекаются ключевые кадры из видеофайлов для создания более точных списков плей-листов HLS. Эта задача может выполняться в течение длительного времени.",
"TaskKeyframeExtractor": "Извлечение ключевых кадров",
- "External": "Внешние"
+ "External": "Внешние",
+ "HearingImpaired": "Для слабослышащих"
}
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index be57d9c68..5c9b9df15 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -192,7 +192,6 @@ namespace Emby.Server.Implementations.TV
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
IncludeItemTypes = new[] { BaseItemKind.Episode },
- OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) },
IsPlayed = true,
Limit = 1,
ParentIndexNumberNotEquals = 0,
@@ -203,11 +202,10 @@ namespace Emby.Server.Implementations.TV
}
};
- if (rewatching)
- {
- // find last watched by date played, not by newest episode watched
- lastQuery.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) };
- }
+ // If rewatching is enabled, sort first by date played and then by season and episode numbers
+ lastQuery.OrderBy = rewatching
+ ? new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) }
+ : new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) };
var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault();
@@ -223,23 +221,19 @@ namespace Emby.Server.Implementations.TV
IsPlayed = rewatching,
IsVirtualItem = false,
ParentIndexNumberNotEquals = 0,
- DtoOptions = dtoOptions,
- MinIndexNumber = lastWatchedEpisode?.IndexNumberEnd ?? lastWatchedEpisode?.IndexNumber,
- MinParentIndexNumber = lastWatchedEpisode?.ParentIndexNumber
+ DtoOptions = dtoOptions
};
- Episode nextEpisode;
- if (rewatching)
- {
- nextQuery.Limit = 2;
- // get watched episode after most recently watched
- nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().ElementAtOrDefault(1);
- }
- else
+ // Locate the next up episode based on the last watched episode's season and episode number
+ var lastWatchedParentIndexNumber = lastWatchedEpisode?.ParentIndexNumber;
+ var lastWatchedIndexNumber = lastWatchedEpisode?.IndexNumberEnd ?? lastWatchedEpisode?.IndexNumber;
+ if (lastWatchedParentIndexNumber.HasValue && lastWatchedIndexNumber.HasValue)
{
- nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault();
+ nextQuery.MinParentAndIndexNumber = (lastWatchedParentIndexNumber.Value, lastWatchedIndexNumber.Value + 1);
}
+ var nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault();
+
if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons)
{
var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user)
diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
index 9875df310..6ee5bf38a 100644
--- a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
+++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
@@ -65,8 +65,9 @@ namespace Jellyfin.Server.Middleware
// Always redirect back to the default path if the base prefix is invalid or missing
_logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);
- var uri = new Uri(localPath);
- var redirectUri = new Uri(baseUrlPrefix + "/" + _configuration[DefaultRedirectKey]);
+ var port = httpContext.Request.Host.Port ?? -1;
+ var uri = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, port, localPath).Uri;
+ var redirectUri = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, port, baseUrlPrefix + "/" + _configuration[DefaultRedirectKey]).Uri;
var target = uri.MakeRelativeUri(redirectUri).ToString();
_logger.LogDebug("Redirecting to {Target}", target);
diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
index 9ae21bb59..1bf528538 100644
--- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
+++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs
@@ -205,7 +205,15 @@ namespace MediaBrowser.Controller.Entities
public int? MinIndexNumber { get; set; }
- public int? MinParentIndexNumber { get; set; }
+ /// <summary>
+ /// Gets or sets the minimum ParentIndexNumber and IndexNumber.
+ /// </summary>
+ /// <remarks>
+ /// It produces this where clause:
+ /// <para>(ParentIndexNumber = X and IndexNumber >= Y) or ParentIndexNumber > X.
+ /// </para>
+ /// </remarks>
+ public (int ParentIndexNumber, int IndexNumber)? MinParentAndIndexNumber { get; set; }
public int? AiredDuringSeason { get; set; }
diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
index a9e1b4a51..92ce14be2 100644
--- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
@@ -68,7 +68,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
IgnoreComments = true
};
- _validProviderIds = _validProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ _validProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var idInfos = ProviderManager.GetExternalIdInfos(item.Item);
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
index 12ea2d55b..10077e5c8 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
@@ -408,10 +408,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
}
- if (isEnglishRequested)
- {
- item.Overview = result.Plot;
- }
+ item.Overview = result.Plot;
if (!Plugin.Instance.Configuration.CastAndCrew)
{
diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs
new file mode 100644
index 000000000..82ce8fc4e
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs
@@ -0,0 +1,70 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Emby.Server.Implementations.LiveTv.Listings;
+using MediaBrowser.Model.LiveTv;
+using Moq;
+using Moq.Protected;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.LiveTv.Listings;
+
+public class XmlTvListingsProviderTests
+{
+ private readonly Fixture _fixture;
+ private readonly XmlTvListingsProvider _xmlTvListingsProvider;
+
+ public XmlTvListingsProviderTests()
+ {
+ var messageHandler = new Mock<HttpMessageHandler>();
+ messageHandler.Protected()
+ .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
+ .Returns<HttpRequestMessage, CancellationToken>(
+ (m, _) =>
+ {
+ return Task.FromResult(new HttpResponseMessage()
+ {
+ Content = new StreamContent(File.OpenRead(Path.Combine("Test Data/LiveTv/Listings/XmlTv", m.RequestUri!.Segments[^1])))
+ });
+ });
+
+ var http = new Mock<IHttpClientFactory>();
+ http.Setup(x => x.CreateClient(It.IsAny<string>()))
+ .Returns(new HttpClient(messageHandler.Object));
+ _fixture = new Fixture();
+ _fixture.Customize(new AutoMoqCustomization
+ {
+ ConfigureMembers = true
+ }).Inject(http);
+ _xmlTvListingsProvider = _fixture.Create<XmlTvListingsProvider>();
+ }
+
+ [Theory]
+ [InlineData("Test Data/LiveTv/Listings/XmlTv/notitle.xml")]
+ [InlineData("https://example.com/notitle.xml")]
+ public async Task GetProgramsAsync_NoTitle_Success(string path)
+ {
+ var info = new ListingsProviderInfo()
+ {
+ Path = path
+ };
+
+ var startDate = new DateTime(2022, 11, 4);
+ var programs = await _xmlTvListingsProvider.GetProgramsAsync(info, "3297", startDate, startDate.AddDays(1), CancellationToken.None);
+ var programsList = programs.ToList();
+ Assert.Single(programsList);
+ var program = programsList[0];
+ Assert.Null(program.Name);
+ Assert.Null(program.SeriesId);
+ Assert.Null(program.EpisodeTitle);
+ Assert.True(program.IsSports);
+ Assert.True(program.HasImage);
+ Assert.Equal("https://domain.tld/image.png", program.ImageUrl);
+ Assert.Equal("3297", program.ChannelId);
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/notitle.xml b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/notitle.xml
new file mode 100644
index 000000000..5a5be7997
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/notitle.xml
@@ -0,0 +1,10 @@
+<tv date="20221104">
+ <programme channel="3297" start="20221104130000 -0400" stop="20221105235959 -0400">
+ <category lang="en">sports</category>
+ <episode-num system="original-air-date">2022-11-04 13:00:00</episode-num>
+ <icon height="" src="https://domain.tld/image.png" width=""/>
+ <credits/>
+ <video/>
+ <date/>
+ </programme>
+</tv>