diff options
120 files changed, 968 insertions, 420 deletions
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 9cd9c08e75..9b44eff4c6 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.7", "commands": [ "dotnet-ef" ] diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 5194c7df06..442114dd80 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 @@ -28,13 +32,13 @@ jobs: dotnet-version: '10.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index f9e2fbc3a6..dd48209a1f 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -26,7 +26,7 @@ jobs: dotnet build Jellyfin.Server -o ./out - name: Upload Head - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: abi-head retention-days: 14 @@ -65,7 +65,7 @@ jobs: dotnet build Jellyfin.Server -o ./out - name: Upload Head - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: abi-base retention-days: 14 diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index fc32cc884d..f0ecb166b4 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -35,7 +35,7 @@ jobs: --verbosity minimal - name: Merge code coverage results - uses: danielpalme/ReportGenerator-GitHub-Action@cf6fe1b38ed5becc89ffe056c1f240825993be5b # v5.5.4 + uses: danielpalme/ReportGenerator-GitHub-Action@a003c8fb9ac008fd0fffd5faa4f7d3ecb52e0675 # v5.5.7 with: reports: "**/coverage.cobertura.xml" targetdir: "merged/" diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 2adb8f1010..9d3d99cb71 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -36,7 +36,7 @@ jobs: rename: name: Rename - if: contains(github.event.comment.body, '@jellyfin-bot rename') && github.event.comment.author_association == 'MEMBER' + if: contains(github.event.comment.body, '@jellyfin-bot rename') runs-on: ubuntu-latest steps: - name: pull in script diff --git a/.github/workflows/openapi-generate.yml b/.github/workflows/openapi-generate.yml index 255cc49e82..dbfaf9d30b 100644 --- a/.github/workflows/openapi-generate.yml +++ b/.github/workflows/openapi-generate.yml @@ -36,7 +36,7 @@ jobs: run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter Jellyfin.Server.Integration.Tests.OpenApiSpecTests - name: Upload Artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ inputs.artifact }} path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json diff --git a/.github/workflows/openapi-pull-request.yml b/.github/workflows/openapi-pull-request.yml index 563a0a406f..4acd0f4d4f 100644 --- a/.github/workflows/openapi-pull-request.yml +++ b/.github/workflows/openapi-pull-request.yml @@ -74,7 +74,7 @@ jobs: docker run -v /tmp/openapi-report:/data openapitools/openapi-diff:2.1.6 /data/base.json /data/head.json --state -l ERROR --markdown /data/openapi-report.md - name: Upload Artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: openapi-report path: /tmp/openapi-report/openapi-report.md diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 4b0cec3c92..c42962786d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1,5 +1,6 @@ # Jellyfin Contributors + - [0x25CBFC4F](https://github.com/0x25CBFC4F) - [1337joe](https://github.com/1337joe) - [97carmine](https://github.com/97carmine) - [Abbe98](https://github.com/Abbe98) @@ -14,7 +15,7 @@ - [bilde2910](https://github.com/bilde2910) - [bfayers](https://github.com/bfayers) - [BnMcG](https://github.com/BnMcG) - - [Bond-009](https://github.com/Bond-009) + - [Bond_009](https://github.com/Bond-009) - [brianjmurrell](https://github.com/brianjmurrell) - [bugfixin](https://github.com/bugfixin) - [chaosinnovator](https://github.com/chaosinnovator) @@ -31,6 +32,7 @@ - [DaveChild](https://github.com/DaveChild) - [DavidFair](https://github.com/DavidFair) - [Delgan](https://github.com/Delgan) + - [DerMaddis](https://github.com/dermaddis) - [Derpipose](https://github.com/Derpipose) - [dcrdev](https://github.com/dcrdev) - [dhartung](https://github.com/dhartung) @@ -54,6 +56,7 @@ - [geilername](https://github.com/geilername) - [GermanCoding](https://github.com/GermanCoding) - [gnattu](https://github.com/gnattu) + - [gnuyent](https://github.com/gnuyent) - [GodTamIt](https://github.com/GodTamIt) - [grafixeyehero](https://github.com/grafixeyehero) - [h1nk](https://github.com/h1nk) @@ -61,6 +64,7 @@ - [HelloWorld017](https://github.com/HelloWorld017) - [ikomhoog](https://github.com/ikomhoog) - [iwalton3](https://github.com/iwalton3) + - [Jakob Kukla](https://github.com/jakobkukla) - [jftuga](https://github.com/jftuga) - [jkhsjdhjs](https://github.com/jkhsjdhjs) - [jmshrv](https://github.com/jmshrv) @@ -69,8 +73,10 @@ - [JustAMan](https://github.com/JustAMan) - [justinfenn](https://github.com/justinfenn) - [JPVenson](https://github.com/JPVenson) + - [JPUC1143](https://github.com/Jpuc1143/) - [KerryRJ](https://github.com/KerryRJ) - [Larvitar](https://github.com/Larvitar) + - [lbenini](https://github.com/lbenini) - [LeoVerto](https://github.com/LeoVerto) - [Liggy](https://github.com/Liggy) - [lmaonator](https://github.com/lmaonator) @@ -83,15 +89,19 @@ - [marius-luca-87](https://github.com/marius-luca-87) - [mark-monteiro](https://github.com/mark-monteiro) - [MarkCiliaVincenti](https://github.com/MarkCiliaVincenti) + - [Martin Reuter](https://github.com/reuterma24) - [Matt07211](https://github.com/Matt07211) + - [Matthew Jones](https://github.com/matthew-jones-uk) - [Maxr1998](https://github.com/Maxr1998) - [mcarlton00](https://github.com/mcarlton00) + - [Michael McElroy](https://github.com/mcmcelro) - [mitchfizz05](https://github.com/mitchfizz05) - [mohd-akram](https://github.com/mohd-akram) - [MrTimscampi](https://github.com/MrTimscampi) - [n8225](https://github.com/n8225) - [Nalsai](https://github.com/Nalsai) - [Narfinger](https://github.com/Narfinger) + - [Nathan McCrina](https://github.com/nfmccrina) - [NathanPickard](https://github.com/NathanPickard) - [neilsb](https://github.com/neilsb) - [nevado](https://github.com/nevado) @@ -102,6 +112,7 @@ - [OancaAndrei](https://github.com/OancaAndrei) - [obradovichv](https://github.com/obradovichv) - [oddstr13](https://github.com/oddstr13) + - [olsh](https://github.com/olsh) - [orryverducci](https://github.com/orryverducci) - [petermcneil](https://github.com/petermcneil) - [Phlogi](https://github.com/Phlogi) @@ -112,6 +123,7 @@ - [RazeLighter777](https://github.com/RazeLighter777) - [redSpoutnik](https://github.com/redSpoutnik) - [ringmatter](https://github.com/ringmatter) + - [Robert Lützner](https://github.com/rluetzner) - [ryan-hartzell](https://github.com/ryan-hartzell) - [s0urcelab](https://github.com/s0urcelab) - [sachk](https://github.com/sachk) @@ -127,6 +139,7 @@ - [sl1288](https://github.com/sl1288) - [Smith00101010](https://github.com/Smith00101010) - [sorinyo2004](https://github.com/sorinyo2004) + - [Soumyadip Auddy](https://github.com/SoumyadipAuddy) - [sparky8251](https://github.com/sparky8251) - [spookbits](https://github.com/spookbits) - [ssenart](https://github.com/ssenart) @@ -149,6 +162,7 @@ - [twinkybot](https://github.com/twinkybot) - [Ullmie02](https://github.com/Ullmie02) - [Unhelpful](https://github.com/Unhelpful) + - [Utku Özdemir](https://github.com/utkuozdemir) - [viaregio](https://github.com/viaregio) - [vitorsemeano](https://github.com/vitorsemeano) - [voodoos](https://github.com/voodoos) @@ -212,6 +226,9 @@ - [martenumberto](https://github.com/martenumberto) - [ZeusCraft10](https://github.com/ZeusCraft10) - [MarcoCoreDuo](https://github.com/MarcoCoreDuo) + - [LiHRaM](https://github.com/LiHRaM) + - [MSalman5230](https://github.com/MSalman5230) + - [dwandw](https://github.com/dwandw) # Emby Contributors @@ -275,17 +292,3 @@ - [tikuf](https://github.com/tikuf/) - [Tim Hobbs](https://github.com/timhobbs) - [SvenVandenbrande](https://github.com/SvenVandenbrande) - - [olsh](https://github.com/olsh) - - [lbenini](https://github.com/lbenini) - - [gnuyent](https://github.com/gnuyent) - - [Matthew Jones](https://github.com/matthew-jones-uk) - - [Jakob Kukla](https://github.com/jakobkukla) - - [Utku Özdemir](https://github.com/utkuozdemir) - - [JPUC1143](https://github.com/Jpuc1143/) - - [0x25CBFC4F](https://github.com/0x25CBFC4F) - - [Robert Lützner](https://github.com/rluetzner) - - [Nathan McCrina](https://github.com/nfmccrina) - - [Martin Reuter](https://github.com/reuterma24) - - [Michael McElroy](https://github.com/mcmcelro) - - [Soumyadip Auddy](https://github.com/SoumyadipAuddy) - - [DerMaddis](https://github.com/dermaddis) diff --git a/Directory.Packages.props b/Directory.Packages.props index f15f7c7a75..9b073e67a5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,48 +6,48 @@ <ItemGroup Label="Package Dependencies"> <PackageVersion Include="AsyncKeyedLock" Version="8.0.2" /> <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" /> - <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" /> + <PackageVersion Include="AutoFixture.Xunit3" Version="4.19.0" /> <PackageVersion Include="AutoFixture" Version="4.18.1" /> <PackageVersion Include="BDInfo" Version="0.8.0" /> <PackageVersion Include="BitFaster.Caching" Version="2.5.4" /> <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" /> <PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" /> <PackageVersion Include="CommandLineParser" Version="2.9.1" /> - <PackageVersion Include="coverlet.collector" Version="8.0.1" /> + <PackageVersion Include="coverlet.collector" Version="10.0.0" /> <PackageVersion Include="Diacritics" Version="4.1.4" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> - <PackageVersion Include="FsCheck.Xunit" Version="3.3.2" /> + <PackageVersion Include="FsCheck.Xunit.v3" Version="3.3.3" /> <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.1" /> <PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" /> <PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" /> <PackageVersion Include="Ignore" Version="0.2.1" /> - <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> + <PackageVersion Include="Jellyfin.XmlTv" Version="10.12.0-pre1" /> <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.7" /> + <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.7" /> <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.NET.Test.Sdk" Version="18.3.0" /> + <PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.7" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.7" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.7" /> + <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.7" /> + <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.7" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.7" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.7" /> + <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.7" /> + <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.7" /> + <PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.7" /> + <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.7" /> + <PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.7" /> + <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.0" /> <PackageVersion Include="MimeTypes" Version="2.5.2" /> <PackageVersion Include="Morestachio" Version="5.0.1.631" /> <PackageVersion Include="Moq" Version="4.18.4" /> @@ -77,14 +77,13 @@ <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.7" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> - <PackageVersion Include="z440.atl.core" Version="7.12.0" /> + <PackageVersion Include="z440.atl.core" Version="7.13.0" /> <PackageVersion Include="TMDbLib" Version="3.0.0" /> <PackageVersion Include="UTF.Unknown" Version="2.6.0" /> - <PackageVersion Include="Xunit.Priority" Version="1.1.6" /> - <PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" /> - <PackageVersion Include="Xunit.SkippableFact" Version="1.5.61" /> - <PackageVersion Include="xunit" Version="2.9.3" /> + <PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" /> + <PackageVersion Include="xunit.v3" Version="3.2.2" /> + <PackageVersion Include="Xunit.v3.Priority" Version="1.1.18" /> </ItemGroup> </Project> diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index 7cff2a25b6..23bd5cf200 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -60,6 +60,7 @@ namespace Emby.Server.Implementations.IO _fileSystem = fileSystem; appLifetime.ApplicationStarted.Register(Start); + appLifetime.ApplicationStopping.Register(Stop); } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs index 7cc851b73b..ef20ae9bca 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs @@ -50,6 +50,10 @@ public class ArtistsValidator public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) { var names = _itemRepo.GetAllArtistNames(); + var existingArtistIds = _libraryManager.GetItemIds(new InternalItemsQuery + { + IncludeItemTypes = [BaseItemKind.MusicArtist] + }).ToHashSet(); var numComplete = 0; var count = names.Count; @@ -59,8 +63,13 @@ public class ArtistsValidator try { var item = _libraryManager.GetArtist(name); + var isNew = !existingArtistIds.Contains(item.Id); + var neverRefreshed = item.DateLastRefreshed == default; - await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + if (isNew || neverRefreshed) + { + await item.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } } catch (OperationCanceledException) { diff --git a/Emby.Server.Implementations/Localization/Core/bs.json b/Emby.Server.Implementations/Localization/Core/bs.json new file mode 100644 index 0000000000..72b2a1f693 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/bs.json @@ -0,0 +1,141 @@ +{ + "Albums": "Albumi", + "Artists": "Umjetnici", + "Books": "Knjige", + "Channels": "Kanalima", + "Collections": "Zbirke", + "Default": "Zadano", + "Favorites": "Omiljeni", + "Folders": "Mape", + "Genres": "Žanrovi", + "HeaderAlbumArtists": "Umjetnici albuma", + "HeaderContinueWatching": "Nastavi gledati", + "Movies": "Filmovi", + "MusicVideos": "Muzički spotovi", + "Photos": "Slike", + "Playlists": "Plejliste", + "Shows": "Pokazuje", + "Songs": "Pjesme", + "ValueSpecialEpisodeName": "Posebno - {0}", + "AppDeviceValues": "Aplikacija: {0}, Uređaj: {1}", + "Application": "Prijava", + "AuthenticationSucceededWithUserName": "{0} uspješno autentificirano", + "CameraImageUploadedFrom": "Nova slika s kamere je postavljena sa {0}", + "ChapterNameValue": "Poglavlje {0}", + "DeviceOfflineWithName": "{0} se odspojio", + "DeviceOnlineWithName": "{0} je povezan", + "External": "Vanjsko", + "FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave sa {0}", + "Forced": "Prisilno", + "HeaderFavoriteAlbums": "Omiljeni albumi", + "HeaderFavoriteArtists": "Omiljeni umjetnici", + "HeaderFavoriteEpisodes": "Omiljene epizode", + "HeaderFavoriteShows": "Omiljene emisije", + "HeaderFavoriteSongs": "Omiljene pjesme", + "HeaderLiveTV": "TV uživo", + "HeaderNextUp": "Slijedi", + "HeaderRecordingGroups": "Grupe za snimanje", + "HearingImpaired": "Oštećen sluh", + "HomeVideos": "Kućni videozapisi", + "Inherit": "Nasljedi", + "ItemAddedWithName": "{0} je dodan u biblioteku", + "ItemRemovedWithName": "{0} je uklonjen iz biblioteke", + "LabelIpAddressValue": "IP adresa: {0}", + "LabelRunningTimeValue": "Trajanje: {0}", + "Latest": "Posljednje dodano", + "MessageApplicationUpdated": "Jellyfin Server je ažuriran", + "MessageApplicationUpdatedTo": "Jellyfin Server je ažuriran na {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "Sekcija za konfiguraciju servera {0} je ažurirana", + "MessageServerConfigurationUpdated": "Konfiguracija servera je ažurirana", + "MixedContent": "Miješani sadržaj", + "Music": "Muzika", + "NameInstallFailed": "{0} instalacija je propala", + "NameSeasonNumber": "Sezona {0}", + "NameSeasonUnknown": "Sezona nepoznata", + "NewVersionIsAvailable": "Dostupna je nova verzija Jellyfin Servera za preuzimanje.", + "NotificationOptionApplicationUpdateAvailable": "Dostupno ažuriranje aplikacije", + "NotificationOptionApplicationUpdateInstalled": "Ažuriranje aplikacije instalirano", + "NotificationOptionAudioPlayback": "Pokrenuto je reproduciranje zvuka", + "NotificationOptionAudioPlaybackStopped": "Zaustavljeno je reproduciranje zvuka", + "NotificationOptionCameraImageUploaded": "Učitana slika s kamere", + "NotificationOptionInstallationFailed": "Neuspjeh instalacije", + "NotificationOptionNewLibraryContent": "Dodan novi sadržaj", + "NotificationOptionPluginError": "Neuspjeh dodatka", + "NotificationOptionPluginInstalled": "Dodatak je instaliran", + "NotificationOptionPluginUninstalled": "Dodatak je deinstaliran", + "NotificationOptionPluginUpdateInstalled": "Ažuriranje dodatka je instalirano", + "NotificationOptionServerRestartRequired": "Potreban je ponovni pokret servera", + "NotificationOptionTaskFailed": "Neuspjeh zakazane zadatke", + "NotificationOptionUserLockedOut": "Korisnik je zaključan", + "NotificationOptionVideoPlayback": "Pokrenuto je reproduciranje videa", + "NotificationOptionVideoPlaybackStopped": "Reprodukcija videa je zaustavljena", + "Plugin": "Plugin", + "PluginInstalledWithName": "{0} je instaliran", + "PluginUninstalledWithName": "{0} je deinstaliran", + "PluginUpdatedWithName": "{0} je ažurirano", + "ProviderValue": "Pružatelj: {0}", + "ScheduledTaskFailedWithName": "{0} nije uspjelo", + "ScheduledTaskStartedWithName": "{0} počelo", + "ServerNameNeedsToBeRestarted": "{0} treba ponovo pokrenuti", + "StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Molimo pokušajte ponovo za kratko vrijeme.", + "SubtitleDownloadFailureFromForItem": "Podtitlovi nisu uspjeli preuzeti sa {0} za {1}", + "Sync": "Sinkronizacija", + "System": "Sistem", + "TvShows": "TV serije", + "Undefined": "Nedefinirano", + "User": "Korisnik", + "UserCreatedWithName": "Korisnik {0} je kreiran", + "UserDeletedWithName": "Korisnik {0} je izbrisan", + "UserDownloadingItemWithValues": "{0} preuzima {1}", + "UserLockedOutWithName": "Korisnik {0} je zaključan", + "UserOfflineFromDevice": "{0} se odspojio od {1}", + "UserOnlineFromDevice": "{0} je online od {1}", + "UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}", + "UserPolicyUpdatedWithName": "Pravila za korisnike su ažurirana za {0}", + "UserStartedPlayingItemWithValues": "{0} igra protiv {1} na {2}", + "UserStoppedPlayingItemWithValues": "{0} je završio igru protiv {1} na {2}", + "ValueHasBeenAddedToLibrary": "{0} je dodan u vašu medijsku biblioteku", + "VersionNumber": "Verzija {0}", + "TasksMaintenanceCategory": "Održavanje", + "TasksLibraryCategory": "Biblioteka", + "TasksApplicationCategory": "Prijava", + "TasksChannelsCategory": "Internetski kanali", + "TaskCleanActivityLog": "Očisti dnevnik aktivnosti", + "TaskCleanActivityLogDescription": "Brisanje unosa u dnevnik aktivnosti starijih od konfigurisane starosti.", + "TaskCleanCache": "Očistite direktorij keša", + "TaskCleanCacheDescription": "Brisanje keš datoteka koje sistemu više nisu potrebne.", + "TaskRefreshChapterImages": "Izvadi slike iz poglavlja", + "TaskRefreshChapterImagesDescription": "Stvara minijature za videozapise koji imaju poglavlja.", + "TaskAudioNormalization": "Normalizacija zvuka", + "TaskAudioNormalizationDescription": "Skeneriše datoteke radi podataka za normalizaciju zvuka.", + "TaskRefreshLibrary": "Skenerisati medijsku biblioteku", + "TaskRefreshLibraryDescription": "Skenerira vašu medijsku biblioteku na nove datoteke i osvježava metapodatke.", + "TaskCleanLogs": "Očisti direktorij dnevnika", + "TaskCleanLogsDescription": "Brisanje dnevničkih datoteka starijih od {0} dana.", + "TaskRefreshPeople": "Osvježite ljude", + "TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i režisere u vašoj medijskoj biblioteci.", + "TaskRefreshTrickplayImages": "Generirajte Trickplay slike", + "TaskRefreshTrickplayImagesDescription": "Stvara pregled trik-igara za videozapise u omogućenim bibliotekama.", + "TaskUpdatePlugins": "Ažuriraj dodatke", + "TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja dodataka koji su konfigurisani da se automatski ažuriraju.", + "TaskCleanTranscode": "Očisti Transcode direktorij", + "TaskCleanTranscodeDescription": "Brisanje transkodiranih datoteka starijih od jednog dana.", + "TaskRefreshChannels": "Osvježi kanale", + "TaskRefreshChannelsDescription": "Osvježava informacije o internetskom kanalu.", + "TaskDownloadMissingLyrics": "Preuzmi nedostajuće tekstove", + "TaskDownloadMissingLyricsDescription": "Preuzmi tekstove pjesama", + "TaskDownloadMissingSubtitles": "Preuzmite nedostajuće titlove", + "TaskDownloadMissingSubtitlesDescription": "Pretražuje internet u potrazi za nedostajućim titlovima na osnovu konfiguracije metapodataka.", + "TaskOptimizeDatabase": "Optimizirajte bazu podataka", + "TaskOptimizeDatabaseDescription": "Komprimira bazu podataka i čisti slobodan prostor. Pokretanje ovog zadatka nakon skeniranja biblioteke ili izvođenja drugih promjena koje podrazumijevaju izmjene baze podataka može poboljšati performanse.", + "TaskKeyframeExtractor": "Izvađač ključnih sličica", + "TaskKeyframeExtractorDescription": "Izvlači ključne okvire iz video datoteka kako bi kreirao preciznije HLS playliste. Ovaj zadatak može trajati dugo.", + "TaskCleanCollectionsAndPlaylists": "Očistite kolekcije i playliste", + "TaskCleanCollectionsAndPlaylistsDescription": "Uklanja stavke iz kolekcija i playlista koje više ne postoje.", + "TaskExtractMediaSegments": "Analiza medijskog segmenta", + "TaskExtractMediaSegmentsDescription": "Izvlači ili dobija medijske segmente iz dodataka koji podržavaju MediaSegment.", + "TaskMoveTrickplayImages": "Migracija lokacije slike Trickplay", + "TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke trik-igara prema postavkama biblioteke.", + "CleanupUserDataTask": "Zadatak čišćenja korisničkih podataka", + "CleanupUserDataTaskDescription": "Čisti sve korisničke podatke (stanje praćenja, status omiljenog itd.) sa medija koji više nije prisutan najmanje 90 dana." +} diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index fce3a614c2..ec5cbf0d43 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -63,8 +63,8 @@ "Photos": "Fotos", "Playlists": "Llistes de reproducció", "Plugin": "Complement", - "PluginInstalledWithName": "{0} s'ha instal·lat", - "PluginUninstalledWithName": "{0} s'ha desinstal·lat", + "PluginInstalledWithName": "S'ha instal·lat {0}", + "PluginUninstalledWithName": "S'ha desinstal·lat {0}", "PluginUpdatedWithName": "S'ha actualitzat {0}", "ProviderValue": "Proveïdor: {0}", "ScheduledTaskFailedWithName": "{0} ha fallat", 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/Emby.Server.Implementations/Localization/Core/ht.json b/Emby.Server.Implementations/Localization/Core/ht.json index f927d3173a..183c422a85 100644 --- a/Emby.Server.Implementations/Localization/Core/ht.json +++ b/Emby.Server.Implementations/Localization/Core/ht.json @@ -58,5 +58,8 @@ "ValueSpecialEpisodeName": "Spesyal - {0}", "VersionNumber": "Vesyon {0}", "TasksApplicationCategory": "Aplikasyon", - "TasksMaintenanceCategory": "Antretyen" + "TasksMaintenanceCategory": "Antretyen", + "AppDeviceValues": "Aplikasyon: {0}, Aparèy: {1}", + "AuthenticationSucceededWithUserName": "{0} otantifye avèk siksè", + "CameraImageUploadedFrom": "Une nouvelle image de la caméra a été téléchargée depuis {0}" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 8ae899a73c..0a454b2938 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -29,7 +29,7 @@ "Inherit": "繼承", "ItemAddedWithName": "{0} 經已加咗入媒體櫃", "ItemRemovedWithName": "{0} 經已由媒體櫃移除咗", - "LabelIpAddressValue": "IP 地址:{0}", + "LabelIpAddressValue": "IP 位址:{0}", "LabelRunningTimeValue": "運行時間:{0}", "Latest": "最新", "MessageApplicationUpdated": "Jellyfin 經已更新咗", diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index bc80c2b405..6fca5bc1ba 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -138,7 +138,7 @@ namespace Emby.Server.Implementations.Localization string twoCharName = parts[2]; if (string.IsNullOrWhiteSpace(twoCharName)) { - continue; + twoCharName = string.Empty; } else if (twoCharName.Contains('-', StringComparison.OrdinalIgnoreCase)) { diff --git a/Emby.Server.Implementations/Localization/countries.json b/Emby.Server.Implementations/Localization/countries.json index d92dc880b1..811a7d4094 100644 --- a/Emby.Server.Implementations/Localization/countries.json +++ b/Emby.Server.Implementations/Localization/countries.json @@ -750,6 +750,12 @@ "TwoLetterISORegionName": "TJ" }, { + "DisplayName": "Tanzania", + "Name": "TZ", + "ThreeLetterISORegionName": "TZA", + "TwoLetterISORegionName": "TZ" + }, + { "DisplayName": "Thailand", "Name": "TH", "ThreeLetterISORegionName": "THA", diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 6b8888d244..d4d0f4537b 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -832,10 +832,6 @@ namespace Emby.Server.Implementations.Session { data.Played = true; } - else - { - data.Played = false; - } _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None); } diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs index 47d3f4b7f7..d6cc0e71a4 100644 --- a/Jellyfin.Api/Controllers/ActivityLogController.cs +++ b/Jellyfin.Api/Controllers/ActivityLogController.cs @@ -19,6 +19,7 @@ namespace Jellyfin.Api.Controllers; /// </summary> [Route("System/ActivityLog")] [Authorize(Policy = Policies.RequiresElevation)] +[Tags("System")] public class ActivityLogController : BaseJellyfinApiController { private readonly IActivityManager _activityManager; diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs index 3363d7bad2..161479e4ca 100644 --- a/Jellyfin.Api/Controllers/ApiKeyController.cs +++ b/Jellyfin.Api/Controllers/ApiKeyController.cs @@ -14,6 +14,7 @@ namespace Jellyfin.Api.Controllers; /// Authentication controller. /// </summary> [Route("Auth")] +[Tags("Authentication")] public class ApiKeyController : BaseJellyfinApiController { private readonly IAuthenticationManager _authenticationManager; diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index 99b0fde06d..f97ab414ce 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -25,6 +25,7 @@ namespace Jellyfin.Api.Controllers; /// </summary> [Route("Artists")] [Authorize] +[Tags("Artist")] public class ArtistsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index 0d85b3a0db..e46ef0e31d 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -25,6 +25,7 @@ namespace Jellyfin.Api.Controllers; /// Channels Controller. /// </summary> [Authorize] +[Tags("Channel")] public class ChannelsController : BaseJellyfinApiController { private readonly IChannelManager _channelManager; diff --git a/Jellyfin.Api/Controllers/ClientLogController.cs b/Jellyfin.Api/Controllers/ClientLogController.cs index 139888bde8..c213b87940 100644 --- a/Jellyfin.Api/Controllers/ClientLogController.cs +++ b/Jellyfin.Api/Controllers/ClientLogController.cs @@ -15,6 +15,7 @@ namespace Jellyfin.Api.Controllers; /// Client log controller. /// </summary> [Authorize] +[Tags("System")] public class ClientLogController : BaseJellyfinApiController { private const int MaxDocumentSize = 1_000_000; diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 9e03fbeb06..ecd667b2e8 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -20,6 +20,7 @@ namespace Jellyfin.Api.Controllers; /// </summary> [Route("System")] [Authorize] +[Tags("System")] public class ConfigurationController : BaseJellyfinApiController { private readonly IServerConfigurationManager _configurationManager; diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 50050262f0..eadb8c9855 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -19,6 +19,7 @@ namespace Jellyfin.Api.Controllers; /// Devices Controller. /// </summary> [Authorize(Policy = Policies.RequiresElevation)] +[Tags("Device")] public class DevicesController : BaseJellyfinApiController { private readonly IDeviceManager _deviceManager; diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index ef54e9db54..61d40a7268 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -20,6 +20,7 @@ namespace Jellyfin.Api.Controllers; /// Display Preferences Controller. /// </summary> [Authorize] +[Tags("DisplayPreference")] public class DisplayPreferencesController : BaseJellyfinApiController { private readonly IDisplayPreferencesManager _displayPreferencesManager; diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index 456e643fd7..39c3f5abcf 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -25,6 +25,7 @@ namespace Jellyfin.Api.Controllers; /// The genres controller. /// </summary> [Authorize] +[Tags("Genre")] public class GenresController : BaseJellyfinApiController { private readonly IUserManager _userManager; diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 39760556a6..69cdba6afd 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -30,6 +30,7 @@ namespace Jellyfin.Api.Controllers; /// </summary> [Route("")] [Authorize] +[Tags("Item")] public class ItemsController : BaseJellyfinApiController { private readonly IUserManager _userManager; 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/Jellyfin.Api/Controllers/LyricsController.cs b/Jellyfin.Api/Controllers/LyricsController.cs index 8eb4cadf88..5a27b2719e 100644 --- a/Jellyfin.Api/Controllers/LyricsController.cs +++ b/Jellyfin.Api/Controllers/LyricsController.cs @@ -7,7 +7,6 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; using Jellyfin.Extensions; using MediaBrowser.Common.Api; using MediaBrowser.Controller.Entities.Audio; @@ -27,6 +26,7 @@ namespace Jellyfin.Api.Controllers; /// Lyrics controller. /// </summary> [Route("")] +[Tags("Lyric")] public class LyricsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Controllers/MediaSegmentsController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs index b8836d7cf1..65565826a4 100644 --- a/Jellyfin.Api/Controllers/MediaSegmentsController.cs +++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs @@ -20,6 +20,7 @@ namespace Jellyfin.Api.Controllers; /// Media Segments api. /// </summary> [Authorize] +[Tags("MediaSegment")] public class MediaSegmentsController : BaseJellyfinApiController { private readonly IMediaSegmentManager _mediaSegmentManager; diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index ace9a06395..50d34d0656 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; @@ -18,6 +17,7 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api.Controllers; @@ -26,6 +26,7 @@ namespace Jellyfin.Api.Controllers; /// Movies controller. /// </summary> [Authorize] +[Tags("Movie")] public class MoviesController : BaseJellyfinApiController { private readonly IUserManager _userManager; diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index a6427df67a..48d4ebdc04 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -25,6 +25,7 @@ namespace Jellyfin.Api.Controllers; /// The music genres controller. /// </summary> [Authorize] +[Tags("MusicGenre")] public class MusicGenresController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 274e94ee6d..1f8f963f70 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; -using Jellyfin.Api.Constants; using MediaBrowser.Common.Api; using MediaBrowser.Common.Updates; using MediaBrowser.Controller.Configuration; @@ -19,6 +18,7 @@ namespace Jellyfin.Api.Controllers; /// </summary> [Route("")] [Authorize(Policy = Policies.RequiresElevation)] +[Tags("Plugin")] public class PackageController : BaseJellyfinApiController { private readonly IInstallationManager _installationManager; diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 2b2afb0fe6..8e7026341d 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -22,6 +22,7 @@ namespace Jellyfin.Api.Controllers; /// Persons controller. /// </summary> [Authorize] +[Tags("Person")] public class PersonsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; @@ -50,6 +51,9 @@ public class PersonsController : BaseJellyfinApiController /// <param name="startIndex">Optional. All items with a lower index will be dropped from the response.</param> /// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="searchTerm">The search term.</param> + /// <param name="nameStartsWith">Optional. Filter by items whose name starts with the given input string.</param> + /// <param name="nameLessThan">Optional. Filter by items whose name will appear before this value when sorted alphabetically.</param> + /// <param name="nameStartsWithOrGreater">Optional. Filter by items whose name will appear after this value when sorted alphabetically.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param> /// <param name="filters">Optional. Specify additional filters to apply.</param> /// <param name="isFavorite">Optional filter by items that are marked as favorite, or not. userId is required.</param> @@ -70,6 +74,9 @@ public class PersonsController : BaseJellyfinApiController [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? searchTerm, + [FromQuery] string? nameStartsWith, + [FromQuery] string? nameLessThan, + [FromQuery] string? nameStartsWithOrGreater, [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, @@ -97,6 +104,9 @@ public class PersonsController : BaseJellyfinApiController excludePersonTypes) { NameContains = searchTerm, + NameStartsWith = nameStartsWith, + NameLessThan = nameLessThan, + NameStartsWithOrGreater = nameStartsWithOrGreater, User = user, IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite, AppearsInItemId = appearsInItemId ?? Guid.Empty, diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 9679180937..048a49ffd4 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -29,6 +29,7 @@ namespace Jellyfin.Api.Controllers; /// Playlists controller. /// </summary> [Authorize] +[Tags("Playlist")] public class PlaylistsController : BaseJellyfinApiController { private readonly IPlaylistManager _playlistManager; diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs index ade0906b34..b8361dfd88 100644 --- a/Jellyfin.Api/Controllers/PlaystateController.cs +++ b/Jellyfin.Api/Controllers/PlaystateController.cs @@ -6,7 +6,6 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Database.Implementations.Entities; -using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; @@ -25,6 +24,7 @@ namespace Jellyfin.Api.Controllers; /// </summary> [Route("")] [Authorize] +[Tags("Session")] public class PlaystateController : BaseJellyfinApiController { private readonly IUserManager _userManager; diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 53b7349e7d..e20efe90a9 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; using Jellyfin.Extensions.Json; using MediaBrowser.Common.Api; using MediaBrowser.Common.Plugins; @@ -23,6 +22,7 @@ namespace Jellyfin.Api.Controllers; /// Plugins controller. /// </summary> [Authorize(Policy = Policies.RequiresElevation)] +[Tags("Plugin")] public class PluginsController : BaseJellyfinApiController { private readonly IInstallationManager _installationManager; diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs index bdb2a4d20b..5c7b38e137 100644 --- a/Jellyfin.Api/Controllers/QuickConnectController.cs +++ b/Jellyfin.Api/Controllers/QuickConnectController.cs @@ -16,6 +16,7 @@ namespace Jellyfin.Api.Controllers; /// <summary> /// Quick connect controller. /// </summary> +[Tags("Authentication")] public class QuickConnectController : BaseJellyfinApiController { private readonly IQuickConnect _quickConnect; diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs index 065466cbca..f122d0f5e5 100644 --- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs +++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using Jellyfin.Api.Constants; using MediaBrowser.Common.Api; using MediaBrowser.Model.Tasks; using Microsoft.AspNetCore.Authorization; @@ -15,6 +14,7 @@ namespace Jellyfin.Api.Controllers; /// Scheduled Tasks Controller. /// </summary> [Authorize(Policy = Policies.RequiresElevation)] +[Tags("ScheduledTask")] public class ScheduledTasksController : BaseJellyfinApiController { private readonly ITaskManager _taskManager; diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index ad08dc5f9b..a8feb206a4 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -22,6 +22,7 @@ namespace Jellyfin.Api.Controllers; /// Studios controller. /// </summary> [Authorize] +[Tags("Studio")] public class StudiosController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index e9e404076f..9c5515dd92 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -23,6 +23,7 @@ namespace Jellyfin.Api.Controllers; /// </summary> [Route("")] [Authorize] +[Tags("Suggestion")] public class SuggestionsController : BaseJellyfinApiController { private readonly IDtoService _dtoService; diff --git a/Jellyfin.Api/Controllers/TimeSyncController.cs b/Jellyfin.Api/Controllers/TimeSyncController.cs index d7304cf426..fe6e11f9e2 100644 --- a/Jellyfin.Api/Controllers/TimeSyncController.cs +++ b/Jellyfin.Api/Controllers/TimeSyncController.cs @@ -9,6 +9,7 @@ namespace Jellyfin.Api.Controllers; /// The time sync controller. /// </summary> [Route("")] +[Tags("System")] public class TimeSyncController : BaseJellyfinApiController { /// <summary> diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index 3e4bac89a5..f8c5bd4b87 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -15,6 +15,7 @@ namespace Jellyfin.Api.Controllers; /// The trailers controller. /// </summary> [Authorize] +[Tags("Trailer")] public class TrailersController : BaseJellyfinApiController { private readonly ItemsController _itemsController; diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs index c9f8b36768..d7a10ce5f6 100644 --- a/Jellyfin.Api/Controllers/TrickplayController.cs +++ b/Jellyfin.Api/Controllers/TrickplayController.cs @@ -5,7 +5,6 @@ using System.Text; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Trickplay; @@ -21,6 +20,7 @@ namespace Jellyfin.Api.Controllers; /// </summary> [Route("")] [Authorize] +[Tags("TrickPlay")] public class TrickplayController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index c86c9b8f61..e45a100b77 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -27,6 +27,7 @@ namespace Jellyfin.Api.Controllers; /// </summary> [Route("Shows")] [Authorize] +[Tags("Show")] public class TvShowsController : BaseJellyfinApiController { private readonly IUserManager _userManager; diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index f4e0c86143..d4e9b234c5 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -29,6 +29,7 @@ namespace Jellyfin.Api.Controllers; /// The universal audio controller. /// </summary> [Route("")] +[Tags("Audio")] public class UniversalAudioController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index ed4bba2bb1..c1d06bad36 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -26,6 +26,7 @@ namespace Jellyfin.Api.Controllers; /// </summary> [Route("")] [Authorize] +[Tags("UserView")] public class UserViewsController : BaseJellyfinApiController { private readonly IUserManager _userManager; diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs index b67c6fdb7b..2c8b452c35 100644 --- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs +++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs @@ -19,6 +19,7 @@ namespace Jellyfin.Api.Controllers; /// Attachments controller. /// </summary> [Route("Videos")] +[Tags("Video")] public class VideoAttachmentsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 7854edc5ac..394a95ee5f 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -35,6 +35,7 @@ namespace Jellyfin.Api.Controllers; /// <summary> /// The videos controller. /// </summary> +[Tags("Video")] public class VideosController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index 685334a9f0..aa6464ee7a 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -26,6 +26,7 @@ namespace Jellyfin.Api.Controllers; /// Years controller. /// </summary> [Authorize] +[Tags("Year")] public class YearsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index ad9953d1b6..7147fbfe7d 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -235,6 +235,21 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I query = query.Where(e => e.Name.ToUpper().Contains(nameContainsUpper)); } + if (!string.IsNullOrWhiteSpace(filter.NameStartsWith)) + { + query = query.Where(e => e.Name.StartsWith(filter.NameStartsWith.ToLowerInvariant())); + } + + if (!string.IsNullOrWhiteSpace(filter.NameLessThan)) + { + query = query.Where(e => e.Name.CompareTo(filter.NameLessThan.ToLowerInvariant()) < 0); + } + + if (!string.IsNullOrWhiteSpace(filter.NameStartsWithOrGreater)) + { + query = query.Where(e => e.Name.CompareTo(filter.NameStartsWithOrGreater.ToLowerInvariant()) >= 0); + } + return query; } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index c71c193e2e..a498901481 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -312,7 +312,7 @@ namespace Jellyfin.Server.Extensions return; } - if (prefixLength == NetworkConstants.MinimumIPv4PrefixSize) + if ((addr.AddressFamily == AddressFamily.InterNetwork && prefixLength == NetworkConstants.MinimumIPv4PrefixSize) || (addr.AddressFamily == AddressFamily.InterNetworkV6 && prefixLength == NetworkConstants.MinimumIPv6PrefixSize)) { options.KnownProxies.Add(addr); } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 93f71fdc69..93ba672535 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -161,7 +161,6 @@ namespace Jellyfin.Server _loggerFactory, options, startupConfig); - _appHost = appHost; var configurationCompleted = false; try { @@ -207,6 +206,7 @@ namespace Jellyfin.Server await jellyfinMigrationService.MigrateStepAsync(JellyfinMigrationStageTypes.CoreInitialisation, appHost.ServiceProvider).ConfigureAwait(false); await appHost.InitializeServices(startupConfig).ConfigureAwait(false); + _appHost = appHost; await jellyfinMigrationService.MigrateStepAsync(JellyfinMigrationStageTypes.AppInitialisation, appHost.ServiceProvider).ConfigureAwait(false); await jellyfinMigrationService.CleanupSystemAfterMigration(_logger).ConfigureAwait(false); @@ -263,6 +263,7 @@ namespace Jellyfin.Server _appHost = null; _jellyfinHost?.Dispose(); + _jellyfinHost = null; } } diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 1aa39f97b6..05975929db 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -142,6 +142,7 @@ public sealed class SetupServer : IDisposable ThrowIfDisposed(); var retryAfterValue = TimeSpan.FromSeconds(5); var config = _configurationManager.GetNetworkConfiguration()!; + _startupServer?.Dispose(); _startupServer = Host.CreateDefaultBuilder(["hostBuilder:reloadConfigOnChange=false"]) .UseConsoleLifetime() .UseSerilog() diff --git a/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs b/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs deleted file mode 100644 index 0445397ad8..0000000000 --- a/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs +++ /dev/null @@ -1,21 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.Providers; - -namespace MediaBrowser.Common.Providers -{ - public class SubtitleConfigurationFactory : IConfigurationFactory - { - /// <inheritdoc /> - public IEnumerable<ConfigurationStore> GetConfigurations() - { - yield return new ConfigurationStore() - { - Key = "subtitles", - ConfigurationType = typeof(SubtitleOptions) - }; - } - } -} diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs index f4b3910b0e..e12ba22343 100644 --- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs @@ -42,6 +42,12 @@ namespace MediaBrowser.Controller.Entities public string NameContains { get; set; } + public string NameStartsWith { get; set; } + + public string NameLessThan { get; set; } + + public string NameStartsWithOrGreater { get; set; } + public User User { get; set; } public bool? IsFavorite { get; set; } diff --git a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs index 3c3ac2471f..905aad17b9 100644 --- a/MediaBrowser.Controller/LiveTv/ProgramInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ProgramInfo.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -12,45 +10,45 @@ namespace MediaBrowser.Controller.LiveTv { public ProgramInfo() { - Genres = new List<string>(); + Genres = []; - ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + ProviderIds = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase); + SeriesProviderIds = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase); } /// <summary> /// Gets or sets the id of the program. /// </summary> - public string Id { get; set; } + public string? Id { get; set; } /// <summary> /// Gets or sets the channel identifier. /// </summary> /// <value>The channel identifier.</value> - public string ChannelId { get; set; } + public string? ChannelId { get; set; } /// <summary> /// Gets or sets the name of the program. /// </summary> - public string Name { get; set; } + public string? Name { get; set; } /// <summary> /// Gets or sets the official rating. /// </summary> /// <value>The official rating.</value> - public string OfficialRating { get; set; } + public string? OfficialRating { get; set; } /// <summary> /// Gets or sets the overview. /// </summary> /// <value>The overview.</value> - public string Overview { get; set; } + public string? Overview { get; set; } /// <summary> /// Gets or sets the short overview. /// </summary> /// <value>The short overview.</value> - public string ShortOverview { get; set; } + public string? ShortOverview { get; set; } /// <summary> /// Gets or sets the start date of the program, in UTC. @@ -108,25 +106,25 @@ namespace MediaBrowser.Controller.LiveTv /// Gets or sets the episode title. /// </summary> /// <value>The episode title.</value> - public string EpisodeTitle { get; set; } + public string? EpisodeTitle { get; set; } /// <summary> /// Gets or sets the image path if it can be accessed directly from the file system. /// </summary> /// <value>The image path.</value> - public string ImagePath { get; set; } + public string? ImagePath { get; set; } /// <summary> /// Gets or sets the image url if it can be downloaded. /// </summary> /// <value>The image URL.</value> - public string ImageUrl { get; set; } + public string? ImageUrl { get; set; } - public string ThumbImageUrl { get; set; } + public string? ThumbImageUrl { get; set; } - public string LogoImageUrl { get; set; } + public string? LogoImageUrl { get; set; } - public string BackdropImageUrl { get; set; } + public string? BackdropImageUrl { get; set; } /// <summary> /// Gets or sets a value indicating whether this instance has image. @@ -188,19 +186,19 @@ namespace MediaBrowser.Controller.LiveTv /// Gets or sets the home page URL. /// </summary> /// <value>The home page URL.</value> - public string HomePageUrl { get; set; } + public string? HomePageUrl { get; set; } /// <summary> /// Gets or sets the series identifier. /// </summary> /// <value>The series identifier.</value> - public string SeriesId { get; set; } + public string? SeriesId { get; set; } /// <summary> /// Gets or sets the show identifier. /// </summary> /// <value>The show identifier.</value> - public string ShowId { get; set; } + public string? ShowId { get; set; } /// <summary> /// Gets or sets the season number. @@ -218,10 +216,10 @@ namespace MediaBrowser.Controller.LiveTv /// Gets or sets the etag. /// </summary> /// <value>The etag.</value> - public string Etag { get; set; } + public string? Etag { get; set; } - public Dictionary<string, string> ProviderIds { get; set; } + public Dictionary<string, string?> ProviderIds { get; set; } - public Dictionary<string, string> SeriesProviderIds { get; set; } + public Dictionary<string, string?> SeriesProviderIds { get; set; } } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 73c5b88c8b..770965cab3 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -1331,8 +1331,7 @@ namespace MediaBrowser.MediaEncoding.Encoder public bool CanExtractSubtitles(string codec) { - // TODO is there ever a case when a subtitle can't be extracted?? - return true; + return _configurationManager.GetEncodingOptions().EnableSubtitleExtraction; } private sealed class ProcessWrapper : IDisposable diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index d3e7b52315..3c6a03713f 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -729,6 +729,9 @@ namespace MediaBrowser.MediaEncoding.Probing stream.Type = MediaStreamType.Audio; stream.LocalizedDefault = _localization.GetLocalizedString("Default"); stream.LocalizedExternal = _localization.GetLocalizedString("External"); + stream.LocalizedLanguage = string.IsNullOrEmpty(stream.Language) + ? null + : _localization.FindLanguageInfo(stream.Language)?.DisplayName; stream.Channels = streamInfo.Channels; @@ -767,6 +770,9 @@ namespace MediaBrowser.MediaEncoding.Probing stream.LocalizedForced = _localization.GetLocalizedString("Forced"); stream.LocalizedExternal = _localization.GetLocalizedString("External"); stream.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired"); + stream.LocalizedLanguage = string.IsNullOrEmpty(stream.Language) + ? null + : _localization.FindLanguageInfo(stream.Language)?.DisplayName; if (string.IsNullOrEmpty(stream.Title)) { 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/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 75b8c137f7..c9697c685c 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1555,7 +1555,7 @@ namespace MediaBrowser.Model.Dlna continue; } - if (!subtitleStream.IsExternal && !transcoderSupport.CanExtractSubtitles(subtitleStream.Codec)) + if (!subtitleStream.IsExternal && playMethod == PlayMethod.Transcode && !transcoderSupport.CanExtractSubtitles(subtitleStream.Codec)) { continue; } diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 11f81ff7d8..4491fb5ace 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; -using System.Linq; using System.Text; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs index 79f8675cbf..c0d1bc86e7 100644 --- a/MediaBrowser.Model/Net/MimeTypes.cs +++ b/MediaBrowser.Model/Net/MimeTypes.cs @@ -132,6 +132,7 @@ namespace MediaBrowser.Model.Net // Type image new("image/jpeg", ".jpg"), + new("image/jpg", ".jpg"), new("image/tiff", ".tiff"), new("image/x-png", ".png"), new("image/x-icon", ".ico"), diff --git a/MediaBrowser.Model/Providers/SubtitleOptions.cs b/MediaBrowser.Model/Providers/SubtitleOptions.cs deleted file mode 100644 index 6ea1e14862..0000000000 --- a/MediaBrowser.Model/Providers/SubtitleOptions.cs +++ /dev/null @@ -1,36 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; - -namespace MediaBrowser.Model.Providers -{ - public class SubtitleOptions - { - public SubtitleOptions() - { - DownloadLanguages = Array.Empty<string>(); - - SkipIfAudioTrackMatches = true; - RequirePerfectMatch = true; - } - - public bool SkipIfEmbeddedSubtitlesPresent { get; set; } - - public bool SkipIfAudioTrackMatches { get; set; } - - public string[] DownloadLanguages { get; set; } - - public bool DownloadMovieSubtitles { get; set; } - - public bool DownloadEpisodeSubtitles { get; set; } - - public string OpenSubtitlesUsername { get; set; } - - public string OpenSubtitlesPasswordHash { get; set; } - - public bool IsOpenSubtitleVipAccount { get; set; } - - public bool RequirePerfectMatch { get; set; } - } -} diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index e0354dbdfa..727f481b65 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -255,7 +255,7 @@ namespace MediaBrowser.Providers.Manager catch (Exception ex) { result.ErrorMessage = ex.Message; - _logger.LogError(ex, "Error in {Provider}", provider.Name); + _logger.LogError(ex, "Error in {Provider} for {Item}", provider.Name, item.Path ?? item.Name); } } @@ -339,7 +339,7 @@ namespace MediaBrowser.Providers.Manager catch (Exception ex) { result.ErrorMessage = ex.Message; - _logger.LogError(ex, "Error in {Provider}", provider.Name); + _logger.LogError(ex, "Error in {Provider} for {Item}", provider.Name, item.Path ?? item.Name); } } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index e9cb46eab5..abdfb1e3b7 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -820,7 +820,7 @@ namespace MediaBrowser.Providers.Manager } catch (Exception ex) { - Logger.LogError(ex, "Error in {Provider}", provider.Name); + Logger.LogError(ex, "Error in {Provider} for {Item}", provider.Name, logName); // If a local provider fails, consider that a failure refreshResult.ErrorMessage = ex.Message; @@ -886,7 +886,7 @@ namespace MediaBrowser.Providers.Manager catch (Exception ex) { refreshResult.ErrorMessage = ex.Message; - Logger.LogError(ex, "Error in {Provider}", provider.Name); + Logger.LogError(ex, "Error in {Provider} for {Item}", provider.Name, logName); } } @@ -935,7 +935,7 @@ namespace MediaBrowser.Providers.Manager { refreshResult.Failures++; refreshResult.ErrorMessage = ex.Message; - Logger.LogError(ex, "Error in {Provider}", provider.Name); + Logger.LogError(ex, "Error in {Provider} for {Item}", provider.Name, logName); } } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index f8e2aece1f..0bab73180f 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -487,6 +487,13 @@ namespace MediaBrowser.Providers.Manager return true; } + // Artists without a folder structure that are derived from metadata have no real path in the library, + // so GetLibraryOptions returns null. Allow all providers through rather than blocking them. + if (item is MusicArtist && libraryTypeOptions is null) + { + return true; + } + return _baseItemManager.IsMetadataFetcherEnabled(item, libraryTypeOptions, provider.Name); } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index bde23e842f..fdc2f36469 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -8,7 +8,6 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; using Jellyfin.Extensions; -using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -25,7 +24,6 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Providers; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.MediaInfo @@ -74,7 +72,6 @@ namespace MediaBrowser.Providers.MediaInfo _subtitleResolver = subtitleResolver; _mediaAttachmentRepository = mediaAttachmentRepository; _mediaStreamRepository = mediaStreamRepository; - _mediaStreamRepository = mediaStreamRepository; } public async Task<ItemUpdateType> ProbeVideo<T>( @@ -366,6 +363,8 @@ namespace MediaBrowser.Providers.MediaInfo blurayVideoStream.ColorSpace = ffmpegVideoStream.ColorSpace; blurayVideoStream.ColorTransfer = ffmpegVideoStream.ColorTransfer; blurayVideoStream.ColorPrimaries = ffmpegVideoStream.ColorPrimaries; + blurayVideoStream.BitDepth = ffmpegVideoStream.BitDepth; + blurayVideoStream.PixelFormat = ffmpegVideoStream.PixelFormat; } } @@ -549,47 +548,19 @@ namespace MediaBrowser.Providers.MediaInfo var enableSubtitleDownloading = options.MetadataRefreshMode == MetadataRefreshMode.Default || options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh; - var subtitleOptions = _config.GetConfiguration<SubtitleOptions>("subtitles"); - var libraryOptions = _libraryManager.GetLibraryOptions(video); - string[] subtitleDownloadLanguages; - bool skipIfEmbeddedSubtitlesPresent; - bool skipIfAudioTrackMatches; - bool requirePerfectMatch; - bool enabled; - - if (libraryOptions.SubtitleDownloadLanguages is null) - { - subtitleDownloadLanguages = subtitleOptions.DownloadLanguages; - skipIfEmbeddedSubtitlesPresent = subtitleOptions.SkipIfEmbeddedSubtitlesPresent; - skipIfAudioTrackMatches = subtitleOptions.SkipIfAudioTrackMatches; - requirePerfectMatch = subtitleOptions.RequirePerfectMatch; - enabled = (subtitleOptions.DownloadEpisodeSubtitles && - video is Episode) || - (subtitleOptions.DownloadMovieSubtitles && - video is Movie); - } - else - { - subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages; - skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; - skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; - requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch; - enabled = true; - } - - if (enableSubtitleDownloading && enabled) + if (enableSubtitleDownloading && libraryOptions.SubtitleDownloadLanguages is not null) { var downloadedLanguages = await new SubtitleDownloader( _logger, _subtitleManager).DownloadSubtitles( video, currentStreams.Concat(externalSubtitleStreams).ToList(), - skipIfEmbeddedSubtitlesPresent, - skipIfAudioTrackMatches, - requirePerfectMatch, - subtitleDownloadLanguages, + libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent, + libraryOptions.SkipSubtitlesIfAudioTrackMatches, + libraryOptions.RequirePerfectSubtitleMatch, + libraryOptions.SubtitleDownloadLanguages, libraryOptions.DisabledSubtitleFetchers, libraryOptions.SubtitleFetcherOrder, true, diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs index 1134baf92d..f1582febf2 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs @@ -8,14 +8,13 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; -using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Providers; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; @@ -28,19 +27,24 @@ namespace MediaBrowser.Providers.MediaInfo private readonly ISubtitleManager _subtitleManager; private readonly ILogger<SubtitleScheduledTask> _logger; private readonly ILocalizationManager _localization; + private readonly ISubtitleProvider[] _subtitleProviders; public SubtitleScheduledTask( ILibraryManager libraryManager, IServerConfigurationManager config, ISubtitleManager subtitleManager, ILogger<SubtitleScheduledTask> logger, - ILocalizationManager localization) + ILocalizationManager localization, + IEnumerable<ISubtitleProvider> subtitleProviders) { _libraryManager = libraryManager; _config = config; _subtitleManager = subtitleManager; _logger = logger; _localization = localization; + _subtitleProviders = subtitleProviders + .OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0) + .ToArray(); } public string Name => _localization.GetLocalizedString("TaskDownloadMissingSubtitles"); @@ -57,16 +61,9 @@ namespace MediaBrowser.Providers.MediaInfo public bool IsLogged => true; - private SubtitleOptions GetOptions() - { - return _config.GetConfiguration<SubtitleOptions>("subtitles"); - } - /// <inheritdoc /> public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { - var options = GetOptions(); - var types = new[] { BaseItemKind.Episode, BaseItemKind.Movie }; var dict = new Dictionary<Guid, BaseItem>(); @@ -81,17 +78,20 @@ namespace MediaBrowser.Providers.MediaInfo if (libraryOptions.SubtitleDownloadLanguages is null) { - subtitleDownloadLanguages = options.DownloadLanguages; - skipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent; - skipIfAudioTrackMatches = options.SkipIfAudioTrackMatches; + // Skip this library if subtitle download languages are not configured + continue; } - else + + if (_subtitleProviders.All(provider => libraryOptions.DisabledSubtitleFetchers.Contains(provider.Name, StringComparer.OrdinalIgnoreCase))) { - subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages; - skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; - skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; + // Skip this library if all subtitle providers are disabled + continue; } + subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages; + skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; + skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; + foreach (var lang in subtitleDownloadLanguages) { var query = new InternalItemsQuery @@ -144,7 +144,7 @@ namespace MediaBrowser.Providers.MediaInfo try { - await DownloadSubtitles(video as Video, options, cancellationToken).ConfigureAwait(false); + await DownloadSubtitles(video as Video, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -160,7 +160,7 @@ namespace MediaBrowser.Providers.MediaInfo } } - private async Task<bool> DownloadSubtitles(Video video, SubtitleOptions options, CancellationToken cancellationToken) + private async Task<bool> DownloadSubtitles(Video video, CancellationToken cancellationToken) { var mediaStreams = video.GetMediaStreams(); @@ -173,19 +173,15 @@ namespace MediaBrowser.Providers.MediaInfo if (libraryOptions.SubtitleDownloadLanguages is null) { - subtitleDownloadLanguages = options.DownloadLanguages; - skipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent; - skipIfAudioTrackMatches = options.SkipIfAudioTrackMatches; - requirePerfectMatch = options.RequirePerfectMatch; - } - else - { - subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages; - skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; - skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; - requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch; + // Subtitle downloading is not configured for this library + return true; } + subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages; + skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; + skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; + requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch; + var downloadedLanguages = await new SubtitleDownloader( _logger, _subtitleManager).DownloadSubtitles( diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs index 00bd96282c..d8cb6b4b24 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs @@ -125,7 +125,9 @@ namespace MediaBrowser.Providers.Plugins.AudioDb if (string.IsNullOrWhiteSpace(overview)) { - overview = result.strBiographyEN; + overview = string.IsNullOrWhiteSpace(result.strBiographyEN) + ? result.strBiography + : result.strBiographyEN; } item.Overview = (overview ?? string.Empty).StripHtml(); @@ -224,6 +226,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public string strTwitter { get; set; } + public string strBiography { get; set; } + public string strBiographyEN { get; set; } public string strBiographyDE { get; set; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs index 1323d2604a..9df21596c5 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs @@ -22,7 +22,7 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz; /// <summary> /// MusicBrainz artist provider. /// </summary> -public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>, IDisposable +public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>, IDisposable, IHasOrder { private readonly ILogger<MusicBrainzArtistProvider> _logger; private Query _musicBrainzQuery; @@ -42,6 +42,10 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, Ar /// <inheritdoc /> public string Name => "MusicBrainz"; + /// <inheritdoc /> + /// Runs first to populate the MusicBrainz artist ID used by downstream providers. + public int Order => 0; + private void ReloadConfig(object? sender, BasePluginConfiguration e) { var configuration = (PluginConfiguration)e; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs index da63df8e29..2b52abcb5b 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/SqliteDatabaseProvider.cs @@ -61,7 +61,7 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider var customOptions = databaseConfiguration.CustomProviderOptions?.Options; var sqliteConnectionBuilder = new SqliteConnectionStringBuilder(); - sqliteConnectionBuilder.DataSource = Path.Combine(_applicationPaths.DataPath, "jellyfin.db"); + sqliteConnectionBuilder.DataSource = GetOption(customOptions, "path", e => e, () => Path.Combine(_applicationPaths.DataPath, "jellyfin.db")); sqliteConnectionBuilder.Cache = GetOption(customOptions, "cache", Enum.Parse<SqliteCacheMode>, () => SqliteCacheMode.Default); sqliteConnectionBuilder.Pooling = GetOption(customOptions, "pooling", e => e.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase), () => true); sqliteConnectionBuilder.DefaultTimeout = GetOption(customOptions, "command-timeout", int.Parse, () => 30); diff --git a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs index 7938b7a6e4..318c3a2d36 100644 --- a/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs +++ b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -62,21 +60,21 @@ namespace Jellyfin.LiveTv.Listings _logger.LogInformation("xmltv path: {Path}", info.Path); string cacheFilename = info.Id + ".xml"; - string cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename); - - if (File.Exists(cacheFile) && File.GetLastWriteTimeUtc(cacheFile) >= DateTime.UtcNow.Subtract(_maxCacheAge)) - { - return cacheFile; - } + string cacheDir = Path.Join(_config.ApplicationPaths.CachePath, "xmltv"); + string cacheFile = Path.Join(cacheDir, cacheFilename); - // Must check if file exists as parent directory may not exist. if (File.Exists(cacheFile)) { + if (File.GetLastWriteTimeUtc(cacheFile) >= DateTime.UtcNow.Subtract(_maxCacheAge)) + { + return cacheFile; + } + File.Delete(cacheFile); } else { - Directory.CreateDirectory(Path.GetDirectoryName(cacheFile)); + Directory.CreateDirectory(cacheDir); } if (info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) @@ -154,33 +152,37 @@ namespace Jellyfin.LiveTv.Listings private static ProgramInfo GetProgramInfo(XmlTvProgram program, ListingsProviderInfo info) { - string episodeTitle = program.Episode.Title; + string? episodeTitle = program.Episode?.Title; var programCategories = program.Categories.Where(c => !string.IsNullOrWhiteSpace(c)).ToList(); + var imageUrl = program.Icons.FirstOrDefault()?.Source; + var rating = program.Ratings.FirstOrDefault()?.Value; + var starRating = program.StarRatings?.FirstOrDefault()?.StarRating; var programInfo = new ProgramInfo { ChannelId = program.ChannelId, EndDate = program.EndDate.UtcDateTime, - EpisodeNumber = program.Episode.Episode, + EpisodeNumber = program.Episode?.Episode, EpisodeTitle = episodeTitle, Genres = programCategories, StartDate = program.StartDate.UtcDateTime, Name = program.Title, Overview = program.Description, ProductionYear = program.CopyrightDate?.Year, - SeasonNumber = program.Episode.Series, - IsSeries = program.Episode.Episode is not null, + SeasonNumber = program.Episode?.Series, + IsSeries = program.Episode?.Episode is not null, IsRepeat = program.IsPreviouslyShown && !program.IsNew, IsPremiere = program.Premiere is not null, + IsLive = program.IsLive, IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), IsMovie = programCategories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), IsNews = programCategories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), IsSports = programCategories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), - ImageUrl = string.IsNullOrEmpty(program.Icon?.Source) ? null : program.Icon.Source, - HasImage = !string.IsNullOrEmpty(program.Icon?.Source), - OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value, - CommunityRating = program.StarRating, - SeriesId = program.Episode.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture) + ImageUrl = string.IsNullOrEmpty(imageUrl) ? null : imageUrl, + HasImage = !string.IsNullOrEmpty(imageUrl), + OfficialRating = string.IsNullOrEmpty(rating) ? null : rating, + CommunityRating = starRating is null ? null : (float)starRating.Value, + SeriesId = program.Episode?.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture) }; if (string.IsNullOrWhiteSpace(program.ProgramId)) @@ -261,7 +263,7 @@ namespace Jellyfin.LiveTv.Listings { Id = c.Id, Name = c.DisplayName, - ImageUrl = string.IsNullOrEmpty(c.Icon?.Source) ? null : c.Icon.Source, + ImageUrl = string.IsNullOrEmpty(c.Icons.FirstOrDefault()?.Source) ? null : c.Icons.FirstOrDefault()!.Source, Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number }).ToList(); } diff --git a/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs index e95df16354..60ed740609 100644 --- a/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs +++ b/tests/Jellyfin.Api.Tests/Controllers/UserControllerTests.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; -using AutoFixture.Xunit2; +using AutoFixture.Xunit3; using Jellyfin.Api.Controllers; using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Common.Net; diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index 6b84c4438f..253eab9d79 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -3,15 +3,16 @@ <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> <PropertyGroup> <ProjectGuid>{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}</ProjectGuid> + <OutputType>Exe</OutputType> </PropertyGroup> <ItemGroup> <PackageReference Include="AutoFixture" /> <PackageReference Include="AutoFixture.AutoMoq" /> - <PackageReference Include="AutoFixture.Xunit2" /> + <PackageReference Include="AutoFixture.Xunit3" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" /> <PackageReference Include="Microsoft.NET.Test.Sdk" /> - <PackageReference Include="xunit" /> + <PackageReference Include="xunit.v3" /> <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj index 8fef7fde05..f01d522e11 100644 --- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj +++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj @@ -3,17 +3,18 @@ <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> <PropertyGroup> <ProjectGuid>{DF194677-DFD3-42AF-9F75-D44D5A416478}</ProjectGuid> + <OutputType>Exe</OutputType> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" /> - <PackageReference Include="xunit" /> + <PackageReference Include="xunit.v3" /> <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="coverlet.collector" /> - <PackageReference Include="FsCheck.Xunit" /> + <PackageReference Include="FsCheck.Xunit.v3" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj index 54d93b48cf..7db94f9c81 100644 --- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj +++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj @@ -3,12 +3,13 @@ <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> <PropertyGroup> <ProjectGuid>{462584F7-5023-4019-9EAC-B98CA458C0A0}</ProjectGuid> + <OutputType>Exe</OutputType> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="Moq" /> - <PackageReference Include="xunit" /> + <PackageReference Include="xunit.v3" /> <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> diff --git a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj index 0364898298..6921fc8a97 100644 --- a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj +++ b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj @@ -1,8 +1,12 @@ <Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <OutputType>Exe</OutputType> + </PropertyGroup> + <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" /> - <PackageReference Include="xunit" /> + <PackageReference Include="xunit.v3" /> <PackageReference Include="xunit.runner.visualstudio"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> @@ -11,7 +15,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> - <PackageReference Include="FsCheck.Xunit" /> + <PackageReference Include="FsCheck.Xunit.v3" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj b/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj index bdf6bc383a..a9b19e0104 100644 --- a/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj +++ b/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj @@ -1,6 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> + <OutputType>Exe</OutputType> </PropertyGroup> <ItemGroup> @@ -14,12 +15,11 @@ <PackageReference Include="AutoFixture.AutoMoq" /> <PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="Moq" /> - <PackageReference Include="xunit" /> + <PackageReference Include="xunit.v3" /> <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Xunit.SkippableFact" /> <PackageReference Include="coverlet.collector" /> </ItemGroup> diff --git a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj index eab003715c..47a116ee42 100644 --- a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj @@ -1,8 +1,12 @@ <Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <OutputType>Exe</OutputType> + </PropertyGroup> + <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" /> - <PackageReference Include="xunit" /> + <PackageReference Include="xunit.v3" /> <PackageReference Include="xunit.runner.visualstudio"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> diff --git a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj index 894bec6aa5..9a58c697f0 100644 --- a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj @@ -1,8 +1,12 @@ <Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <OutputType>Exe</OutputType> + </PropertyGroup> + <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" /> - <PackageReference Include="xunit" /> + <PackageReference Include="xunit.v3" /> <PackageReference Include="xunit.runner.visualstudio"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj index 6b703e7416..c7065c670a 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj @@ -3,6 +3,7 @@ <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> <PropertyGroup> <ProjectGuid>{28464062-0939-4AA7-9F7B-24DDDA61A7C0}</ProjectGuid> + <OutputType>Exe</OutputType> </PropertyGroup> <ItemGroup> @@ -14,11 +15,11 @@ <ItemGroup> <PackageReference Include="AutoFixture" /> <PackageReference Include="AutoFixture.AutoMoq" /> - <PackageReference Include="AutoFixture.Xunit2" /> + <PackageReference Include="AutoFixture.Xunit3" /> <PackageReference Include="coverlet.collector" /> <PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="Moq" /> - <PackageReference Include="xunit" /> + <PackageReference Include="xunit.v3" /> <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> 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); + } + } +} diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 2c1080ffe3..8269ae58cd 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -617,5 +617,60 @@ namespace Jellyfin.Model.Tests return (path, query, filename, extension); } + + [Theory] + // EnableSubtitleExtraction = false, internal subtitles + [InlineData("srt", "srt", false, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)] + [InlineData("srt", "srt", false, false, PlayMethod.DirectPlay, SubtitleDeliveryMethod.External)] + [InlineData("pgssub", "pgssub", false, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)] + [InlineData("pgssub", "pgssub", false, false, PlayMethod.DirectPlay, SubtitleDeliveryMethod.External)] + [InlineData("pgssub", "srt", false, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)] + // EnableSubtitleExtraction = false, external subtitles + [InlineData("srt", "srt", false, true, PlayMethod.Transcode, SubtitleDeliveryMethod.External)] + // EnableSubtitleExtraction = true, internal subtitles + [InlineData("srt", "srt", true, false, PlayMethod.Transcode, SubtitleDeliveryMethod.External)] + [InlineData("pgssub", "pgssub", true, false, PlayMethod.Transcode, SubtitleDeliveryMethod.External)] + [InlineData("pgssub", "pgssub", true, false, PlayMethod.DirectPlay, SubtitleDeliveryMethod.External)] + [InlineData("pgssub", "srt", true, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)] + // EnableSubtitleExtraction = true, external subtitles + [InlineData("srt", "srt", true, true, PlayMethod.Transcode, SubtitleDeliveryMethod.External)] + public void GetSubtitleProfile_RespectsExtractionSetting( + string codec, + string profileFormat, + bool enableSubtitleExtraction, + bool isExternal, + PlayMethod playMethod, + SubtitleDeliveryMethod expectedMethod) + { + var mediaSource = new MediaSourceInfo(); + var subtitleStream = new MediaStream + { + Type = MediaStreamType.Subtitle, + Index = 0, + IsExternal = isExternal, + Path = isExternal ? "/media/sub." + codec : null, + Codec = codec, + SupportsExternalStream = MediaStream.IsTextFormat(codec) + }; + + var subtitleProfiles = new[] + { + new SubtitleProfile { Format = profileFormat, Method = SubtitleDeliveryMethod.External } + }; + + var transcoderSupport = new Mock<ITranscoderSupport>(); + transcoderSupport.Setup(t => t.CanExtractSubtitles(It.IsAny<string>())).Returns(enableSubtitleExtraction); + + var result = StreamBuilder.GetSubtitleProfile( + mediaSource, + subtitleStream, + subtitleProfiles, + playMethod, + transcoderSupport.Object, + null, + null); + + Assert.Equal(expectedMethod, result.Method); + } } } diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj index 8345b610e5..9e2a9a8873 100644 --- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj +++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj @@ -1,15 +1,19 @@ <Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <OutputType>Exe</OutputType> + </PropertyGroup> + <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="Moq" /> - <PackageReference Include="xunit" /> + <PackageReference Include="xunit.v3" /> <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="coverlet.collector" /> - <PackageReference Include="FsCheck.Xunit" /> + <PackageReference Include="FsCheck.Xunit.v3" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj index 7c26494487..1f3e42077f 100644 --- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj +++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj @@ -3,12 +3,13 @@ <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> <PropertyGroup> <ProjectGuid>{3998657B-1CCC-49DD-A19F-275DC8495F57}</ProjectGuid> + <OutputType>Exe</OutputType> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="Moq" /> - <PackageReference Include="xunit" /> + <PackageReference Include="xunit.v3" /> <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj index 2d7f112109..09ba120a5e 100644 --- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj +++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj @@ -3,17 +3,18 @@ <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> <PropertyGroup> <ProjectGuid>{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}</ProjectGuid> + <OutputType>Exe</OutputType> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" /> - <PackageReference Include="xunit" /> + <PackageReference Include="xunit.v3" /> <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="coverlet.collector" /> - <PackageReference Include="FsCheck.Xunit" /> + <PackageReference Include="FsCheck.Xunit.v3" /> <PackageReference Include="Moq" /> </ItemGroup> diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj index 1263043a51..990544b5a8 100644 --- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj +++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj @@ -1,5 +1,9 @@ <Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <OutputType>Exe</OutputType> + </PropertyGroup> + <ItemGroup> <None Include="Test Data\**\*.*"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> @@ -9,10 +13,10 @@ <ItemGroup> <PackageReference Include="AutoFixture" /> <PackageReference Include="AutoFixture.AutoMoq" /> - <PackageReference Include="AutoFixture.Xunit2" /> + <PackageReference Include="AutoFixture.Xunit3" /> <PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="Moq" /> - <PackageReference Include="xunit" /> + <PackageReference Include="xunit.v3" /> <PackageReference Include="xunit.runner.visualstudio"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> diff --git a/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs b/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs index 6997b51ac8..c06279af2d 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs @@ -25,12 +25,12 @@ public class ManagedFileSystemTests public void MoveDirectory_SameFileSystem_Correct() => MoveDirectoryInternal(); - [SkippableFact] + [Fact] public void MoveDirectory_DifferentFileSystem_Correct() { const string DestinationParent = "/dev/shm"; - Skip.IfNot(Directory.Exists(DestinationParent)); + Assert.SkipUnless(Directory.Exists(DestinationParent), $"{DestinationParent} is not available"); MoveDirectoryInternal(DestinationParent); } @@ -57,7 +57,7 @@ public class ManagedFileSystemTests Directory.Delete(destinationDir, true); } - [SkippableTheory] + [Theory] [InlineData("/Volumes/Library/Sample/Music/Playlists/", "../Beethoven/Misc/Moonlight Sonata.mp3", "/Volumes/Library/Sample/Music/Beethoven/Misc/Moonlight Sonata.mp3")] [InlineData("/Volumes/Library/Sample/Music/Playlists/", "../../Beethoven/Misc/Moonlight Sonata.mp3", "/Volumes/Library/Sample/Beethoven/Misc/Moonlight Sonata.mp3")] [InlineData("/Volumes/Library/Sample/Music/Playlists/", "Beethoven/Misc/Moonlight Sonata.mp3", "/Volumes/Library/Sample/Music/Playlists/Beethoven/Misc/Moonlight Sonata.mp3")] @@ -67,13 +67,13 @@ public class ManagedFileSystemTests string filePath, string expectedAbsolutePath) { - Skip.If(OperatingSystem.IsWindows()); + Assert.SkipWhen(OperatingSystem.IsWindows(), "Unix-only test"); var generatedPath = _sut.MakeAbsolutePath(folderPath, filePath); Assert.Equal(expectedAbsolutePath, generatedPath); } - [SkippableTheory] + [Theory] [InlineData(@"C:\\Volumes\Library\Sample\Music\Playlists\", @"..\Beethoven\Misc\Moonlight Sonata.mp3", @"C:\Volumes\Library\Sample\Music\Beethoven\Misc\Moonlight Sonata.mp3")] [InlineData(@"C:\\Volumes\Library\Sample\Music\Playlists\", @"..\..\Beethoven\Misc\Moonlight Sonata.mp3", @"C:\Volumes\Library\Sample\Beethoven\Misc\Moonlight Sonata.mp3")] [InlineData(@"C:\\Volumes\Library\Sample\Music\Playlists\", @"Beethoven\Misc\Moonlight Sonata.mp3", @"C:\Volumes\Library\Sample\Music\Playlists\Beethoven\Misc\Moonlight Sonata.mp3")] @@ -83,7 +83,7 @@ public class ManagedFileSystemTests string filePath, string expectedAbsolutePath) { - Skip.IfNot(OperatingSystem.IsWindows()); + Assert.SkipUnless(OperatingSystem.IsWindows(), "Windows-only test"); var generatedPath = _sut.MakeAbsolutePath(folderPath, filePath); @@ -100,10 +100,10 @@ public class ManagedFileSystemTests Assert.Equal(expectedFileName, _sut.GetValidFilename(filename)); } - [SkippableFact] + [Fact] public void GetFileInfo_DanglingSymlink_ExistsFalse() { - Skip.If(OperatingSystem.IsWindows()); + Assert.SkipWhen(OperatingSystem.IsWindows(), "Unix-only test"); string testFileDir = Path.Combine(Path.GetTempPath(), "jellyfin-test-data"); string testFileName = Path.Combine(testFileDir, Path.GetRandomFileName() + "-danglingsym.link"); diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index 4e2604e6e1..958ffb8b6e 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -3,6 +3,7 @@ <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> <PropertyGroup> <ProjectGuid>{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}</ProjectGuid> + <OutputType>Exe</OutputType> </PropertyGroup> <ItemGroup> @@ -16,12 +17,11 @@ <PackageReference Include="AutoFixture.AutoMoq" /> <PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="Moq" /> - <PackageReference Include="xunit" /> + <PackageReference Include="xunit.v3" /> <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Xunit.SkippableFact" /> <PackageReference Include="coverlet.collector" /> </ItemGroup> diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index e60522bf78..5bcfc580ff 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -22,7 +22,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization }); var countries = localizationManager.GetCountries().ToList(); - Assert.Equal(139, countries.Count); + Assert.Equal(140, countries.Count); var germany = countries.FirstOrDefault(x => x.Name.Equals("DE", StringComparison.Ordinal)); Assert.NotNull(germany); @@ -41,7 +41,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization await localizationManager.LoadAll(); var cultures = localizationManager.GetCultures().ToList(); - Assert.Equal(194, cultures.Count); + Assert.Equal(496, cultures.Count); var germany = cultures.FirstOrDefault(x => x.TwoLetterISOLanguageName.Equals("de", StringComparison.Ordinal)); Assert.NotNull(germany); @@ -99,6 +99,25 @@ namespace Jellyfin.Server.Implementations.Tests.Localization Assert.Contains("ger", germany.ThreeLetterISOLanguageNames); } + [Theory] + [InlineData("mul", "Multiple languages")] + [InlineData("und", "Undetermined")] + [InlineData("mis", "Uncoded languages")] + [InlineData("zxx", "No linguistic content; Not applicable")] + public async Task FindLanguageInfo_ISO6392Only_Success(string code, string expectedDisplayName) + { + var localizationManager = Setup(new ServerConfiguration + { + UICulture = "en-US" + }); + await localizationManager.LoadAll(); + + var culture = localizationManager.FindLanguageInfo(code); + Assert.NotNull(culture); + Assert.Equal(expectedDisplayName, culture.DisplayName); + Assert.Equal(code, culture.ThreeLetterISOLanguageName); + } + [Fact] public async Task GetParentalRatings_Default_Success() { diff --git a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs index 3d8ea15a31..ede9e61536 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Plugins/PluginManagerTests.cs @@ -192,13 +192,13 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins }; var metafilePath = Path.Combine(_pluginPath, "meta.json"); - await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options)); + await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options), TestContext.Current.CancellationToken); var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0)); await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active); - var resultBytes = await File.ReadAllBytesAsync(metafilePath); + var resultBytes = await File.ReadAllBytesAsync(metafilePath, TestContext.Current.CancellationToken); var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options); Assert.NotNull(result); @@ -232,7 +232,7 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active); var metafilePath = Path.Combine(_pluginPath, "meta.json"); - var resultBytes = await File.ReadAllBytesAsync(metafilePath); + var resultBytes = await File.ReadAllBytesAsync(metafilePath, TestContext.Current.CancellationToken); var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options); Assert.NotNull(result); @@ -252,13 +252,13 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins }; var metafilePath = Path.Combine(_pluginPath, "meta.json"); - await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options)); + await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options), TestContext.Current.CancellationToken); var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0)); await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active); - var resultBytes = await File.ReadAllBytesAsync(metafilePath); + var resultBytes = await File.ReadAllBytesAsync(metafilePath, TestContext.Current.CancellationToken); var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options); Assert.NotNull(result); @@ -278,13 +278,13 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins }; var metafilePath = Path.Combine(_pluginPath, "meta.json"); - await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options)); + await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options), TestContext.Current.CancellationToken); var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0)); await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active); - var resultBytes = await File.ReadAllBytesAsync(metafilePath); + var resultBytes = await File.ReadAllBytesAsync(metafilePath, TestContext.Current.CancellationToken); var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options); Assert.NotNull(result); diff --git a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs index f58a3276ba..92e10c9f92 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Updates/InstallationManagerTests.cs @@ -51,7 +51,8 @@ namespace Jellyfin.Server.Implementations.Tests.Updates PackageInfo[] packages = await _installationManager.GetPackages( "Jellyfin Stable", "https://repo.jellyfin.org/files/plugin/manifest.json", - false); + false, + TestContext.Current.CancellationToken); Assert.Equal(25, packages.Length); } @@ -62,7 +63,8 @@ namespace Jellyfin.Server.Implementations.Tests.Updates PackageInfo[] packages = await _installationManager.GetPackages( "Jellyfin Stable", "https://repo.jellyfin.org/files/plugin/manifest.json", - false); + false, + TestContext.Current.CancellationToken); packages = _installationManager.FilterPackages(packages, "Anime").ToArray(); Assert.Single(packages); @@ -74,7 +76,8 @@ namespace Jellyfin.Server.Implementations.Tests.Updates PackageInfo[] packages = await _installationManager.GetPackages( "Jellyfin Stable", "https://repo.jellyfin.org/files/plugin/manifest.json", - false); + false, + TestContext.Current.CancellationToken); packages = _installationManager.FilterPackages(packages, id: new Guid("a4df60c5-6ab4-412a-8f79-2cab93fb2bc5")).ToArray(); Assert.Single(packages); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs index 96ca96558d..ef084430e8 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/ActivityLogControllerTests.cs @@ -21,7 +21,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("System/ActivityLog/Entries"); + var response = await client.GetAsync("System/ActivityLog/Entries", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs index 8761cf69bc..1973af3f25 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs @@ -25,13 +25,13 @@ namespace Jellyfin.Server.Integration.Tests var client = _factory.CreateClient(); // Act - var response = await client.GetAsync("/Branding/Configuration"); + var response = await client.GetAsync("/Branding/Configuration", TestContext.Current.CancellationToken); // Assert 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<BrandingOptions>(); + await response.Content.ReadFromJsonAsync<BrandingOptions>(TestContext.Current.CancellationToken); } [Theory] @@ -43,7 +43,7 @@ namespace Jellyfin.Server.Integration.Tests var client = _factory.CreateClient(); // Act - var response = await client.GetAsync(url); + var response = await client.GetAsync(url, TestContext.Current.CancellationToken); // Assert Assert.True(response.IsSuccessStatusCode); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs index d92dbbd732..32bdc57265 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs @@ -27,7 +27,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { var client = _factory.CreateClient(); - var response = await client.GetAsync("web/ConfigurationPage?name=ThisPageDoesntExists"); + var response = await client.GetAsync("web/ConfigurationPage?name=ThisPageDoesntExists", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -37,12 +37,12 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { var client = _factory.CreateClient(); - var response = await client.GetAsync("/web/ConfigurationPage?name=TestPlugin"); + var response = await client.GetAsync("/web/ConfigurationPage?name=TestPlugin", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Text.Html, response.Content.Headers.ContentType?.MediaType); StreamReader reader = new StreamReader(typeof(TestPlugin).Assembly.GetManifestResourceStream("Jellyfin.Server.Integration.Tests.TestPage.html")!); - Assert.Equal(await response.Content.ReadAsStringAsync(), await reader.ReadToEndAsync()); + Assert.Equal(await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken), await reader.ReadToEndAsync(TestContext.Current.CancellationToken)); } [Fact] @@ -50,7 +50,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { var client = _factory.CreateClient(); - var response = await client.GetAsync("/web/ConfigurationPage?name=BrokenPage"); + var response = await client.GetAsync("/web/ConfigurationPage?name=BrokenPage", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -61,11 +61,11 @@ namespace Jellyfin.Server.Integration.Tests.Controllers var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("/web/ConfigurationPages"); + var response = await client.GetAsync("/web/ConfigurationPages", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - _ = await response.Content.ReadFromJsonAsync<ConfigurationPageInfo[]>(_jsonOptions); + _ = await response.Content.ReadFromJsonAsync<ConfigurationPageInfo[]>(_jsonOptions, TestContext.Current.CancellationToken); // TODO: check content } @@ -75,13 +75,13 @@ namespace Jellyfin.Server.Integration.Tests.Controllers var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("/web/ConfigurationPages?enableInMainMenu=true"); + var response = await client.GetAsync("/web/ConfigurationPages?enableInMainMenu=true", TestContext.Current.CancellationToken); 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 data = await response.Content.ReadFromJsonAsync<ConfigurationPageInfo[]>(_jsonOptions); + var data = await response.Content.ReadFromJsonAsync<ConfigurationPageInfo[]>(_jsonOptions, TestContext.Current.CancellationToken); Assert.NotNull(data); Assert.Empty(data); } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs index 64b9bd8e16..165e269814 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs @@ -28,7 +28,7 @@ public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFact var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("Items"); + var response = await client.GetAsync("Items", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } @@ -40,7 +40,7 @@ public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFact var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())); + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid()), TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -55,9 +55,9 @@ public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFact var userDto = await AuthHelper.GetUserDtoAsync(client); - var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id)); + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id), TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var items = await response.Content.ReadFromJsonAsync<QueryResult<BaseItemDto>>(_jsonOptions); + var items = await response.Content.ReadFromJsonAsync<QueryResult<BaseItemDto>>(_jsonOptions, TestContext.Current.CancellationToken); Assert.NotNull(items); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs index 6881a92101..edbb46b34c 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryControllerTests.cs @@ -34,7 +34,7 @@ public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFa var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())); + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid()), TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -45,7 +45,7 @@ public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFa { var client = _factory.CreateClient(); - var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())); + var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid()), TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } @@ -57,7 +57,7 @@ public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFa var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid())); + var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid()), TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs index 36f1b726da..2de6408cc6 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LibraryStructureControllerTests.cs @@ -9,11 +9,11 @@ using Jellyfin.Extensions.Json; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using Xunit; -using Xunit.Priority; +using Xunit.v3.Priority; namespace Jellyfin.Server.Integration.Tests.Controllers; -[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)] +[TestCaseOrderer(typeof(PriorityOrderer))] public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinApplicationFactory> { private readonly JellyfinApplicationFactory _factory; @@ -40,7 +40,7 @@ public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinAppl } }; - using var response = await client.PostAsJsonAsync("Library/VirtualFolders?name=test&refreshLibrary=true", body, _jsonOptions); + using var response = await client.PostAsJsonAsync("Library/VirtualFolders?name=test&refreshLibrary=true", body, _jsonOptions, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); } @@ -57,7 +57,7 @@ public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinAppl LibraryOptions = new LibraryOptions() }; - using var response = await client.PostAsJsonAsync("Library/VirtualFolders/LibraryOptions", body, _jsonOptions); + using var response = await client.PostAsJsonAsync("Library/VirtualFolders/LibraryOptions", body, _jsonOptions, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -76,16 +76,16 @@ public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinAppl } }; - using var createResponse = await client.PostAsJsonAsync("Library/VirtualFolders?name=test&refreshLibrary=true", createBody, _jsonOptions); + using var createResponse = await client.PostAsJsonAsync("Library/VirtualFolders?name=test&refreshLibrary=true", createBody, _jsonOptions, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NoContent, createResponse.StatusCode); - await Task.Delay(2000).ConfigureAwait(true); + await Task.Delay(2000, TestContext.Current.CancellationToken).ConfigureAwait(true); - using var response = await client.GetAsync("Library/VirtualFolders"); + using var response = await client.GetAsync("Library/VirtualFolders", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var library = await response.Content.ReadFromJsonAsAsyncEnumerable<VirtualFolderInfo>(_jsonOptions) - .FirstOrDefaultAsync(x => string.Equals(x?.Name, "test", StringComparison.Ordinal)); + var library = await response.Content.ReadFromJsonAsAsyncEnumerable<VirtualFolderInfo>(_jsonOptions, TestContext.Current.CancellationToken) + .FirstOrDefaultAsync(x => string.Equals(x?.Name, "test", StringComparison.Ordinal), TestContext.Current.CancellationToken); Assert.NotNull(library); var options = library.LibraryOptions; @@ -99,7 +99,7 @@ public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinAppl LibraryOptions = options }; - using var response2 = await client.PostAsJsonAsync("Library/VirtualFolders/LibraryOptions", body, _jsonOptions); + using var response2 = await client.PostAsJsonAsync("Library/VirtualFolders/LibraryOptions", body, _jsonOptions, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NoContent, response2.StatusCode); } @@ -110,7 +110,7 @@ public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinAppl var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var response = await client.DeleteAsync("Library/VirtualFolders?name=doesntExist"); + using var response = await client.DeleteAsync("Library/VirtualFolders?name=doesntExist", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -121,7 +121,7 @@ public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinAppl var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var response = await client.DeleteAsync("Library/VirtualFolders?name=test&refreshLibrary=true"); + using var response = await client.DeleteAsync("Library/VirtualFolders?name=test&refreshLibrary=true", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/LiveTvControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/LiveTvControllerTests.cs index dd971fa87b..8ca9fb899e 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/LiveTvControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/LiveTvControllerTests.cs @@ -32,7 +32,7 @@ public sealed class LiveTvControllerTests : IClassFixture<JellyfinApplicationFac Url = "Test Data/dummy.m3u8" }; - var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions); + var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } @@ -49,12 +49,12 @@ public sealed class LiveTvControllerTests : IClassFixture<JellyfinApplicationFac Url = "Test Data/dummy.m3u8" }; - var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions); + var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions, TestContext.Current.CancellationToken); 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>(); + var responseBody = await response.Content.ReadFromJsonAsync<TunerHostInfo>(TestContext.Current.CancellationToken); Assert.NotNull(responseBody); Assert.Equal(body.Type, responseBody.Type); Assert.Equal(body.Url, responseBody.Url); @@ -72,7 +72,7 @@ public sealed class LiveTvControllerTests : IClassFixture<JellyfinApplicationFac Url = "Test Data/dummy.m3u8" }; - var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions); + var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -89,7 +89,7 @@ public sealed class LiveTvControllerTests : IClassFixture<JellyfinApplicationFac Url = "thisgoesnowhere" }; - var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions); + var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaInfoControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaInfoControllerTests.cs index abc8b60099..194566bbf0 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaInfoControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaInfoControllerTests.cs @@ -22,7 +22,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("Playback/BitrateTest"); + var response = await client.GetAsync("Playback/BitrateTest", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Octet, response.Content.Headers.ContentType?.MediaType); @@ -36,7 +36,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture)); + var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture), TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Octet, response.Content.Headers.ContentType?.MediaType); @@ -53,7 +53,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture)); + var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture), TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs index 6699c68346..82fdf715f8 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MediaStructureControllerTests.cs @@ -29,7 +29,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); using var postContent = new ByteArrayContent(Array.Empty<byte>()); - var response = await client.PostAsync("Library/VirtualFolders/Name?name=+&newName=test", postContent); + var response = await client.PostAsync("Library/VirtualFolders/Name?name=+&newName=test", postContent, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } @@ -41,7 +41,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); using var postContent = new ByteArrayContent(Array.Empty<byte>()); - var response = await client.PostAsync("Library/VirtualFolders/Name?name=test&newName=+", postContent); + var response = await client.PostAsync("Library/VirtualFolders/Name?name=test&newName=+", postContent, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } @@ -53,7 +53,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); using var postContent = new ByteArrayContent(Array.Empty<byte>()); - var response = await client.PostAsync("Library/VirtualFolders/Name?name=doesnt+exist&newName=test", postContent); + var response = await client.PostAsync("Library/VirtualFolders/Name?name=doesnt+exist&newName=test", postContent, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -70,7 +70,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Path = "/this/path/doesnt/exist" }; - var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths", data, _jsonOptions); + var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths", data, _jsonOptions, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -87,7 +87,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers PathInfo = new MediaPathInfo("test") }; - var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths/Update", data, _jsonOptions); + var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths/Update", data, _jsonOptions, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } @@ -98,7 +98,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=+"); + var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=+", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } @@ -109,7 +109,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=none&path=%2Fthis%2Fpath%2Fdoesnt%2Fexist"); + var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=none&path=%2Fthis%2Fpath%2Fdoesnt%2Fexist", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs index f9982cf12b..3e14850613 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/MusicGenreControllerTests.cs @@ -20,7 +20,7 @@ public sealed class MusicGenreControllerTests : IClassFixture<JellyfinApplicatio var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("MusicGenres/Fake-MusicGenre"); + var response = await client.GetAsync("MusicGenres/Fake-MusicGenre", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs index c673773ffc..361edf3eb7 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs @@ -20,7 +20,7 @@ public class PersonsControllerTests : IClassFixture<JellyfinApplicationFactory> var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var response = await client.GetAsync($"Persons/DoesntExist"); + using var response = await client.GetAsync($"Persons/DoesntExist", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs index 3b9ed17787..db271fc5cd 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/PlaystateControllerTests.cs @@ -21,7 +21,7 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var response = await client.DeleteAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}"); + using var response = await client.DeleteAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -31,7 +31,7 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var response = await client.PostAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}", null); + using var response = await client.PostAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}", null, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -43,7 +43,7 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory var userDto = await AuthHelper.GetUserDtoAsync(client); - using var response = await client.DeleteAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}"); + using var response = await client.DeleteAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -55,7 +55,7 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory var userDto = await AuthHelper.GetUserDtoAsync(client); - using var response = await client.PostAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}", null); + using var response = await client.PostAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}", null, TestContext.Current.CancellationToken); 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 index 547bfcc0ff..c982b9804d 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/PluginsControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/PluginsControllerTests.cs @@ -24,7 +24,7 @@ public sealed class PluginsControllerTests : IClassFixture<JellyfinApplicationFa { var client = _factory.CreateClient(); - var response = await client.GetAsync("/Plugins"); + var response = await client.GetAsync("/Plugins", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } @@ -35,11 +35,11 @@ public sealed class PluginsControllerTests : IClassFixture<JellyfinApplicationFa var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync("/Plugins"); + var response = await client.GetAsync("/Plugins", TestContext.Current.CancellationToken); 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); + _ = await response.Content.ReadFromJsonAsync<PluginInfo[]>(JsonDefaults.Options, TestContext.Current.CancellationToken); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs index c8ae2a88af..0e5d81a4d6 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs @@ -8,11 +8,11 @@ using System.Threading.Tasks; using Jellyfin.Api.Models.StartupDtos; using Jellyfin.Extensions.Json; using Xunit; -using Xunit.Priority; +using Xunit.v3.Priority; namespace Jellyfin.Server.Integration.Tests.Controllers { - [TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)] + [TestCaseOrderer(typeof(PriorityOrderer))] public sealed class StartupControllerTests : IClassFixture<JellyfinApplicationFactory> { private readonly JellyfinApplicationFactory _factory; @@ -37,14 +37,14 @@ namespace Jellyfin.Server.Integration.Tests.Controllers PreferredMetadataLanguage = "nl" }; - using var postResponse = await client.PostAsJsonAsync("/Startup/Configuration", config, _jsonOptions); + using var postResponse = await client.PostAsJsonAsync("/Startup/Configuration", config, _jsonOptions, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode); - using var getResponse = await client.GetAsync("/Startup/Configuration"); + using var getResponse = await client.GetAsync("/Startup/Configuration", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, getResponse.Content.Headers.ContentType?.MediaType); - var newConfig = await getResponse.Content.ReadFromJsonAsync<StartupConfigurationDto>(_jsonOptions); + var newConfig = await getResponse.Content.ReadFromJsonAsync<StartupConfigurationDto>(_jsonOptions, TestContext.Current.CancellationToken); Assert.Equal(config.ServerName, newConfig!.ServerName); Assert.Equal(config.UICulture, newConfig.UICulture); Assert.Equal(config.MetadataCountryCode, newConfig.MetadataCountryCode); @@ -57,11 +57,11 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { var client = _factory.CreateClient(); - using var response = await client.GetAsync("/Startup/User"); + using var response = await client.GetAsync("/Startup/User", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); - var user = await response.Content.ReadFromJsonAsync<StartupUserDto>(_jsonOptions); + var user = await response.Content.ReadFromJsonAsync<StartupUserDto>(_jsonOptions, TestContext.Current.CancellationToken); Assert.NotNull(user); Assert.NotNull(user.Name); Assert.NotEmpty(user.Name); @@ -80,14 +80,14 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Password = "NewPassword" }; - var postResponse = await client.PostAsJsonAsync("/Startup/User", user, _jsonOptions); + var postResponse = await client.PostAsJsonAsync("/Startup/User", user, _jsonOptions, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode); - var getResponse = await client.GetAsync("/Startup/User"); + var getResponse = await client.GetAsync("/Startup/User", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, getResponse.Content.Headers.ContentType?.MediaType); - var newUser = await getResponse.Content.ReadFromJsonAsync<StartupUserDto>(_jsonOptions); + var newUser = await getResponse.Content.ReadFromJsonAsync<StartupUserDto>(_jsonOptions, TestContext.Current.CancellationToken); Assert.NotNull(newUser); Assert.Equal(user.Name, newUser.Name); Assert.Null(newUser.Password); @@ -99,7 +99,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { var client = _factory.CreateClient(); - var response = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>())); + var response = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>()), TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); } @@ -109,7 +109,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { var client = _factory.CreateClient(); - using var response = await client.GetAsync("/Startup/User"); + using var response = await client.GetAsync("/Startup/User", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs index 04d1b3dc27..7ea56be731 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs @@ -10,11 +10,11 @@ using Jellyfin.Api.Models.UserDtos; using Jellyfin.Extensions.Json; using MediaBrowser.Model.Dto; using Xunit; -using Xunit.Priority; +using Xunit.v3.Priority; namespace Jellyfin.Server.Integration.Tests.Controllers { - [TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)] + [TestCaseOrderer(typeof(PriorityOrderer))] public sealed class UserControllerTests : IClassFixture<JellyfinApplicationFactory> { private const string TestUsername = "testUser01"; @@ -41,9 +41,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers { var client = _factory.CreateClient(); - using var response = await client.GetAsync("Users/Public"); + using var response = await client.GetAsync("Users/Public", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var users = await response.Content.ReadFromJsonAsync<UserDto[]>(_jsonOptions); + var users = await response.Content.ReadFromJsonAsync<UserDto[]>(_jsonOptions, TestContext.Current.CancellationToken); // User are hidden by default Assert.NotNull(users); Assert.Empty(users); @@ -56,9 +56,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - using var response = await client.GetAsync("Users"); + using var response = await client.GetAsync("Users", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var users = await response.Content.ReadFromJsonAsync<UserDto[]>(_jsonOptions); + var users = await response.Content.ReadFromJsonAsync<UserDto[]>(_jsonOptions, TestContext.Current.CancellationToken); Assert.NotNull(users); Assert.Single(users); } @@ -89,7 +89,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers using var response = await CreateUserByName(client, createRequest); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var user = await response.Content.ReadFromJsonAsync<UserDto>(_jsonOptions); + var user = await response.Content.ReadFromJsonAsync<UserDto>(_jsonOptions, TestContext.Current.CancellationToken); Assert.Equal(TestUsername, user!.Name); _testUserId = user.Id; @@ -128,7 +128,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers // access token can't be null here as the previous test populated it client.DefaultRequestHeaders.AddAuthHeader(_accessToken!); - using var response = await client.DeleteAsync($"User/{Guid.NewGuid()}"); + using var response = await client.DeleteAsync($"User/{Guid.NewGuid()}", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs index 98ad28f5bd..6e4fccd735 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs @@ -28,7 +28,7 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.GetAsync($"Users/{Guid.NewGuid()}/Items/Root"); + var response = await client.GetAsync($"Users/{Guid.NewGuid()}/Items/Root", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -54,7 +54,7 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client); - var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid(), rootFolderDto.Id)); + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid(), rootFolderDto.Id), TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -71,7 +71,7 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati var userDto = await AuthHelper.GetUserDtoAsync(client); - var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, Guid.NewGuid())); + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, Guid.NewGuid()), TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -84,9 +84,9 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati var userDto = await AuthHelper.GetUserDtoAsync(client); var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id); - var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}"); + var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var rootDto = await response.Content.ReadFromJsonAsync<BaseItemDto>(_jsonOptions); + var rootDto = await response.Content.ReadFromJsonAsync<BaseItemDto>(_jsonOptions, TestContext.Current.CancellationToken); Assert.NotNull(rootDto); } @@ -99,9 +99,9 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati var userDto = await AuthHelper.GetUserDtoAsync(client); var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id); - var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}/Intros"); + var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}/Intros", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var rootDto = await response.Content.ReadFromJsonAsync<QueryResult<BaseItemDto>>(_jsonOptions); + var rootDto = await response.Content.ReadFromJsonAsync<QueryResult<BaseItemDto>>(_jsonOptions, TestContext.Current.CancellationToken); Assert.NotNull(rootDto); } @@ -116,9 +116,9 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati var userDto = await AuthHelper.GetUserDtoAsync(client); var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id); - var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, rootFolderDto.Id)); + var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, rootFolderDto.Id), TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var rootDto = await response.Content.ReadFromJsonAsync<BaseItemDto[]>(_jsonOptions); + var rootDto = await response.Content.ReadFromJsonAsync<BaseItemDto[]>(_jsonOptions, TestContext.Current.CancellationToken); Assert.NotNull(rootDto); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs index 1916ced12c..e0630ff443 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/VideosControllerTests.cs @@ -21,7 +21,7 @@ public sealed class VideosControllerTests : IClassFixture<JellyfinApplicationFac var client = _factory.CreateClient(); client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client)); - var response = await client.DeleteAsync($"Videos/{Guid.NewGuid()}"); + var response = await client.DeleteAsync($"Videos/{Guid.NewGuid()}", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs index d2249cdc3b..a343423b4d 100644 --- a/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs +++ b/tests/Jellyfin.Server.Integration.Tests/EncodedQueryStringTest.cs @@ -27,9 +27,9 @@ namespace Jellyfin.Server.Integration.Tests { var client = _factory.CreateClient(); - var response = await client.GetAsync("Encoder/UrlDecode?" + sourceUrl); + var response = await client.GetAsync("Encoder/UrlDecode?" + sourceUrl, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string reply = await response.Content.ReadAsStringAsync(); + string reply = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); Assert.Equal(unencodedUrl, reply); } @@ -40,9 +40,9 @@ namespace Jellyfin.Server.Integration.Tests { var client = _factory.CreateClient(); - var response = await client.GetAsync("Encoder/UrlArrayDecode?" + sourceUrl); + var response = await client.GetAsync("Encoder/UrlArrayDecode?" + sourceUrl, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string reply = await response.Content.ReadAsStringAsync(); + string reply = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); Assert.Equal(unencodedUrl, reply); } } 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 7b0e23788b..7abad8bb84 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj +++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj @@ -1,17 +1,21 @@ <Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <OutputType>Exe</OutputType> + </PropertyGroup> + <ItemGroup> <PackageReference Include="AutoFixture" /> <PackageReference Include="AutoFixture.AutoMoq" /> - <PackageReference Include="AutoFixture.Xunit2" /> + <PackageReference Include="AutoFixture.Xunit3" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" /> <PackageReference Include="Microsoft.NET.Test.Sdk" /> - <PackageReference Include="xunit" /> + <PackageReference Include="xunit.v3" /> <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Xunit.Priority" /> + <PackageReference Include="Xunit.v3.Priority" /> <PackageReference Include="coverlet.collector" /> <PackageReference Include="Moq" /> </ItemGroup> diff --git a/tests/Jellyfin.Server.Integration.Tests/Middleware/RobotsRedirectionMiddlewareTests.cs b/tests/Jellyfin.Server.Integration.Tests/Middleware/RobotsRedirectionMiddlewareTests.cs index 1ea79f7deb..baeaf4d0cb 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Middleware/RobotsRedirectionMiddlewareTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Middleware/RobotsRedirectionMiddlewareTests.cs @@ -23,7 +23,7 @@ namespace Jellyfin.Server.Integration.Tests.Middleware AllowAutoRedirect = false }); - var response = await client.GetAsync("robots.txt"); + var response = await client.GetAsync("robots.txt", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); Assert.Equal("web/robots.txt", response.Headers.Location?.ToString()); diff --git a/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs b/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs index 62cdd25aec..17a8a55222 100644 --- a/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs @@ -3,7 +3,6 @@ using System.Reflection; using System.Threading.Tasks; using MediaBrowser.Model.IO; using Xunit; -using Xunit.Abstractions; namespace Jellyfin.Server.Integration.Tests { @@ -25,7 +24,7 @@ namespace Jellyfin.Server.Integration.Tests var client = _factory.CreateClient(); // Act - var response = await client.GetAsync("/api-docs/openapi.json"); + var response = await client.GetAsync("/api-docs/openapi.json", TestContext.Current.CancellationToken); // Assert response.EnsureSuccessStatusCode(); @@ -35,7 +34,7 @@ namespace Jellyfin.Server.Integration.Tests string outputPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? ".", "openapi.json")); _outputHelper.WriteLine("Writing OpenAPI Spec JSON to '{0}'.", outputPath); await using var fs = AsyncFile.Create(outputPath); - await response.Content.CopyToAsync(fs); + await response.Content.CopyToAsync(fs, TestContext.Current.CancellationToken); } } } diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj index 21596e0ed2..3ad5310c6b 100644 --- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj +++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj @@ -1,12 +1,16 @@ <Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <OutputType>Exe</OutputType> + </PropertyGroup> + <ItemGroup> <PackageReference Include="AutoFixture" /> <PackageReference Include="AutoFixture.AutoMoq" /> - <PackageReference Include="AutoFixture.Xunit2" /> + <PackageReference Include="AutoFixture.Xunit3" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" /> <PackageReference Include="Microsoft.NET.Test.Sdk" /> - <PackageReference Include="xunit" /> + <PackageReference Include="xunit.v3" /> <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs index 14f4c33b6b..e788f43b86 100644 --- a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs +++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs @@ -23,8 +23,8 @@ namespace Jellyfin.Server.Tests true, true, new string[] { "192.168.t", "127.0.0.1", "::1", "1234.1232.12.1234" }, - new IPAddress[] { IPAddress.Loopback }, - new IPNetwork[] { new IPNetwork(IPAddress.IPv6Loopback, 128) }); + new IPAddress[] { IPAddress.Loopback, IPAddress.IPv6Loopback }, + Array.Empty<IPNetwork>()); data.Add( true, @@ -37,8 +37,8 @@ namespace Jellyfin.Server.Tests true, true, new string[] { "::1" }, - Array.Empty<IPAddress>(), - new IPNetwork[] { new IPNetwork(IPAddress.IPv6Loopback, 128) }); + new IPAddress[] { IPAddress.IPv6Loopback }, + Array.Empty<IPNetwork>()); data.Add( false, @@ -58,15 +58,15 @@ namespace Jellyfin.Server.Tests false, true, new string[] { "localhost" }, - Array.Empty<IPAddress>(), - new IPNetwork[] { new IPNetwork(IPAddress.IPv6Loopback, 128) }); + new IPAddress[] { IPAddress.IPv6Loopback }, + Array.Empty<IPNetwork>()); data.Add( true, true, new string[] { "localhost" }, - new IPAddress[] { IPAddress.Loopback }, - new IPNetwork[] { new IPNetwork(IPAddress.IPv6Loopback, 128) }); + new IPAddress[] { IPAddress.Loopback, IPAddress.IPv6Loopback }, + Array.Empty<IPNetwork>()); return data; } diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj index 9fe0744de1..3b39fe72d6 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj +++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj @@ -1,5 +1,9 @@ <Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <OutputType>Exe</OutputType> + </PropertyGroup> + <ItemGroup> <None Include="Test Data\**\*.*"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> @@ -9,7 +13,7 @@ <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" /> <PackageReference Include="Moq" /> - <PackageReference Include="xunit" /> + <PackageReference Include="xunit.v3" /> <PackageReference Include="xunit.runner.visualstudio"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> |
