diff options
10 files changed, 132 insertions, 33 deletions
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 24e738475..d7f1e3076 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -3,6 +3,8 @@ on: push: branches: - master + tags: + - 'v*' pull_request_target: permissions: {} @@ -138,10 +140,11 @@ jobs: No changes to OpenAPI specification found. See history of this comment for previous changes. - publish: + publish-unstable: name: OpenAPI - Publish Unstable Spec if: | github.event_name != 'pull_request_target' && + ${{ ! startsWith(github.ref, 'refs/tags/v') }} && contains(github.repository_owner, 'jellyfin') runs-on: ubuntu-latest needs: @@ -201,3 +204,67 @@ jobs: sudo ln -s unstable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-unstable_previous.json fi ) 200>/run/workflows/openapi-unstable.lock + + publish-stable: + name: OpenAPI - Publish Stable Spec + if: | + startsWith(github.ref, 'refs/tags/v') && + contains(github.repository_owner, 'jellyfin') + runs-on: ubuntu-latest + needs: + - openapi-head + steps: + - name: Set version number + id: version + run: |- + echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV + - name: Download openapi-head + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + with: + name: openapi-head + path: openapi-head + - name: Upload openapi.json (stable) to repository server + uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7 + with: + host: "${{ secrets.REPO_HOST }}" + username: "${{ secrets.REPO_USER }}" + key: "${{ secrets.REPO_KEY }}" + source: openapi-head/openapi.json + strip_components: 1 + target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}" + - name: Move openapi.json (stable) into place + uses: appleboy/ssh-action@029f5b4aeeeb58fdfe1410a5d17f967dacf36262 # v1.0.3 + with: + host: "${{ secrets.REPO_HOST }}" + username: "${{ secrets.REPO_USER }}" + key: "${{ secrets.REPO_KEY }}" + debug: false + script_stop: false + script: | + if ! test -d /run/workflows; then + sudo mkdir -p /run/workflows + sudo chown ${{ secrets.REPO_USER }} /run/workflows + fi + ( + flock -x -w 300 200 || exit 1 + TGT_DIR="/srv/repository/main/openapi" + LAST_SPEC="$( ls -lt ${TGT_DIR}/stable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )" + # If new and previous spec don't differ (diff retcode 0), remove incoming and finish + if diff /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/${LAST_SPEC} &>/dev/null; then + rm -r /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }} + exit 0 + fi + # Move new spec into place + sudo mv /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json + # Delete previous jellyfin-openapi-stable_previous.json + sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json + # Move current jellyfin-openapi-stable.json symlink to jellyfin-openapi-stable_previous.json + sudo mv ${TGT_DIR}/jellyfin-openapi-stable.json ${TGT_DIR}/jellyfin-openapi-stable_previous.json + # Create new jellyfin-openapi-stable.json symlink + sudo ln -s stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-stable.json + # Check that the previous openapi stable spec link is correct + if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-stable_previous.json )" != "stable/${LAST_SPEC}" ]]; then + sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json + sudo ln -s stable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-stable_previous.json + fi + ) 200>/run/workflows/openapi-stable.lock diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 6add7e0b3..c394b25bd 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -422,7 +422,7 @@ namespace Emby.Server.Implementations // Initialize runtime stat collection if (ConfigurationManager.Configuration.EnableMetrics) { - DotNetRuntimeStatsBuilder.Default().StartCollecting(); + _disposableParts.Add(DotNetRuntimeStatsBuilder.Default().StartCollecting()); } var networkConfiguration = ConfigurationManager.GetNetworkConfiguration(); diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 67854a2a7..d5afac266 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -80,12 +80,14 @@ namespace Emby.Server.Implementations.IO public virtual string MakeAbsolutePath(string folderPath, string filePath) { // path is actually a stream - if (string.IsNullOrWhiteSpace(filePath) || filePath.Contains("://", StringComparison.Ordinal)) + if (string.IsNullOrWhiteSpace(filePath)) { return filePath; } - if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/') + var isAbsolutePath = Path.IsPathRooted(filePath) && (!OperatingSystem.IsWindows() || filePath[0] != '\\'); + + if (isAbsolutePath) { // absolute local path return filePath; @@ -97,17 +99,10 @@ namespace Emby.Server.Implementations.IO return filePath; } - var firstChar = filePath[0]; - if (firstChar == '/') - { - // for this we don't really know - return filePath; - } - var filePathSpan = filePath.AsSpan(); - // relative path - if (firstChar == '\\') + // relative path on windows + if (filePath[0] == '\\') { filePathSpan = filePathSpan.Slice(1); } diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json index c229f3538..1a69627fa 100644 --- a/Emby.Server.Implementations/Localization/Core/en-US.json +++ b/Emby.Server.Implementations/Localization/Core/en-US.json @@ -13,7 +13,7 @@ "DeviceOfflineWithName": "{0} has disconnected", "DeviceOnlineWithName": "{0} is connected", "External": "External", - "FailedLoginAttemptWithUserName": "Failed login try from {0}", + "FailedLoginAttemptWithUserName": "Failed login attempt from {0}", "Favorites": "Favorites", "Folders": "Folders", "Forced": "Forced", diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index bae201c70..ac453a5b0 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -321,7 +321,11 @@ namespace Emby.Server.Implementations.Localization // Try splitting by : to handle "Germany: FSK-18" if (rating.Contains(':', StringComparison.OrdinalIgnoreCase)) { - return GetRatingLevel(rating.AsSpan().RightPart(':').ToString()); + var ratingLevelRightPart = rating.AsSpan().RightPart(':'); + if (ratingLevelRightPart.Length != 0) + { + return GetRatingLevel(ratingLevelRightPart.ToString()); + } } // Handle prefix country code to handle "DE-18" @@ -332,8 +336,12 @@ namespace Emby.Server.Implementations.Localization // Extract culture from country prefix var culture = FindLanguageInfo(ratingSpan.LeftPart('-').ToString()); - // Check rating system of culture - return GetRatingLevel(ratingSpan.RightPart('-').ToString(), culture?.TwoLetterISOLanguageName); + var ratingLevelRightPart = ratingSpan.RightPart('-'); + if (ratingLevelRightPart.Length != 0) + { + // Check rating system of culture + return GetRatingLevel(ratingLevelRightPart.ToString(), culture?.TwoLetterISOLanguageName); + } } return null; diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index fd7696906..295fb8112 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -185,6 +185,7 @@ namespace Jellyfin.Server } catch (Exception ex) { + _restartOnShutdown = false; _logger.LogCritical(ex, "Error while starting server"); } finally diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 236e47f95..0f2ec1f8f 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -594,7 +594,7 @@ namespace MediaBrowser.Controller.Entities } var fanoutConcurrency = ConfigurationManager.Configuration.LibraryScanFanoutConcurrency; - var parallelism = fanoutConcurrency > 0 ? fanoutConcurrency : 2 * Environment.ProcessorCount; + var parallelism = fanoutConcurrency > 0 ? fanoutConcurrency : Environment.ProcessorCount; var actionBlock = new ActionBlock<int>( async i => diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 1da44b048..37160b3ba 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -412,7 +412,9 @@ public class NetworkManager : INetworkManager, IDisposable interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6); } - _interfaces = interfaces; + // Users may have complex networking configuration that multiple interfaces sharing the same IP address + // Only return one IP for binding, and let the OS handle the rest + _interfaces = interfaces.DistinctBy(iface => iface.Address).ToList(); } } @@ -1017,7 +1019,7 @@ public class NetworkManager : INetworkManager, IDisposable result = string.Empty; int count = _interfaces.Count; - if (count == 1 && (_interfaces[0].Equals(IPAddress.Any) || _interfaces[0].Equals(IPAddress.IPv6Any))) + if (count == 1 && (_interfaces[0].Address.Equals(IPAddress.Any) || _interfaces[0].Address.Equals(IPAddress.IPv6Any))) { // Ignore IPAny addresses. count = 0; @@ -1049,7 +1051,7 @@ public class NetworkManager : INetworkManager, IDisposable return true; } - _logger.LogWarning("{Source}: External request received, no matching external bind address found, trying internal addresses.", source); + _logger.LogDebug("{Source}: External request received, no matching external bind address found, trying internal addresses", source); } else { @@ -1087,7 +1089,7 @@ public class NetworkManager : INetworkManager, IDisposable if (extResult.Length == 0) { result = string.Empty; - _logger.LogWarning("{Source}: External request received, but no external interface found. Need to route through internal network.", source); + _logger.LogDebug("{Source}: External request received, but no external interface found. Need to route through internal network", source); return false; } diff --git a/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs b/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs index d991f5574..95a5b8179 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/IO/ManagedFileSystemTests.cs @@ -20,26 +20,37 @@ namespace Jellyfin.Server.Implementations.Tests.IO _sut = _fixture.Create<ManagedFileSystem>(); } - [Theory] + [SkippableTheory] [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")] - public void MakeAbsolutePathCorrectlyHandlesRelativeFilePaths( + [InlineData("/Volumes/Library/Sample/Music/Playlists/", "/mnt/Beethoven/Misc/Moonlight Sonata.mp3", "/mnt/Beethoven/Misc/Moonlight Sonata.mp3")] + public void MakeAbsolutePathCorrectlyHandlesRelativeFilePathsOnUnixLike( string folderPath, string filePath, string expectedAbsolutePath) { + Skip.If(OperatingSystem.IsWindows()); + + var generatedPath = _sut.MakeAbsolutePath(folderPath, filePath); + Assert.Equal(expectedAbsolutePath, generatedPath); + } + + [SkippableTheory] + [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")] + [InlineData(@"C:\\Volumes\Library\Sample\Music\Playlists\", @"D:\\Beethoven\Misc\Moonlight Sonata.mp3", @"D:\\Beethoven\Misc\Moonlight Sonata.mp3")] + public void MakeAbsolutePathCorrectlyHandlesRelativeFilePathsOnWindows( + string folderPath, + string filePath, + string expectedAbsolutePath) + { + Skip.If(!OperatingSystem.IsWindows()); + var generatedPath = _sut.MakeAbsolutePath(folderPath, filePath); - if (OperatingSystem.IsWindows()) - { - var expectedWindowsPath = expectedAbsolutePath.Replace('/', '\\'); - Assert.Equal(expectedWindowsPath, generatedPath.Split(':')[1]); - } - else - { - Assert.Equal(expectedAbsolutePath, generatedPath); - } + Assert.Equal(expectedAbsolutePath, generatedPath); } [Theory] diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index 0f7f5c194..0a4a836cb 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Runtime.InteropServices; using System.Threading.Tasks; using Emby.Server.Implementations.Localization; using MediaBrowser.Controller.Configuration; @@ -158,6 +159,20 @@ namespace Jellyfin.Server.Implementations.Tests.Localization } [Theory] + [InlineData("-NO RATING SHOWN-")] + [InlineData(":NO RATING SHOWN:")] + public async Task GetRatingLevel_Split_Success(string value) + { + var localizationManager = Setup(new ServerConfiguration() + { + UICulture = "en-US" + }); + await localizationManager.LoadAll(); + + Assert.Null(localizationManager.GetRatingLevel(value)); + } + + [Theory] [InlineData("Default", "Default")] [InlineData("HeaderLiveTV", "Live TV")] public void GetLocalizedString_Valid_Success(string key, string expected) |
