mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-04-19 16:52:31 +01:00
Compare commits
1 Commits
master
...
renovate/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fb44fe935 |
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "10.0.6",
|
||||
"version": "10.0.5",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
||||
10
.github/workflows/ci-codeql-analysis.yml
vendored
10
.github/workflows/ci-codeql-analysis.yml
vendored
@@ -8,10 +8,6 @@ on:
|
||||
schedule:
|
||||
- cron: '24 2 * * 4'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
@@ -32,13 +28,13 @@ jobs:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
|
||||
4
.github/workflows/ci-compat.yml
vendored
4
.github/workflows/ci-compat.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
dotnet build Jellyfin.Server -o ./out
|
||||
|
||||
- name: Upload Head
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: abi-base
|
||||
retention-days: 14
|
||||
|
||||
2
.github/workflows/ci-tests.yml
vendored
2
.github/workflows/ci-tests.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
--verbosity minimal
|
||||
|
||||
- name: Merge code coverage results
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@7ae927204961589fcb0b0be245c51fbbc87cbca2 # v5.5.5
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@cf6fe1b38ed5becc89ffe056c1f240825993be5b # v5.5.4
|
||||
with:
|
||||
reports: "**/coverage.cobertura.xml"
|
||||
targetdir: "merged/"
|
||||
|
||||
2
.github/workflows/commands.yml
vendored
2
.github/workflows/commands.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
rename:
|
||||
name: Rename
|
||||
if: contains(github.event.comment.body, '@jellyfin-bot rename')
|
||||
if: contains(github.event.comment.body, '@jellyfin-bot rename') && github.event.comment.author_association == 'MEMBER'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: pull in script
|
||||
|
||||
2
.github/workflows/openapi-generate.yml
vendored
2
.github/workflows/openapi-generate.yml
vendored
@@ -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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: ${{ inputs.artifact }}
|
||||
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
|
||||
|
||||
2
.github/workflows/openapi-pull-request.yml
vendored
2
.github/workflows/openapi-pull-request.yml
vendored
@@ -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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: openapi-report
|
||||
path: /tmp/openapi-report/openapi-report.md
|
||||
|
||||
@@ -212,7 +212,6 @@
|
||||
- [martenumberto](https://github.com/martenumberto)
|
||||
- [ZeusCraft10](https://github.com/ZeusCraft10)
|
||||
- [MarcoCoreDuo](https://github.com/MarcoCoreDuo)
|
||||
- [LiHRaM](https://github.com/LiHRaM)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
||||
@@ -13,41 +13,41 @@
|
||||
<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="10.0.0" />
|
||||
<PackageVersion Include="coverlet.collector" Version="8.0.1" />
|
||||
<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="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.1" />
|
||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.3" />
|
||||
<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.12.0-pre1" />
|
||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||
<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.6" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="10.0.5" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.5" />
|
||||
<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.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.6" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
|
||||
<PackageVersion Include="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="MimeTypes" Version="2.5.2" />
|
||||
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
@@ -69,15 +69,15 @@
|
||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||
<PackageVersion Include="SharpFuzz" Version="2.2.0" />
|
||||
<!-- Pinned to 3.116.1 because https://github.com/jellyfin/jellyfin/pull/14255 -->
|
||||
<PackageVersion Include="SkiaSharp" Version="[3.116.1]" />
|
||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="[3.116.1]" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="[3.116.1]" />
|
||||
<PackageVersion Include="SkiaSharp" Version="[3.119.2]" />
|
||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="[3.119.2]" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="[3.119.2]" />
|
||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<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.6" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.5" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="7.12.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="3.0.0" />
|
||||
|
||||
@@ -60,7 +60,6 @@ namespace Emby.Server.Implementations.IO
|
||||
_fileSystem = fileSystem;
|
||||
|
||||
appLifetime.ApplicationStarted.Register(Start);
|
||||
appLifetime.ApplicationStopping.Register(Stop);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -50,10 +50,6 @@ 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;
|
||||
@@ -63,13 +59,8 @@ public class ArtistsValidator
|
||||
try
|
||||
{
|
||||
var item = _libraryManager.GetArtist(name);
|
||||
var isNew = !existingArtistIds.Contains(item.Id);
|
||||
var neverRefreshed = item.DateLastRefreshed == default;
|
||||
|
||||
if (isNew || neverRefreshed)
|
||||
{
|
||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
{
|
||||
"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."
|
||||
}
|
||||
@@ -63,8 +63,8 @@
|
||||
"Photos": "Fotos",
|
||||
"Playlists": "Llistes de reproducció",
|
||||
"Plugin": "Complement",
|
||||
"PluginInstalledWithName": "S'ha instal·lat {0}",
|
||||
"PluginUninstalledWithName": "S'ha desinstal·lat {0}",
|
||||
"PluginInstalledWithName": "{0} s'ha instal·lat",
|
||||
"PluginUninstalledWithName": "{0} s'ha desinstal·lat",
|
||||
"PluginUpdatedWithName": "S'ha actualitzat {0}",
|
||||
"ProviderValue": "Proveïdor: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} ha fallat",
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
"Channels": "Kanavat",
|
||||
"CameraImageUploadedFrom": "Uusi kameran kuva on sirretty lähteestä {0}",
|
||||
"Books": "Kirjat",
|
||||
"AuthenticationSucceededWithUserName": "{0} todennus onnistunut",
|
||||
"Artists": "Artistit",
|
||||
"AuthenticationSucceededWithUserName": "{0} on todennettu",
|
||||
"Artists": "Esittäjät",
|
||||
"Application": "Sovellus",
|
||||
"AppDeviceValues": "Sovellus: {0}, Laite: {1}",
|
||||
"Albums": "Albumit",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"HeaderFavoriteShows": "Omiljene serije",
|
||||
"HeaderFavoriteSongs": "Omiljene pjesme",
|
||||
"HeaderLiveTV": "TV uživo",
|
||||
"HeaderNextUp": "Sljedeće na redu",
|
||||
"HeaderNextUp": "Slijedi",
|
||||
"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": "Titlovi nisu uspješno preuzeti od {0} za {1}",
|
||||
"SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}",
|
||||
"Sync": "Sinkronizacija",
|
||||
"System": "Sustav",
|
||||
"TvShows": "TV emisije",
|
||||
"TvShows": "Serije",
|
||||
"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 biblioteku medija",
|
||||
"ValueSpecialEpisodeName": "Posebno – {0}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku",
|
||||
"ValueSpecialEpisodeName": "Posebno - {0}",
|
||||
"VersionNumber": "Verzija {0}",
|
||||
"TaskRefreshLibraryDescription": "Skenira biblioteku medija radi novih datoteka i osvježava metapodatke.",
|
||||
"TaskRefreshLibrary": "Skeniraj biblioteku medija",
|
||||
"TaskRefreshLibraryDescription": "Skenira medijsku biblioteku radi novih datoteka i osvježava metapodatke.",
|
||||
"TaskRefreshLibrary": "Skeniraj medijsku biblioteku",
|
||||
"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 nedsotajućim titlovima ne osnovi konfiguracije metapodataka.",
|
||||
"TaskDownloadMissingSubtitles": "Preuzmi nedostajuće titlove",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Pretraži Internet za prijevodima koji nedostaju prema konfiguraciji metapodataka.",
|
||||
"TaskDownloadMissingSubtitles": "Preuzmi prijevod koji nedostaje",
|
||||
"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 biblioteci medija.",
|
||||
"TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u medijskoj biblioteci.",
|
||||
"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": "Eksterni",
|
||||
"External": "Vanjski",
|
||||
"TaskKeyframeExtractorDescription": "Izvlačenje ključnih okvira iz videozapisa za stvaranje objektivnije HLS liste za reprodukciju. Pokretanje ovog zadatka može potrajati.",
|
||||
"TaskKeyframeExtractor": "Izvoditelj ključnog okvira",
|
||||
"TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka.",
|
||||
@@ -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 u postavke biblioteke.",
|
||||
"TaskMoveTrickplayImagesDescription": "Premješta postojeće datoteke brzog pregledavanja prema postavkama 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."
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"Inherit": "繼承",
|
||||
"ItemAddedWithName": "{0} 經已加咗入媒體櫃",
|
||||
"ItemRemovedWithName": "{0} 經已由媒體櫃移除咗",
|
||||
"LabelIpAddressValue": "IP 位址:{0}",
|
||||
"LabelIpAddressValue": "IP 地址:{0}",
|
||||
"LabelRunningTimeValue": "運行時間:{0}",
|
||||
"Latest": "最新",
|
||||
"MessageApplicationUpdated": "Jellyfin 經已更新咗",
|
||||
|
||||
@@ -138,7 +138,7 @@ namespace Emby.Server.Implementations.Localization
|
||||
string twoCharName = parts[2];
|
||||
if (string.IsNullOrWhiteSpace(twoCharName))
|
||||
{
|
||||
twoCharName = string.Empty;
|
||||
continue;
|
||||
}
|
||||
else if (twoCharName.Contains('-', StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
|
||||
@@ -749,12 +749,6 @@
|
||||
"ThreeLetterISORegionName": "TAJ",
|
||||
"TwoLetterISORegionName": "TJ"
|
||||
},
|
||||
{
|
||||
"DisplayName": "Tanzania",
|
||||
"Name": "TZ",
|
||||
"ThreeLetterISORegionName": "TZA",
|
||||
"TwoLetterISORegionName": "TZ"
|
||||
},
|
||||
{
|
||||
"DisplayName": "Thailand",
|
||||
"Name": "TH",
|
||||
|
||||
@@ -75,9 +75,7 @@ public class LibraryStructureController : BaseJellyfinApiController
|
||||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> AddVirtualFolder(
|
||||
[FromQuery]
|
||||
[RegularExpression(@"^(?:\S(?:.*\S)?)$", ErrorMessage = "Library name cannot be empty or have leading/trailing spaces.")]
|
||||
string name,
|
||||
[FromQuery] string name,
|
||||
[FromQuery] CollectionTypeOptions? collectionType,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedCollectionModelBinder))] string[] paths,
|
||||
[FromBody] AddVirtualFolderDto? libraryOptionsDto,
|
||||
|
||||
@@ -50,9 +50,6 @@ 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>
|
||||
@@ -73,9 +70,6 @@ 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,
|
||||
@@ -103,9 +97,6 @@ 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,
|
||||
|
||||
@@ -235,21 +235,6 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,44 +28,22 @@ public static class StorageHelper
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the free space of the parent filesystem of a specific directory.
|
||||
/// Gets the free space of a specific directory.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to a folder.</param>
|
||||
/// <returns>Various details about the parent filesystem containing the directory.</returns>
|
||||
/// <returns>The number of bytes available space.</returns>
|
||||
public static FolderStorageInfo GetFreeSpaceOf(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Fully resolve the given path to an actual filesystem target, in case it's a symlink or similar.
|
||||
var resolvedPath = ResolvePath(path);
|
||||
// We iterate all filesystems reported by GetDrives() here, and attempt to find the best
|
||||
// match that contains, as deep as possible, the given path.
|
||||
// This is required because simply calling `DriveInfo` on a path returns that path as
|
||||
// the Name and RootDevice, which is not at all how this should work.
|
||||
var allDrives = DriveInfo.GetDrives();
|
||||
DriveInfo? bestMatch = null;
|
||||
foreach (DriveInfo d in allDrives)
|
||||
{
|
||||
if (resolvedPath.StartsWith(d.RootDirectory.FullName, StringComparison.InvariantCultureIgnoreCase) &&
|
||||
(bestMatch is null || d.RootDirectory.FullName.Length > bestMatch.RootDirectory.FullName.Length))
|
||||
{
|
||||
bestMatch = d;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestMatch is null)
|
||||
{
|
||||
throw new InvalidOperationException($"The path `{path}` has no matching parent device. Space check invalid.");
|
||||
}
|
||||
|
||||
var driveInfo = new DriveInfo(path);
|
||||
return new FolderStorageInfo()
|
||||
{
|
||||
Path = path,
|
||||
ResolvedPath = resolvedPath,
|
||||
FreeSpace = bestMatch.AvailableFreeSpace,
|
||||
UsedSpace = bestMatch.TotalSize - bestMatch.AvailableFreeSpace,
|
||||
StorageType = bestMatch.DriveType.ToString(),
|
||||
DeviceId = bestMatch.Name,
|
||||
FreeSpace = driveInfo.AvailableFreeSpace,
|
||||
UsedSpace = driveInfo.TotalSize - driveInfo.AvailableFreeSpace,
|
||||
StorageType = driveInfo.DriveType.ToString(),
|
||||
DeviceId = driveInfo.Name,
|
||||
};
|
||||
}
|
||||
catch
|
||||
@@ -73,7 +51,6 @@ public static class StorageHelper
|
||||
return new FolderStorageInfo()
|
||||
{
|
||||
Path = path,
|
||||
ResolvedPath = path,
|
||||
FreeSpace = -1,
|
||||
UsedSpace = -1,
|
||||
StorageType = null,
|
||||
@@ -82,26 +59,6 @@ public static class StorageHelper
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Walk a path and fully resolve any symlinks within it.
|
||||
/// </summary>
|
||||
private static string ResolvePath(string path)
|
||||
{
|
||||
var parts = path.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries);
|
||||
var current = Path.DirectorySeparatorChar.ToString();
|
||||
foreach (var part in parts)
|
||||
{
|
||||
current = Path.Combine(current, part);
|
||||
var resolved = new DirectoryInfo(current).ResolveLinkTarget(returnFinalTarget: true);
|
||||
if (resolved is not null)
|
||||
{
|
||||
current = resolved.FullName;
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying drive data from a given path and checks if the available storage capacity matches the threshold.
|
||||
/// </summary>
|
||||
|
||||
@@ -161,6 +161,7 @@ namespace Jellyfin.Server
|
||||
_loggerFactory,
|
||||
options,
|
||||
startupConfig);
|
||||
_appHost = appHost;
|
||||
var configurationCompleted = false;
|
||||
try
|
||||
{
|
||||
@@ -206,7 +207,6 @@ 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,7 +263,6 @@ namespace Jellyfin.Server
|
||||
|
||||
_appHost = null;
|
||||
_jellyfinHost?.Dispose();
|
||||
_jellyfinHost = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -142,7 +142,6 @@ 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()
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#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)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,12 +42,6 @@ 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; }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -10,45 +12,45 @@ namespace MediaBrowser.Controller.LiveTv
|
||||
{
|
||||
public ProgramInfo()
|
||||
{
|
||||
Genres = [];
|
||||
Genres = new List<string>();
|
||||
|
||||
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.
|
||||
@@ -106,25 +108,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.
|
||||
@@ -186,19 +188,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.
|
||||
@@ -216,10 +218,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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1331,7 +1331,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
public bool CanExtractSubtitles(string codec)
|
||||
{
|
||||
return _configurationManager.GetEncodingOptions().EnableSubtitleExtraction;
|
||||
// TODO is there ever a case when a subtitle can't be extracted??
|
||||
return true;
|
||||
}
|
||||
|
||||
private sealed class ProcessWrapper : IDisposable
|
||||
|
||||
@@ -729,9 +729,6 @@ 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;
|
||||
|
||||
@@ -770,9 +767,6 @@ 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))
|
||||
{
|
||||
|
||||
@@ -101,11 +101,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
return ms;
|
||||
}
|
||||
|
||||
internal void FilterEvents(SubtitleTrackInfo track, long startPositionTicks, long endTimeTicks, bool preserveTimestamps)
|
||||
private void FilterEvents(SubtitleTrackInfo track, long startPositionTicks, long endTimeTicks, bool preserveTimestamps)
|
||||
{
|
||||
// Drop subs that have fully elapsed before the requested start position
|
||||
// Drop subs that are earlier than what we're looking for
|
||||
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 = Math.Max(0, trackEvent.EndPositionTicks - startPositionTicks);
|
||||
trackEvent.StartPositionTicks = Math.Max(0, trackEvent.StartPositionTicks - startPositionTicks);
|
||||
trackEvent.EndPositionTicks -= startPositionTicks;
|
||||
trackEvent.StartPositionTicks -= startPositionTicks;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1555,7 +1555,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!subtitleStream.IsExternal && playMethod == PlayMethod.Transcode && !transcoderSupport.CanExtractSubtitles(subtitleStream.Codec))
|
||||
if (!subtitleStream.IsExternal && !transcoderSupport.CanExtractSubtitles(subtitleStream.Codec))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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;
|
||||
|
||||
36
MediaBrowser.Model/Providers/SubtitleOptions.cs
Normal file
36
MediaBrowser.Model/Providers/SubtitleOptions.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
#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; }
|
||||
}
|
||||
}
|
||||
@@ -11,22 +11,17 @@ public record FolderStorageInfo
|
||||
public required string Path { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the fully resolved path of the folder in question (interpolating any symlinks if present).
|
||||
/// </summary>
|
||||
public required string ResolvedPath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the free space of the underlying storage device of the <see cref="ResolvedPath"/>.
|
||||
/// Gets the free space of the underlying storage device of the <see cref="Path"/>.
|
||||
/// </summary>
|
||||
public long FreeSpace { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the used space of the underlying storage device of the <see cref="ResolvedPath"/>.
|
||||
/// Gets the used space of the underlying storage device of the <see cref="Path"/>.
|
||||
/// </summary>
|
||||
public long UsedSpace { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the kind of storage device of the <see cref="ResolvedPath"/>.
|
||||
/// Gets the kind of storage device of the <see cref="Path"/>.
|
||||
/// </summary>
|
||||
public string? StorageType { get; init; }
|
||||
|
||||
|
||||
@@ -487,13 +487,6 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ 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;
|
||||
@@ -24,6 +25,7 @@ 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
|
||||
@@ -72,6 +74,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
_subtitleResolver = subtitleResolver;
|
||||
_mediaAttachmentRepository = mediaAttachmentRepository;
|
||||
_mediaStreamRepository = mediaStreamRepository;
|
||||
_mediaStreamRepository = mediaStreamRepository;
|
||||
}
|
||||
|
||||
public async Task<ItemUpdateType> ProbeVideo<T>(
|
||||
@@ -363,8 +366,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
blurayVideoStream.ColorSpace = ffmpegVideoStream.ColorSpace;
|
||||
blurayVideoStream.ColorTransfer = ffmpegVideoStream.ColorTransfer;
|
||||
blurayVideoStream.ColorPrimaries = ffmpegVideoStream.ColorPrimaries;
|
||||
blurayVideoStream.BitDepth = ffmpegVideoStream.BitDepth;
|
||||
blurayVideoStream.PixelFormat = ffmpegVideoStream.PixelFormat;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,19 +549,47 @@ 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);
|
||||
|
||||
if (enableSubtitleDownloading && libraryOptions.SubtitleDownloadLanguages is not null)
|
||||
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)
|
||||
{
|
||||
var downloadedLanguages = await new SubtitleDownloader(
|
||||
_logger,
|
||||
_subtitleManager).DownloadSubtitles(
|
||||
video,
|
||||
currentStreams.Concat(externalSubtitleStreams).ToList(),
|
||||
libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent,
|
||||
libraryOptions.SkipSubtitlesIfAudioTrackMatches,
|
||||
libraryOptions.RequirePerfectSubtitleMatch,
|
||||
libraryOptions.SubtitleDownloadLanguages,
|
||||
skipIfEmbeddedSubtitlesPresent,
|
||||
skipIfAudioTrackMatches,
|
||||
requirePerfectMatch,
|
||||
subtitleDownloadLanguages,
|
||||
libraryOptions.DisabledSubtitleFetchers,
|
||||
libraryOptions.SubtitleFetcherOrder,
|
||||
true,
|
||||
|
||||
@@ -8,12 +8,14 @@ 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.Subtitles;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -55,9 +57,16 @@ 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>();
|
||||
@@ -72,13 +81,16 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
if (libraryOptions.SubtitleDownloadLanguages is null)
|
||||
{
|
||||
// Skip this library if subtitle download languages are not configured
|
||||
continue;
|
||||
subtitleDownloadLanguages = options.DownloadLanguages;
|
||||
skipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent;
|
||||
skipIfAudioTrackMatches = options.SkipIfAudioTrackMatches;
|
||||
}
|
||||
else
|
||||
{
|
||||
subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages;
|
||||
skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent;
|
||||
skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches;
|
||||
}
|
||||
|
||||
subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages;
|
||||
skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent;
|
||||
skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches;
|
||||
|
||||
foreach (var lang in subtitleDownloadLanguages)
|
||||
{
|
||||
@@ -132,7 +144,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
try
|
||||
{
|
||||
await DownloadSubtitles(video as Video, cancellationToken).ConfigureAwait(false);
|
||||
await DownloadSubtitles(video as Video, options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -148,7 +160,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> DownloadSubtitles(Video video, CancellationToken cancellationToken)
|
||||
private async Task<bool> DownloadSubtitles(Video video, SubtitleOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
var mediaStreams = video.GetMediaStreams();
|
||||
|
||||
@@ -161,14 +173,18 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
|
||||
if (libraryOptions.SubtitleDownloadLanguages is null)
|
||||
{
|
||||
// Subtitle downloading is not configured for this library
|
||||
return true;
|
||||
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;
|
||||
}
|
||||
|
||||
subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages;
|
||||
skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent;
|
||||
skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches;
|
||||
requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch;
|
||||
|
||||
var downloadedLanguages = await new SubtitleDownloader(
|
||||
_logger,
|
||||
|
||||
@@ -125,9 +125,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
|
||||
|
||||
if (string.IsNullOrWhiteSpace(overview))
|
||||
{
|
||||
overview = string.IsNullOrWhiteSpace(result.strBiographyEN)
|
||||
? result.strBiography
|
||||
: result.strBiographyEN;
|
||||
overview = result.strBiographyEN;
|
||||
}
|
||||
|
||||
item.Overview = (overview ?? string.Empty).StripHtml();
|
||||
@@ -226,8 +224,6 @@ 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; }
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz;
|
||||
/// <summary>
|
||||
/// MusicBrainz artist provider.
|
||||
/// </summary>
|
||||
public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>, IDisposable, IHasOrder
|
||||
public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>, IDisposable
|
||||
{
|
||||
private readonly ILogger<MusicBrainzArtistProvider> _logger;
|
||||
private Query _musicBrainzQuery;
|
||||
@@ -42,10 +42,6 @@ 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;
|
||||
|
||||
@@ -61,7 +61,7 @@ public sealed class SqliteDatabaseProvider : IJellyfinDatabaseProvider
|
||||
var customOptions = databaseConfiguration.CustomProviderOptions?.Options;
|
||||
|
||||
var sqliteConnectionBuilder = new SqliteConnectionStringBuilder();
|
||||
sqliteConnectionBuilder.DataSource = GetOption(customOptions, "path", e => e, () => Path.Combine(_applicationPaths.DataPath, "jellyfin.db"));
|
||||
sqliteConnectionBuilder.DataSource = 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);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -60,21 +62,21 @@ namespace Jellyfin.LiveTv.Listings
|
||||
_logger.LogInformation("xmltv path: {Path}", info.Path);
|
||||
|
||||
string cacheFilename = info.Id + ".xml";
|
||||
string cacheDir = Path.Join(_config.ApplicationPaths.CachePath, "xmltv");
|
||||
string cacheFile = Path.Join(cacheDir, cacheFilename);
|
||||
string cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename);
|
||||
|
||||
if (File.Exists(cacheFile) && File.GetLastWriteTimeUtc(cacheFile) >= DateTime.UtcNow.Subtract(_maxCacheAge))
|
||||
{
|
||||
return cacheFile;
|
||||
}
|
||||
|
||||
// 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(cacheDir);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
|
||||
}
|
||||
|
||||
if (info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -152,37 +154,33 @@ 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(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)
|
||||
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)
|
||||
};
|
||||
|
||||
if (string.IsNullOrWhiteSpace(program.ProgramId))
|
||||
@@ -263,7 +261,7 @@ namespace Jellyfin.LiveTv.Listings
|
||||
{
|
||||
Id = c.Id,
|
||||
Name = c.DisplayName,
|
||||
ImageUrl = string.IsNullOrEmpty(c.Icons.FirstOrDefault()?.Source) ? null : c.Icons.FirstOrDefault()!.Source,
|
||||
ImageUrl = string.IsNullOrEmpty(c.Icon?.Source) ? null : c.Icon.Source,
|
||||
Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
@@ -1,282 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -617,60 +617,5 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
|
||||
});
|
||||
var countries = localizationManager.GetCountries().ToList();
|
||||
|
||||
Assert.Equal(140, countries.Count);
|
||||
Assert.Equal(139, 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(496, cultures.Count);
|
||||
Assert.Equal(194, cultures.Count);
|
||||
|
||||
var germany = cultures.FirstOrDefault(x => x.TwoLetterISOLanguageName.Equals("de", StringComparison.Ordinal));
|
||||
Assert.NotNull(germany);
|
||||
@@ -99,25 +99,6 @@ 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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user