aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.config/dotnet-tools.json2
-rw-r--r--.github/workflows/ci-codeql-analysis.yml4
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Directory.Packages.props36
-rw-r--r--Emby.Server.Implementations/Localization/Core/fi.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/hr.json24
-rw-r--r--Jellyfin.Api/Controllers/LibraryStructureController.cs4
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs10
-rw-r--r--tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs282
9 files changed, 328 insertions, 39 deletions
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 9cd9c08e75..3f7a9454c5 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "10.0.5",
+ "version": "10.0.6",
"commands": [
"dotnet-ef"
]
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index 5194c7df06..f6f9a26d06 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -8,6 +8,10 @@ on:
schedule:
- cron: '24 2 * * 4'
+permissions:
+ contents: read
+ security-events: write
+
jobs:
analyze:
name: Analyze
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 4b0cec3c92..01f968690f 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -212,6 +212,7 @@
- [martenumberto](https://github.com/martenumberto)
- [ZeusCraft10](https://github.com/ZeusCraft10)
- [MarcoCoreDuo](https://github.com/MarcoCoreDuo)
+ - [LiHRaM](https://github.com/LiHRaM)
# Emby Contributors
diff --git a/Directory.Packages.props b/Directory.Packages.props
index bf79ff0e0e..9b0d6f93c5 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -26,27 +26,27 @@
<PackageVersion Include="libse" Version="4.0.12" />
<PackageVersion Include="LrcParser" Version="2025.623.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="8.0.1" />
- <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.5" />
- <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.5" />
+ <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.6" />
+ <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.6" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="5.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0" />
- <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.5" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.5" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" />
- <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.5" />
- <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.5" />
- <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.5" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.5" />
- <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.5" />
- <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
- <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.5" />
- <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.5" />
- <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.5" />
- <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.5" />
- <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.5" />
+ <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.6" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.6" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.6" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.6" />
+ <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.6" />
+ <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.6" />
+ <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.6" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.6" />
+ <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.6" />
+ <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.6" />
+ <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.6" />
+ <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.6" />
+ <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.6" />
+ <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.6" />
+ <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.6" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
<PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
@@ -77,7 +77,7 @@
<PackageVersion Include="Svg.Skia" Version="3.4.1" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.7" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.7" />
- <PackageVersion Include="System.Text.Json" Version="10.0.5" />
+ <PackageVersion Include="System.Text.Json" Version="10.0.6" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="z440.atl.core" Version="7.12.0" />
<PackageVersion Include="TMDbLib" Version="3.0.0" />
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index 15a04d22cd..79afbb519b 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -39,8 +39,8 @@
"Channels": "Kanavat",
"CameraImageUploadedFrom": "Uusi kameran kuva on sirretty lähteestä {0}",
"Books": "Kirjat",
- "AuthenticationSucceededWithUserName": "{0} on todennettu",
- "Artists": "Esittäjät",
+ "AuthenticationSucceededWithUserName": "{0} todennus onnistunut",
+ "Artists": "Artistit",
"Application": "Sovellus",
"AppDeviceValues": "Sovellus: {0}, Laite: {1}",
"Albums": "Albumit",
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index 94db435715..eaeb173c20 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -23,7 +23,7 @@
"HeaderFavoriteShows": "Omiljene serije",
"HeaderFavoriteSongs": "Omiljene pjesme",
"HeaderLiveTV": "TV uživo",
- "HeaderNextUp": "Slijedi",
+ "HeaderNextUp": "Sljedeće na redu",
"HeaderRecordingGroups": "Grupa snimka",
"HomeVideos": "Kućni video",
"Inherit": "Naslijedi",
@@ -73,10 +73,10 @@
"Shows": "Emisije",
"Songs": "Pjesme",
"StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.",
- "SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}",
+ "SubtitleDownloadFailureFromForItem": "Titlovi nisu uspješno preuzeti od {0} za {1}",
"Sync": "Sinkronizacija",
"System": "Sustav",
- "TvShows": "Serije",
+ "TvShows": "TV emisije",
"User": "Korisnik",
"UserCreatedWithName": "Korisnik {0} je kreiran",
"UserDeletedWithName": "Korisnik {0} je obrisan",
@@ -88,26 +88,26 @@
"UserPolicyUpdatedWithName": "Pravila za korisnika ažurirana su za {0}",
"UserStartedPlayingItemWithValues": "{0} je pokrenuo reprodukciju {1} na {2}",
"UserStoppedPlayingItemWithValues": "{0} je završio reprodukciju {1} na {2}",
- "ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku",
- "ValueSpecialEpisodeName": "Posebno - {0}",
+ "ValueHasBeenAddedToLibrary": "{0} je dodano u biblioteku medija",
+ "ValueSpecialEpisodeName": "Posebno – {0}",
"VersionNumber": "Verzija {0}",
- "TaskRefreshLibraryDescription": "Skenira medijsku biblioteku radi novih datoteka i osvježava metapodatke.",
- "TaskRefreshLibrary": "Skeniraj medijsku biblioteku",
+ "TaskRefreshLibraryDescription": "Skenira biblioteku medija radi novih datoteka i osvježava metapodatke.",
+ "TaskRefreshLibrary": "Skeniraj biblioteku medija",
"TaskRefreshChapterImagesDescription": "Kreira sličice za videozapise koji imaju poglavlja.",
"TaskRefreshChapterImages": "Izdvoji slike poglavlja",
"TaskCleanCacheDescription": "Briše nepotrebne datoteke iz predmemorije.",
"TaskCleanCache": "Očisti mapu predmemorije",
"TasksApplicationCategory": "Aplikacija",
"TasksMaintenanceCategory": "Održavanje",
- "TaskDownloadMissingSubtitlesDescription": "Pretraži Internet za prijevodima koji nedostaju prema konfiguraciji metapodataka.",
- "TaskDownloadMissingSubtitles": "Preuzmi prijevod koji nedostaje",
+ "TaskDownloadMissingSubtitlesDescription": "Pretraži internet za nedsotajućim titlovima ne osnovi konfiguracije metapodataka.",
+ "TaskDownloadMissingSubtitles": "Preuzmi nedostajuće titlove",
"TaskRefreshChannelsDescription": "Osvježava informacije Internet kanala.",
"TaskRefreshChannels": "Osvježi kanale",
"TaskCleanTranscodeDescription": "Briše transkodirane datoteke starije od jednog dana.",
"TaskCleanTranscode": "Očisti mapu transkodiranja",
"TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su konfigurirani da se ažuriraju automatski.",
"TaskUpdatePlugins": "Ažuriraj dodatke",
- "TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u medijskoj biblioteci.",
+ "TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u biblioteci medija.",
"TaskRefreshPeople": "Osvježi osobe",
"TaskCleanLogsDescription": "Briše zapise dnevnika koji su stariji od {0} dana.",
"TaskCleanLogs": "Očisti mapu dnevnika zapisa",
@@ -119,7 +119,7 @@
"Forced": "Forsirani",
"Default": "Zadano",
"TaskOptimizeDatabase": "Optimiziraj bazu podataka",
- "External": "Vanjski",
+ "External": "Eksterni",
"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.",
@@ -135,7 +135,7 @@
"TaskDownloadMissingLyricsDescription": "Preuzmi tekstove pjesama",
"TaskExtractMediaSegmentsDescription": "Izvlači ili pribavlja dijelove medija iz omogućenih media pluginova.",
"TaskMoveTrickplayImages": "Premjesti mjesto slika brzog pregledavanja",
- "TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke brzog pregledavanja prema postavkama biblioteke.",
+ "TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke brzog pregledavanja u postavke biblioteke.",
"CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka",
"CleanupUserDataTaskDescription": "Briše sve korisničke podatke (stanje gledanja, status favorita itd.) s medija koji više nisu prisutni najmanje 90 dana."
}
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index 117811429a..8136dec177 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -75,7 +75,9 @@ public class LibraryStructureController : BaseJellyfinApiController
[HttpPost]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> AddVirtualFolder(
- [FromQuery] string name,
+ [FromQuery]
+ [RegularExpression(@"^(?:\S(?:.*\S)?)$", ErrorMessage = "Library name cannot be empty or have leading/trailing spaces.")]
+ string name,
[FromQuery] CollectionTypeOptions? collectionType,
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] paths,
[FromBody] AddVirtualFolderDto? libraryOptionsDto,
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index 9aeac7221e..5920fe3289 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -101,11 +101,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
return ms;
}
- private void FilterEvents(SubtitleTrackInfo track, long startPositionTicks, long endTimeTicks, bool preserveTimestamps)
+ internal void FilterEvents(SubtitleTrackInfo track, long startPositionTicks, long endTimeTicks, bool preserveTimestamps)
{
- // Drop subs that are earlier than what we're looking for
+ // Drop subs that have fully elapsed before the requested start position
track.TrackEvents = track.TrackEvents
- .SkipWhile(i => (i.StartPositionTicks - startPositionTicks) < 0 || (i.EndPositionTicks - startPositionTicks) < 0)
+ .SkipWhile(i => (i.StartPositionTicks - startPositionTicks) < 0 && (i.EndPositionTicks - startPositionTicks) < 0)
.ToArray();
if (endTimeTicks > 0)
@@ -119,8 +119,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
foreach (var trackEvent in track.TrackEvents)
{
- trackEvent.EndPositionTicks -= startPositionTicks;
- trackEvent.StartPositionTicks -= startPositionTicks;
+ trackEvent.EndPositionTicks = Math.Max(0, trackEvent.EndPositionTicks - startPositionTicks);
+ trackEvent.StartPositionTicks = Math.Max(0, trackEvent.StartPositionTicks - startPositionTicks);
}
}
}
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs
new file mode 100644
index 0000000000..5f84e85592
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Subtitles/FilterEventsTests.cs
@@ -0,0 +1,282 @@
+using System;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using MediaBrowser.MediaEncoding.Subtitles;
+using MediaBrowser.Model.MediaInfo;
+using Xunit;
+
+namespace Jellyfin.MediaEncoding.Subtitles.Tests
+{
+ public class FilterEventsTests
+ {
+ private readonly SubtitleEncoder _encoder;
+
+ public FilterEventsTests()
+ {
+ var fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
+ _encoder = fixture.Create<SubtitleEncoder>();
+ }
+
+ [Fact]
+ public void FilterEvents_SubtitleSpanningSegmentBoundary_IsRetained()
+ {
+ // Subtitle starts at 5s, ends at 15s.
+ // Segment requested from 10s to 20s.
+ // The subtitle is still on screen at 10s and should NOT be dropped.
+ var track = new SubtitleTrackInfo
+ {
+ TrackEvents = new[]
+ {
+ new SubtitleTrackEvent("1", "Still on screen")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(5).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
+ },
+ new SubtitleTrackEvent("2", "Next subtitle")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(17).Ticks
+ }
+ }
+ };
+
+ _encoder.FilterEvents(
+ track,
+ startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
+ endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
+ preserveTimestamps: true);
+
+ Assert.Equal(2, track.TrackEvents.Count);
+ Assert.Equal("1", track.TrackEvents[0].Id);
+ Assert.Equal("2", track.TrackEvents[1].Id);
+ }
+
+ [Fact]
+ public void FilterEvents_SubtitleFullyBeforeSegment_IsDropped()
+ {
+ // Subtitle starts at 2s, ends at 5s.
+ // Segment requested from 10s.
+ // The subtitle ended before the segment — should be dropped.
+ var track = new SubtitleTrackInfo
+ {
+ TrackEvents = new[]
+ {
+ new SubtitleTrackEvent("1", "Already gone")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(2).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(5).Ticks
+ },
+ new SubtitleTrackEvent("2", "Visible")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(17).Ticks
+ }
+ }
+ };
+
+ _encoder.FilterEvents(
+ track,
+ startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
+ endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
+ preserveTimestamps: true);
+
+ Assert.Single(track.TrackEvents);
+ Assert.Equal("2", track.TrackEvents[0].Id);
+ }
+
+ [Fact]
+ public void FilterEvents_SubtitleAfterSegment_IsDropped()
+ {
+ // Segment is 10s-20s, subtitle starts at 25s.
+ var track = new SubtitleTrackInfo
+ {
+ TrackEvents = new[]
+ {
+ new SubtitleTrackEvent("1", "In range")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
+ },
+ new SubtitleTrackEvent("2", "After segment")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(25).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(30).Ticks
+ }
+ }
+ };
+
+ _encoder.FilterEvents(
+ track,
+ startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
+ endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
+ preserveTimestamps: true);
+
+ Assert.Single(track.TrackEvents);
+ Assert.Equal("1", track.TrackEvents[0].Id);
+ }
+
+ [Fact]
+ public void FilterEvents_PreserveTimestampsFalse_AdjustsTimestamps()
+ {
+ var track = new SubtitleTrackInfo
+ {
+ TrackEvents = new[]
+ {
+ new SubtitleTrackEvent("1", "Subtitle")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(15).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(20).Ticks
+ }
+ }
+ };
+
+ _encoder.FilterEvents(
+ track,
+ startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
+ endTimeTicks: TimeSpan.FromSeconds(30).Ticks,
+ preserveTimestamps: false);
+
+ Assert.Single(track.TrackEvents);
+ // Timestamps should be shifted back by 10s
+ Assert.Equal(TimeSpan.FromSeconds(5).Ticks, track.TrackEvents[0].StartPositionTicks);
+ Assert.Equal(TimeSpan.FromSeconds(10).Ticks, track.TrackEvents[0].EndPositionTicks);
+ }
+
+ [Fact]
+ public void FilterEvents_PreserveTimestampsTrue_KeepsOriginalTimestamps()
+ {
+ var startTicks = TimeSpan.FromSeconds(15).Ticks;
+ var endTicks = TimeSpan.FromSeconds(20).Ticks;
+
+ var track = new SubtitleTrackInfo
+ {
+ TrackEvents = new[]
+ {
+ new SubtitleTrackEvent("1", "Subtitle")
+ {
+ StartPositionTicks = startTicks,
+ EndPositionTicks = endTicks
+ }
+ }
+ };
+
+ _encoder.FilterEvents(
+ track,
+ startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
+ endTimeTicks: TimeSpan.FromSeconds(30).Ticks,
+ preserveTimestamps: true);
+
+ Assert.Single(track.TrackEvents);
+ Assert.Equal(startTicks, track.TrackEvents[0].StartPositionTicks);
+ Assert.Equal(endTicks, track.TrackEvents[0].EndPositionTicks);
+ }
+
+ [Fact]
+ public void FilterEvents_SubtitleEndingExactlyAtSegmentStart_IsRetained()
+ {
+ // Subtitle ends exactly when the segment begins.
+ // EndPositionTicks == startPositionTicks means (end - start) == 0, not < 0,
+ // so SkipWhile stops and the subtitle is retained.
+ var track = new SubtitleTrackInfo
+ {
+ TrackEvents = new[]
+ {
+ new SubtitleTrackEvent("1", "Boundary subtitle")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(5).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(10).Ticks
+ },
+ new SubtitleTrackEvent("2", "In range")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
+ }
+ }
+ };
+
+ _encoder.FilterEvents(
+ track,
+ startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
+ endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
+ preserveTimestamps: true);
+
+ Assert.Equal(2, track.TrackEvents.Count);
+ Assert.Equal("1", track.TrackEvents[0].Id);
+ }
+
+ [Fact]
+ public void FilterEvents_SpanningBoundaryWithTimestampAdjustment_DoesNotProduceNegativeTimestamps()
+ {
+ // Subtitle starts at 5s, ends at 15s.
+ // Segment requested from 10s to 20s, preserveTimestamps = false.
+ // The subtitle spans the boundary and is retained, but shifting
+ // StartPositionTicks by -10s would produce -5s (negative).
+ var track = new SubtitleTrackInfo
+ {
+ TrackEvents = new[]
+ {
+ new SubtitleTrackEvent("1", "Spans boundary")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(5).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
+ },
+ new SubtitleTrackEvent("2", "Fully in range")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(17).Ticks
+ }
+ }
+ };
+
+ _encoder.FilterEvents(
+ track,
+ startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
+ endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
+ preserveTimestamps: false);
+
+ Assert.Equal(2, track.TrackEvents.Count);
+ // Subtitle 1: start should be clamped to 0, not -5s
+ Assert.True(track.TrackEvents[0].StartPositionTicks >= 0, "StartPositionTicks must not be negative");
+ Assert.Equal(TimeSpan.FromSeconds(5).Ticks, track.TrackEvents[0].EndPositionTicks);
+ // Subtitle 2: normal shift (12s - 10s = 2s, 17s - 10s = 7s)
+ Assert.Equal(TimeSpan.FromSeconds(2).Ticks, track.TrackEvents[1].StartPositionTicks);
+ Assert.Equal(TimeSpan.FromSeconds(7).Ticks, track.TrackEvents[1].EndPositionTicks);
+ }
+
+ [Fact]
+ public void FilterEvents_NoEndTimeTicks_ReturnsAllFromStartPosition()
+ {
+ var track = new SubtitleTrackInfo
+ {
+ TrackEvents = new[]
+ {
+ new SubtitleTrackEvent("1", "Before")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(2).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(4).Ticks
+ },
+ new SubtitleTrackEvent("2", "After")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
+ },
+ new SubtitleTrackEvent("3", "Much later")
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(500).Ticks,
+ EndPositionTicks = TimeSpan.FromSeconds(505).Ticks
+ }
+ }
+ };
+
+ _encoder.FilterEvents(
+ track,
+ startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
+ endTimeTicks: 0,
+ preserveTimestamps: true);
+
+ Assert.Equal(2, track.TrackEvents.Count);
+ Assert.Equal("2", track.TrackEvents[0].Id);
+ Assert.Equal("3", track.TrackEvents[1].Id);
+ }
+ }
+}