Pārlūkot izejas kodu

Merge remote-tracking branch 'jellyfinorigin/master' into feature/DatabaseRefactor

JPVenson 3 mēneši atpakaļ
vecāks
revīzija
feea5af2f3
24 mainītis faili ar 122 papildinājumiem un 142 dzēšanām
  1. 3 3
      .github/workflows/ci-codeql-analysis.yml
  2. 4 4
      .github/workflows/ci-compat.yml
  3. 8 8
      .github/workflows/ci-openapi.yml
  4. 1 1
      Directory.Packages.props
  5. 2 2
      Emby.Server.Implementations/Collections/CollectionManager.cs
  6. 1 9
      Emby.Server.Implementations/Library/LibraryManager.cs
  7. 1 1
      Emby.Server.Implementations/Localization/Core/hu.json
  8. 2 2
      Emby.Server.Implementations/Localization/Core/it.json
  9. 20 6
      Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
  10. 0 16
      Jellyfin.Api/Controllers/SystemController.cs
  11. 1 1
      Jellyfin.Server.Implementations/Users/UserManager.cs
  12. 0 6
      MediaBrowser.Common/Net/INetworkManager.cs
  13. 5 0
      MediaBrowser.Controller/Entities/Folder.cs
  14. 6 1
      MediaBrowser.Model/Entities/MetadataProvider.cs
  15. 6 1
      MediaBrowser.Model/Providers/ExternalIdMediaType.cs
  16. 0 47
      MediaBrowser.Model/System/WakeOnLanInfo.cs
  17. 19 0
      MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
  18. 27 0
      MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzRecordingId.cs
  19. 2 3
      MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
  20. 3 3
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
  21. 5 5
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
  22. 2 0
      src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs
  23. 0 23
      src/Jellyfin.Networking/Manager/NetworkManager.cs
  24. 4 0
      tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerTests.cs

+ 3 - 3
.github/workflows/ci-codeql-analysis.yml

@@ -27,11 +27,11 @@ jobs:
         dotnet-version: '9.0.x'
 
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
+      uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
       with:
         languages: ${{ matrix.language }}
         queries: +security-extended
     - name: Autobuild
-      uses: github/codeql-action/autobuild@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
+      uses: github/codeql-action/autobuild@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
+      uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10

+ 4 - 4
.github/workflows/ci-compat.yml

@@ -26,7 +26,7 @@ jobs:
           dotnet build Jellyfin.Server -o ./out
 
       - name: Upload Head
-        uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
+        uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
         with:
           name: abi-head
           retention-days: 14
@@ -65,7 +65,7 @@ jobs:
           dotnet build Jellyfin.Server -o ./out
 
       - name: Upload Head
-        uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
+        uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
         with:
           name: abi-base
           retention-days: 14
@@ -85,13 +85,13 @@ jobs:
 
     steps:
       - name: Download abi-head
-        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+        uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
         with:
           name: abi-head
           path: abi-head
 
       - name: Download abi-base
-        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+        uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
         with:
           name: abi-base
           path: abi-base

+ 8 - 8
.github/workflows/ci-openapi.yml

@@ -27,7 +27,7 @@ jobs:
       - name: Generate openapi.json
         run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
       - name: Upload openapi.json
-        uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
+        uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
         with:
           name: openapi-head
           retention-days: 14
@@ -61,7 +61,7 @@ jobs:
       - name: Generate openapi.json
         run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
       - name: Upload openapi.json
-        uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
+        uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
         with:
           name: openapi-base
           retention-days: 14
@@ -80,12 +80,12 @@ jobs:
       - openapi-base
     steps:
       - name: Download openapi-head
-        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+        uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
         with:
           name: openapi-head
           path: openapi-head
       - name: Download openapi-base
-        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+        uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
         with:
           name: openapi-base
           path: openapi-base
@@ -158,7 +158,7 @@ jobs:
         run: |-
           echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
       - name: Download openapi-head
-        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+        uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
         with:
           name: openapi-head
           path: openapi-head
@@ -172,7 +172,7 @@ jobs:
           strip_components: 1
           target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
       - name: Move openapi.json (unstable) into place
-        uses: appleboy/ssh-action@7eaf76671a0d7eec5d98ee897acda4f968735a17 # v1.2.0
+        uses: appleboy/ssh-action@8faa84277b88b6cd1455986f459aa66cf72bc8a3 # v1.2.1
         with:
           host: "${{ secrets.REPO_HOST }}"
           username: "${{ secrets.REPO_USER }}"
@@ -220,7 +220,7 @@ jobs:
         run: |-
           echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
       - name: Download openapi-head
-        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+        uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
         with:
           name: openapi-head
           path: openapi-head
@@ -234,7 +234,7 @@ jobs:
           strip_components: 1
           target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
       - name: Move openapi.json (stable) into place
-        uses: appleboy/ssh-action@7eaf76671a0d7eec5d98ee897acda4f968735a17 # v1.2.0
+        uses: appleboy/ssh-action@8faa84277b88b6cd1455986f459aa66cf72bc8a3 # v1.2.1
         with:
           host: "${{ secrets.REPO_HOST }}"
           username: "${{ secrets.REPO_USER }}"

+ 1 - 1
Directory.Packages.props

@@ -79,7 +79,7 @@
     <PackageVersion Include="System.Text.Json" Version="9.0.2" />
     <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.2" />
     <PackageVersion Include="TagLibSharp" Version="2.3.0" />
-    <PackageVersion Include="z440.atl.core" Version="6.16.0" />
+    <PackageVersion Include="z440.atl.core" Version="6.17.0" />
     <PackageVersion Include="TMDbLib" Version="2.2.0" />
     <PackageVersion Include="UTF.Unknown" Version="2.5.1" />
     <PackageVersion Include="Xunit.Priority" Version="1.1.6" />

+ 2 - 2
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -204,7 +204,7 @@ namespace Emby.Server.Implementations.Collections
         {
             if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
             {
-                throw new ArgumentException("No collection exists with the supplied Id");
+                throw new ArgumentException("No collection exists with the supplied collectionId " + collectionId);
             }
 
             List<BaseItem>? itemList = null;
@@ -218,7 +218,7 @@ namespace Emby.Server.Implementations.Collections
 
                 if (item is null)
                 {
-                    throw new ArgumentException("No item exists with the supplied Id");
+                    throw new ArgumentException("No item exists with the supplied Id " + id);
                 }
 
                 if (!currentLinkedChildrenIds.Contains(id))

+ 1 - 9
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -458,6 +458,7 @@ namespace Emby.Server.Implementations.Library
             foreach (var child in children)
             {
                 _itemRepository.DeleteItem(child.Id);
+                _cache.TryRemove(child.Id, out _);
             }
 
             _cache.TryRemove(item.Id, out _);
@@ -2631,15 +2632,6 @@ namespace Emby.Server.Implementations.Library
                 {
                     episode.ParentIndexNumber = season.IndexNumber;
                 }
-                else
-                {
-                    /*
-                    Anime series don't generally have a season in their file name, however,
-                    TVDb needs a season to correctly get the metadata.
-                    Hence, a null season needs to be filled with something. */
-                    // FIXME perhaps this would be better for TVDb parser to ask for season 1 if no season is specified
-                    episode.ParentIndexNumber = 1;
-                }
 
                 if (episode.ParentIndexNumber.HasValue)
                 {

+ 1 - 1
Emby.Server.Implementations/Localization/Core/hu.json

@@ -13,7 +13,7 @@
     "DeviceOnlineWithName": "{0} belépett",
     "FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet innen: {0}",
     "Favorites": "Kedvencek",
-    "Folders": "Könyvtárak",
+    "Folders": "Mappák",
     "Genres": "Műfajok",
     "HeaderAlbumArtists": "Albumelőadók",
     "HeaderContinueWatching": "Megtekintés folytatása",

+ 2 - 2
Emby.Server.Implementations/Localization/Core/it.json

@@ -58,8 +58,8 @@
     "NotificationOptionServerRestartRequired": "Riavvio del server necessario",
     "NotificationOptionTaskFailed": "Operazione pianificata fallita",
     "NotificationOptionUserLockedOut": "Utente bloccato",
-    "NotificationOptionVideoPlayback": "La riproduzione video è iniziata",
-    "NotificationOptionVideoPlaybackStopped": "La riproduzione video è stata interrotta",
+    "NotificationOptionVideoPlayback": "Riproduzione video iniziata",
+    "NotificationOptionVideoPlaybackStopped": "Riproduzione video interrotta",
     "Photos": "Foto",
     "Playlists": "Playlist",
     "Plugin": "Plugin",

+ 20 - 6
Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs

@@ -116,6 +116,7 @@ public partial class AudioNormalizationTask : IScheduledTask
                 {
                     a.LUFS = await CalculateLUFSAsync(
                         string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
+                        OperatingSystem.IsWindows(), // Wait for process to exit on Windows before we try deleting the concat file
                         cancellationToken).ConfigureAwait(false);
                 }
                 finally
@@ -142,7 +143,10 @@ public partial class AudioNormalizationTask : IScheduledTask
                     continue;
                 }
 
-                t.LUFS = await CalculateLUFSAsync(string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)), cancellationToken).ConfigureAwait(false);
+                t.LUFS = await CalculateLUFSAsync(
+                    string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)),
+                    false,
+                    cancellationToken).ConfigureAwait(false);
             }
 
             _itemRepository.SaveItems(tracks, cancellationToken);
@@ -162,7 +166,7 @@ public partial class AudioNormalizationTask : IScheduledTask
         ];
     }
 
-    private async Task<float?> CalculateLUFSAsync(string inputArgs, CancellationToken cancellationToken)
+    private async Task<float?> CalculateLUFSAsync(string inputArgs, bool waitForExit, CancellationToken cancellationToken)
     {
         var args = $"-hide_banner {inputArgs} -af ebur128=framelog=verbose -f null -";
 
@@ -189,18 +193,28 @@ public partial class AudioNormalizationTask : IScheduledTask
             }
 
             using var reader = process.StandardError;
+            float? lufs = null;
             await foreach (var line in reader.ReadAllLinesAsync(cancellationToken))
             {
                 Match match = LUFSRegex().Match(line);
-
                 if (match.Success)
                 {
-                    return float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
+                    lufs = float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
+                    break;
                 }
             }
 
-            _logger.LogError("Failed to find LUFS value in output");
-            return null;
+            if (lufs is null)
+            {
+                _logger.LogError("Failed to find LUFS value in output");
+            }
+
+            if (waitForExit)
+            {
+                await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
+            }
+
+            return lufs;
         }
     }
 }

+ 0 - 16
Jellyfin.Api/Controllers/SystemController.cs

@@ -212,20 +212,4 @@ public class SystemController : BaseJellyfinApiController
         FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
         return File(stream, "text/plain; charset=utf-8");
     }
-
-    /// <summary>
-    /// Gets wake on lan information.
-    /// </summary>
-    /// <response code="200">Information retrieved.</response>
-    /// <returns>An <see cref="IEnumerable{WakeOnLanInfo}"/> with the WakeOnLan infos.</returns>
-    [HttpGet("WakeOnLanInfo")]
-    [Authorize]
-    [Obsolete("This endpoint is obsolete.")]
-    [ProducesResponseType(StatusCodes.Status200OK)]
-    public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
-    {
-        var result = _networkManager.GetMacAddresses()
-            .Select(i => new WakeOnLanInfo(i));
-        return Ok(result);
-    }
 }

+ 1 - 1
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -114,7 +114,7 @@ namespace Jellyfin.Server.Implementations.Users
         // This is some regex that matches only on unicode "word" characters, as well as -, _ and @
         // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
         // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( )
-        [GeneratedRegex(@"^[\w\ \-'._@+]+$")]
+        [GeneratedRegex(@"^(?!\s)[\w\ \-'._@+]+(?<!\s)$")]
         private static partial Regex ValidUsernameRegex();
 
         /// <inheritdoc/>

+ 0 - 6
MediaBrowser.Common/Net/INetworkManager.cs

@@ -94,12 +94,6 @@ namespace MediaBrowser.Common.Net
         /// <returns>IP address to use, or loopback address if all else fails.</returns>
         string GetBindAddress(string source, out int? port);
 
-        /// <summary>
-        /// Get a list of all the MAC addresses associated with active interfaces.
-        /// </summary>
-        /// <returns>List of MAC addresses.</returns>
-        IReadOnlyList<PhysicalAddress> GetMacAddresses();
-
         /// <summary>
         /// Returns true if the address is part of the user defined LAN.
         /// </summary>

+ 5 - 0
MediaBrowser.Controller/Entities/Folder.cs

@@ -1203,6 +1203,11 @@ namespace MediaBrowser.Controller.Entities
                 return false;
             }
 
+            if (request.Is4K.HasValue)
+            {
+                return false;
+            }
+
             if (request.IsHD.HasValue)
             {
                 return false;

+ 6 - 1
MediaBrowser.Model/Entities/MetadataProvider.cs

@@ -84,6 +84,11 @@ namespace MediaBrowser.Model.Entities
         /// <summary>
         /// The TvMaze provider.
         /// </summary>
-        TvMaze = 19
+        TvMaze = 19,
+
+        /// <summary>
+        /// The MusicBrainz recording provider.
+        /// </summary>
+        MusicBrainzRecording = 20,
     }
 }

+ 6 - 1
MediaBrowser.Model/Providers/ExternalIdMediaType.cs

@@ -71,6 +71,11 @@ namespace MediaBrowser.Model.Providers
         /// <summary>
         /// A book.
         /// </summary>
-        Book = 13
+        Book = 13,
+
+        /// <summary>
+        /// A music recording.
+        /// </summary>
+        Recording = 14
     }
 }

+ 0 - 47
MediaBrowser.Model/System/WakeOnLanInfo.cs

@@ -1,47 +0,0 @@
-using System.Net.NetworkInformation;
-
-namespace MediaBrowser.Model.System
-{
-    /// <summary>
-    /// Provides the MAC address and port for wake-on-LAN functionality.
-    /// </summary>
-    public class WakeOnLanInfo
-    {
-        /// <summary>
-        /// Initializes a new instance of the <see cref="WakeOnLanInfo" /> class.
-        /// </summary>
-        /// <param name="macAddress">The MAC address.</param>
-        public WakeOnLanInfo(PhysicalAddress macAddress) : this(macAddress.ToString())
-        {
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="WakeOnLanInfo" /> class.
-        /// </summary>
-        /// <param name="macAddress">The MAC address.</param>
-        public WakeOnLanInfo(string macAddress) : this()
-        {
-            MacAddress = macAddress;
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="WakeOnLanInfo" /> class.
-        /// </summary>
-        public WakeOnLanInfo()
-        {
-            Port = 9;
-        }
-
-        /// <summary>
-        /// Gets the MAC address of the device.
-        /// </summary>
-        /// <value>The MAC address.</value>
-        public string? MacAddress { get; }
-
-        /// <summary>
-        /// Gets or sets the wake-on-LAN port.
-        /// </summary>
-        /// <value>The wake-on-LAN port.</value>
-        public int Port { get; set; }
-    }
-}

+ 19 - 0
MediaBrowser.Providers/MediaInfo/AudioFileProber.cs

@@ -19,6 +19,7 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.MediaInfo;
 using Microsoft.Extensions.Logging;
+using static Jellyfin.Extensions.StringExtensions;
 
 namespace MediaBrowser.Providers.MediaInfo
 {
@@ -400,6 +401,24 @@ namespace MediaBrowser.Providers.MediaInfo
                 }
             }
 
+            if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzRecording, out _))
+            {
+                if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_TRACKID", out var recordingMbId)
+                     || track.AdditionalFields.TryGetValue("MusicBrainz Track Id", out recordingMbId))
+                    && !string.IsNullOrEmpty(recordingMbId))
+                {
+                    audio.TrySetProviderId(MetadataProvider.MusicBrainzRecording, recordingMbId);
+                }
+                else if (track.AdditionalFields.TryGetValue("UFID", out var ufIdValue) && !string.IsNullOrEmpty(ufIdValue))
+                {
+                    // If tagged with MB Picard, the format is 'http://musicbrainz.org\0<recording MBID>'
+                    if (ufIdValue.Contains("musicbrainz.org", StringComparison.OrdinalIgnoreCase))
+                    {
+                        audio.TrySetProviderId(MetadataProvider.MusicBrainzRecording, ufIdValue.AsSpan().RightPart('\0').ToString());
+                    }
+                }
+            }
+
             // Save extracted lyrics if they exist,
             // and if the audio doesn't yet have lyrics.
             var lyrics = track.Lyrics.SynchronizedLyrics.Count > 0 ? track.Lyrics.FormatSynchToLRC() : track.Lyrics.UnsynchronizedLyrics;

+ 27 - 0
MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzRecordingId.cs

@@ -0,0 +1,27 @@
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+
+namespace MediaBrowser.Providers.Plugins.MusicBrainz;
+
+/// <summary>
+/// MusicBrainz recording id.
+/// </summary>
+public class MusicBrainzRecordingId : IExternalId
+{
+    /// <inheritdoc />
+    public string ProviderName => "MusicBrainz";
+
+    /// <inheritdoc />
+    public string Key => MetadataProvider.MusicBrainzRecording.ToString();
+
+    /// <inheritdoc />
+    public ExternalIdMediaType? Type => ExternalIdMediaType.Recording;
+
+    /// <inheritdoc />
+    public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/recording/{0}";
+
+    /// <inheritdoc />
+    public bool Supports(IHasProviderIds item) => item is Audio;
+}

+ 2 - 3
MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs

@@ -55,13 +55,12 @@ namespace MediaBrowser.Providers.Plugins.Omdb
 
             if (info.SeriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out string? seriesImdbId)
                 && !string.IsNullOrEmpty(seriesImdbId)
-                && info.IndexNumber.HasValue
-                && info.ParentIndexNumber.HasValue)
+                && info.IndexNumber.HasValue)
             {
                 result.HasMetadata = await _omdbProvider.FetchEpisodeData(
                     result,
                     info.IndexNumber.Value,
-                    info.ParentIndexNumber.Value,
+                    info.ParentIndexNumber ?? 1,
                     info.GetProviderId(MetadataProvider.Imdb),
                     seriesImdbId,
                     info.MetadataLanguage,

+ 3 - 3
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs

@@ -63,10 +63,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
                 return Enumerable.Empty<RemoteImageInfo>();
             }
 
-            var seasonNumber = episode.ParentIndexNumber;
+            var seasonNumber = episode.ParentIndexNumber ?? 1;
             var episodeNumber = episode.IndexNumber;
 
-            if (!seasonNumber.HasValue || !episodeNumber.HasValue)
+            if (!episodeNumber.HasValue)
             {
                 return Enumerable.Empty<RemoteImageInfo>();
             }
@@ -75,7 +75,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 
             // TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
             var episodeResult = await _tmdbClientManager
-                .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, series.DisplayOrder, null, null, cancellationToken)
+                .GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, series.DisplayOrder, null, null, cancellationToken)
                 .ConfigureAwait(false);
 
             var stills = episodeResult?.Images?.Stills;

+ 5 - 5
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs

@@ -47,7 +47,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
         public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
         {
             // The search query must either provide an episode number or date
-            if (!searchInfo.IndexNumber.HasValue || !searchInfo.ParentIndexNumber.HasValue)
+            if (!searchInfo.IndexNumber.HasValue)
             {
                 return Enumerable.Empty<RemoteSearchResult>();
             }
@@ -96,10 +96,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
                 return metadataResult;
             }
 
-            var seasonNumber = info.ParentIndexNumber;
+            var seasonNumber = info.ParentIndexNumber ?? 1;
             var episodeNumber = info.IndexNumber;
 
-            if (!seasonNumber.HasValue || !episodeNumber.HasValue)
+            if (!episodeNumber.HasValue)
             {
                 return metadataResult;
             }
@@ -112,7 +112,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
                 List<TvEpisode>? result = null;
                 for (int? episode = startindex; episode <= endindex; episode++)
                 {
-                    var episodeInfo = await _tmdbClientManager.GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episode.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken).ConfigureAwait(false);
+                    var episodeInfo = await _tmdbClientManager.GetEpisodeAsync(seriesTmdbId, seasonNumber, episode.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken).ConfigureAwait(false);
                     if (episodeInfo is not null)
                     {
                         (result ??= new List<TvEpisode>()).Add(episodeInfo);
@@ -156,7 +156,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
             else
             {
                 episodeResult = await _tmdbClientManager
-                    .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
+                    .GetEpisodeAsync(seriesTmdbId, seasonNumber, episodeNumber.Value, info.SeriesDisplayOrder, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
                     .ConfigureAwait(false);
             }
 

+ 2 - 0
src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs

@@ -4,6 +4,8 @@ using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities.Security;
 using Jellyfin.Data.Interfaces;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using Microsoft.EntityFrameworkCore.Metadata.Conventions;
 using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Server.Implementations;

+ 0 - 23
src/Jellyfin.Networking/Manager/NetworkManager.cs

@@ -49,11 +49,6 @@ public class NetworkManager : INetworkManager, IDisposable
     /// </summary>
     private bool _eventfire;
 
-    /// <summary>
-    /// List of all interface MAC addresses.
-    /// </summary>
-    private IReadOnlyList<PhysicalAddress> _macAddresses;
-
     /// <summary>
     /// Dictionary containing interface addresses and their subnets.
     /// </summary>
@@ -91,7 +86,6 @@ public class NetworkManager : INetworkManager, IDisposable
         _startupConfig = startupConfig;
         _initLock = new();
         _interfaces = new List<IPData>();
-        _macAddresses = new List<PhysicalAddress>();
         _publishedServerUrls = new List<PublishedServerUriOverride>();
         _networkEventLock = new();
         _remoteAddressFilter = new List<IPNetwork>();
@@ -215,7 +209,6 @@ public class NetworkManager : INetworkManager, IDisposable
 
     /// <summary>
     /// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state.
-    /// Generate a list of all active mac addresses that aren't loopback addresses.
     /// </summary>
     private void InitializeInterfaces()
     {
@@ -224,7 +217,6 @@ public class NetworkManager : INetworkManager, IDisposable
             _logger.LogDebug("Refreshing interfaces.");
 
             var interfaces = new List<IPData>();
-            var macAddresses = new List<PhysicalAddress>();
 
             try
             {
@@ -236,13 +228,6 @@ public class NetworkManager : INetworkManager, IDisposable
                     try
                     {
                         var ipProperties = adapter.GetIPProperties();
-                        var mac = adapter.GetPhysicalAddress();
-
-                        // Populate MAC list
-                        if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && !PhysicalAddress.None.Equals(mac))
-                        {
-                            macAddresses.Add(mac);
-                        }
 
                         // Populate interface list
                         foreach (var info in ipProperties.UnicastAddresses)
@@ -302,7 +287,6 @@ public class NetworkManager : INetworkManager, IDisposable
             _logger.LogDebug("Discovered {NumberOfInterfaces} interfaces.", interfaces.Count);
             _logger.LogDebug("Interfaces addresses: {Addresses}", interfaces.OrderByDescending(s => s.AddressFamily == AddressFamily.InterNetwork).Select(s => s.Address.ToString()));
 
-            _macAddresses = macAddresses;
             _interfaces = interfaces;
         }
     }
@@ -711,13 +695,6 @@ public class NetworkManager : INetworkManager, IDisposable
         return true;
     }
 
-    /// <inheritdoc/>
-    public IReadOnlyList<PhysicalAddress> GetMacAddresses()
-    {
-        // Populated in construction - so always has values.
-        return _macAddresses;
-    }
-
     /// <inheritdoc/>
     public IReadOnlyList<IPData> GetLoopbacks()
     {

+ 4 - 0
tests/Jellyfin.Server.Implementations.Tests/Users/UserManagerTests.cs

@@ -23,6 +23,10 @@ namespace Jellyfin.Server.Implementations.Tests.Users
         [InlineData(" ")]
         [InlineData("")]
         [InlineData("special characters like & $ ? are not allowed")]
+        [InlineData("thishasaspaceontheend ")]
+        [InlineData(" thishasaspaceatthestart")]
+        [InlineData(" thishasaspaceatbothends ")]
+        [InlineData(" this has a space at both ends and inbetween ")]
         public void ThrowIfInvalidUsername_WhenInvalidUsername_ThrowsArgumentException(string username)
         {
             Assert.Throws<ArgumentException>(() => UserManager.ThrowIfInvalidUsername(username));